From 137da50d510af1902961c778defae5c0ae9e93f8 Mon Sep 17 00:00:00 2001 From: Tony Fischer Date: Sun, 26 Jan 2025 00:16:43 +0100 Subject: [PATCH 001/728] docs(docs): add snacks.nvim picker integration docs (#726) --- README.md | 21 +++++++++++++++++++++ doc/CopilotChat.txt | 17 +++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/README.md b/README.md index 258c78d1..69b97a1d 100644 --- a/README.md +++ b/README.md @@ -681,6 +681,27 @@ Requires [fzf-lua](https://github.com/ibhagwan/fzf-lua) plugin to be installed. +
+snacks.nvim integration + +Requires [snacks.nvim](https://github.com/folke/snacks.nvim) plugin to be installed and the [picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) to be configured. + +```lua +-- lazy.nvim keys + + -- Show prompts actions with snacks.nvim picker + { + "ccp", + function() + local actions = require("CopilotChat.actions") + require("CopilotChat.integrations.snacks").pick(actions.prompt_actions()) + end, + desc = "CopilotChat - Prompt actions", + }, +``` + +
+
render-markdown integration diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f32f7033..8360de43 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -716,6 +716,23 @@ Requires fzf-lua plugin to be installed. desc = "CopilotChat - Prompt actions", }, < +snacks.nvim integration ~ + +Requires snacks.nvim plugin to be installed and the picker to be configured. + +>lua + -- lazy.nvim keys + + -- Show prompts actions with snacks.nvim picker + { + "ccp", + function() + local actions = require("CopilotChat.actions") + require("CopilotChat.integrations.snacks").pick(actions.prompt_actions()) + end, + desc = "CopilotChat - Prompt actions", + }, +< render-markdown integration ~ From ae0caaf75023b93360e4b9d125583a37a76c7530 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jan 2025 23:17:03 +0000 Subject: [PATCH 002/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8360de43..4c4531cd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -716,9 +716,13 @@ Requires fzf-lua plugin to be installed. desc = "CopilotChat - Prompt actions", }, < + snacks.nvim integration ~ -Requires snacks.nvim plugin to be installed and the picker to be configured. +Requires snacks.nvim plugin to be +installed and the picker + to be +configured. >lua -- lazy.nvim keys From 7444ffb7fd4f0c22ea0728bfad84ea506bb1eb26 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 07:17:51 +0800 Subject: [PATCH 003/728] docs: add tku137 as a contributor for doc (#727) --- .all-contributorsrc | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 59b41fcd..7498853e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -318,7 +318,7 @@ "name": "Tony Fischer", "avatar_url": "https://avatars.githubusercontent.com/u/3052212?v=4", "profile": "https://github.com/tku137", - "contributions": ["code"] + "contributions": ["code", "doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 69b97a1d..85675de2 100644 --- a/README.md +++ b/README.md @@ -811,7 +811,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d abdennourzahaf
abdennourzahaf

📖 Josiah
Josiah

💻 - Tony Fischer
Tony Fischer

💻 + Tony Fischer
Tony Fischer

💻 📖 From 1b1542b5c7993bf1e9bbee42cbb22c4371d69ee5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jan 2025 23:18:14 +0000 Subject: [PATCH 004/728] 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 4c4531cd..46425c82 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -792,7 +792,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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖This project follows the all-contributors specification. Contributions of any kind are welcome! From 9b8e94438a6fca2b564ebdd37bea542a67cad767 Mon Sep 17 00:00:00 2001 From: Kohei Wada <64008205+Kohei-Wada@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:02:58 +0900 Subject: [PATCH 005/728] feat: Add theme options to select Telescope themes (#710) * feat: Add theme options to select Telescope themes Added theme options to the M.pick function to allow selection of dropdown, ivy, and cursor themes. * refactor: remove theme argument and integrate into opts Simplified code by removing the theme argument and integrating it into opts. * fix: correct default theme settings Fixed to apply the default theme when opts and opts.theme are not set. --- lua/CopilotChat/integrations/telescope.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/integrations/telescope.lua b/lua/CopilotChat/integrations/telescope.lua index e0c4dcee..e636aeb6 100644 --- a/lua/CopilotChat/integrations/telescope.lua +++ b/lua/CopilotChat/integrations/telescope.lua @@ -19,7 +19,11 @@ function M.pick(pick_actions, opts) end utils.return_to_normal_mode() - opts = themes.get_dropdown(opts or {}) + + if not (opts and opts.theme) then + opts = themes.get_dropdown(opts or {}) + end + pickers .new(opts, { prompt_title = pick_actions.prompt, From eddedd3978dc2da91c52e27b0ceefef8a1ed7ff0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Jan 2025 07:03:16 +0000 Subject: [PATCH 006/728] 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 46425c82..9959bd67 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 January 25 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 January 26 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From ade6c93a270c19ea7af4a50c407f355a7cb21a87 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 08:04:29 +0100 Subject: [PATCH 007/728] docs: add Kohei-Wada as a contributor for code (#728) * 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 7498853e..0cf929fb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -319,6 +319,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/3052212?v=4", "profile": "https://github.com/tku137", "contributions": ["code", "doc"] + }, + { + "login": "Kohei-Wada", + "name": "Kohei Wada", + "avatar_url": "https://avatars.githubusercontent.com/u/64008205?v=4", + "profile": "https://qiita.com/program3152019", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 85675de2..87bebd8f 100644 --- a/README.md +++ b/README.md @@ -812,6 +812,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d abdennourzahaf
abdennourzahaf

📖 Josiah
Josiah

💻 Tony Fischer
Tony Fischer

💻 📖 + Kohei Wada
Kohei Wada

💻 From 920bf9f8af0d13c1745c419254a9c0c39e27b02c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 26 Jan 2025 23:39:46 +0100 Subject: [PATCH 008/728] Update minimal neovim version to 0.10.0 and fix selection type Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/health.lua | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 87bebd8f..d6f5b291 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 # Requirements -- [Neovim 0.9.5+](https://neovim.io/) - Older versions are not supported, and for best compatibility 0.10.0+ is preferred +- [Neovim 0.10.0+](https://neovim.io/) - Older versions are not officially supported - [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 diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b3993d63..1cb20e13 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -56,7 +56,7 @@ local utils = require('CopilotChat.utils') ---@field temperature number? ---@field headless boolean? ---@field callback fun(response: string, source: CopilotChat.source)? ----@field selection nil|fun(source: CopilotChat.source):CopilotChat.select.selection? +---@field selection false|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/health.lua b/lua/CopilotChat/health.lua index 3908c74c..1ea82884 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -57,10 +57,10 @@ function M.check() 'nvim: outdated, please upgrade to a up to date nightly version. See "https://github.com/neovim/neovim".' ) end - elseif vim.fn.has('nvim-0.9.5') == 1 then + elseif vim.fn.has('nvim-0.10.0') == 1 then ok('nvim: ' .. vim_version) else - error('nvim: unsupported, please upgrade to 0.9.5 or later. See "https://neovim.io/".') + error('nvim: unsupported, please upgrade to 0.10.0 or later. See "https://neovim.io/".') end start('CopilotChat.nvim [commands]') From ebf60a1add160da7a6b76596c9804954c7971b6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Jan 2025 22:40:50 +0000 Subject: [PATCH 009/728] 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 9959bd67..3f08df61 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -29,7 +29,7 @@ Table of Contents *CopilotChat-table-of-contents* ============================================================================== 1. Requirements *CopilotChat-requirements* -- Neovim 0.9.5+ - Older versions are not supported, and for best compatibility 0.10.0+ is preferred +- Neovim 0.10.0+ - Older versions are not officially supported - 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 @@ -792,7 +792,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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 71b7104a0fcafd38b256c3050ea450a8a12ded17 Mon Sep 17 00:00:00 2001 From: Sebastian Yaghoubi <79172513+syaghoubi00@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:56:39 -0800 Subject: [PATCH 010/728] docs(readme): add perplexity search keybind to tips (#732) --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index d6f5b291..536df437 100644 --- a/README.md +++ b/README.md @@ -726,6 +726,35 @@ require('CopilotChat').setup({
+
+Ask a quick question with Perplexity + +Requires [PerplexityAI Agent](https://github.com/marketplace/perplexityai) to be added to [GitHub](https://github.com/) account. + +This sets the `selection = false` to be able to ask generic questions unrelated to current code. + +```lua +-- lazy.nvim keys + + -- Ask the Perplexity agent a quick question + { + "ccs", + function() + local input = vim.fn.input("Perplexity: ") + if input ~= "" then + require("CopilotChat").ask(input, { + agent = "perplexityai", + selection = false, + }) + end + end, + desc = "CopilotChat - Perplexity Search", + mode = { "n", "v" }, + }, +``` + +
+ # Roadmap - Improved caching for context (persistence through restarts/smarter caching) From e6b905157e1d10f2f60bfa2103011f3dbc94555c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Jan 2025 02:57:04 +0000 Subject: [PATCH 011/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3f08df61..f61ad5b6 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 January 26 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 January 27 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -759,6 +759,34 @@ installed. }) < +Ask a quick question with Perplexity ~ + +Requires PerplexityAI Agent to be +added to GitHub account. + +This sets the `selection = false` to be able to ask generic questions unrelated +to current code. + +>lua + -- lazy.nvim keys + + -- Ask the Perplexity agent a quick question + { + "ccs", + function() + local input = vim.fn.input("Perplexity: ") + if input ~= "" then + require("CopilotChat").ask(input, { + agent = "perplexityai", + selection = false, + }) + end + end, + desc = "CopilotChat - Perplexity Search", + mode = { "n", "v" }, + }, +< + ============================================================================== 6. Roadmap *CopilotChat-roadmap* From dafae8d295c330c9ece28e86499a9aea853fb228 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:00:10 +0800 Subject: [PATCH 012/728] docs: add syaghoubi00 as a contributor for doc (#733) 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 0cf929fb..bf588a8a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -326,6 +326,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/64008205?v=4", "profile": "https://qiita.com/program3152019", "contributions": ["code"] + }, + { + "login": "syaghoubi00", + "name": "Sebastian Yaghoubi", + "avatar_url": "https://avatars.githubusercontent.com/u/79172513?v=4", + "profile": "https://zags.dev", + "contributions": ["doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 536df437..6663d9f6 100644 --- a/README.md +++ b/README.md @@ -842,6 +842,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Josiah
Josiah

💻 Tony Fischer
Tony Fischer

💻 📖 Kohei Wada
Kohei Wada

💻 + Sebastian Yaghoubi
Sebastian Yaghoubi

📖 From 1b375c24602680b5fe28c3c223a822111857dc37 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Jan 2025 03:00:36 +0000 Subject: [PATCH 013/728] 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 f61ad5b6..c89da385 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -820,7 +820,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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖This project follows the all-contributors specification. Contributions of any kind are welcome! From d8532572e8303c8fe4fdef6fc7eae3fbd442011d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 4 Feb 2025 01:48:02 +0100 Subject: [PATCH 014/728] feat: add quickfix list context support This commit adds support for the quickfix list context in CopilotChat. The quickfix context allows including the contents of files listed in the quickfix list as part of the chat context. The implementation includes: - New quickfix context type in config.lua - Function to extract and process quickfix list files - Updated documentation in README.md with the new context type Signed-off-by: Tomas Slusny --- README.md | 4 ++++ lua/CopilotChat/config.lua | 6 +++++ lua/CopilotChat/context.lua | 44 +++++++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6663d9f6..22a3089f 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,7 @@ Default contexts are: - `git:staged` - Includes staged changes in chat context. - `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). +- `quickfix` - Includes quickfix list file contents in chat context. You can define custom contexts like this: @@ -487,6 +488,9 @@ Also see [here](/lua/CopilotChat/config.lua): register = { -- see config.lua for implementation }, + quickfix = { + -- see config.lua for implementation + }, }, -- default prompts diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 1cb20e13..3df75b6e 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -283,6 +283,12 @@ return { } end, }, + quickfix = { + description = 'Includes quickfix list file contents in chat context.', + resolve = function() + return context.quickfix() + end, + }, }, -- default prompts diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 56c27424..fa8ea745 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -282,6 +282,8 @@ end ---@param filetype string ---@return CopilotChat.context.embed? local function get_file(filename, filetype) + notify.publish(notify.STATUS, 'Reading file ' .. filename) + local modified = utils.file_mtime(filename) if not modified then return nil @@ -377,8 +379,6 @@ 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 @@ -532,6 +532,46 @@ function M.register(register) } end +--- Get the content of the quickfix list +---@return table +function M.quickfix() + async.util.scheduler() + + local items = vim.fn.getqflist() + if not items or #items == 0 then + return {} + end + + local unique_files = {} + for _, item in ipairs(items) do + local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) + if filename then + unique_files[filename] = true + end + end + + local 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, vim.tbl_values(unique_files)) + ) + + local out = {} + for _, file in ipairs(files) do + local file_data = get_file(file.name, file.ft) + if file_data then + table.insert(out, file_data) + end + end + return out +end + --- Filter embeddings based on the query ---@param copilot CopilotChat.Copilot ---@param prompt string From 6e36e5c212daacb82438eacd2ca3c2d8b4ebe48c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Feb 2025 00:57:45 +0000 Subject: [PATCH 015/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index c89da385..daad55d4 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 January 27 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 04 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -303,6 +303,7 @@ prompt by using `:` followed by the input (or pressing `complete` key after - `git:staged` - Includes staged changes in chat context. - `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). +- `quickfix` - Includes quickfix list file contents in chat context. You can define custom contexts like this: @@ -534,6 +535,9 @@ Also see here : register = { -- see config.lua for implementation }, + quickfix = { + -- see config.lua for implementation + }, }, -- default prompts From b7386a141d95b2f41292319694c31b38b81561b1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 4 Feb 2025 01:59:11 +0100 Subject: [PATCH 016/728] feat: add quickfix list support for chat answers Split existing quickfix mapping (gq) into two separate mappings: - gqa: adds all chat answers to quickfix list - gqd: adds all code diffs to quickfix list (previous gq functionality) This allows for easier navigation between different parts of the chat history, particularly when reviewing AI responses. Signed-off-by: Tomas Slusny --- README.md | 8 ++++++-- lua/CopilotChat/config.lua | 5 ++++- lua/CopilotChat/init.lua | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 22a3089f..2b2ca3c8 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,8 @@ See [@deathbeam](https://github.com/deathbeam) for [configuration](https://githu - `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 +- `gqa` - Add all answers from chat to quickfix list +- `gqd` - Add all diffs from chat to quickfix list - `gy` - Yank nearest diff to register (defaults to `"`) - `gd` - Show diff between source and nearest diff - `gi` - Show info about current chat (model, agent, system prompt) @@ -547,8 +548,11 @@ Also see [here](/lua/CopilotChat/config.lua): jump_to_diff = { normal = 'gj', }, + quickfix_answers = { + normal = 'gqa', + }, quickfix_diffs = { - normal = 'gq', + normal = 'gqd', }, yank_diff = { normal = 'gy', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 3df75b6e..648459fd 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -383,8 +383,11 @@ return { jump_to_diff = { normal = 'gj', }, + quickfix_answers = { + normal = 'gqa', + }, quickfix_diffs = { - normal = 'gq', + normal = 'gqd', }, yank_diff = { normal = 'gy', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 914ada6b..2dc6b0b1 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1116,6 +1116,39 @@ function M.setup(config) ) end) + map_key('quickfix_answers', bufnr, function() + local items = {} + for i, section in ipairs(state.chat.sections) do + if section.answer then + local prev_section = state.chat.sections[i - 1] + local text = '' + if prev_section then + text = vim.trim( + table.concat( + vim.api.nvim_buf_get_lines( + bufnr, + prev_section.start_line - 1, + prev_section.end_line, + false + ), + ' ' + ) + ) + end + + table.insert(items, { + bufnr = bufnr, + lnum = section.start_line, + end_lnum = section.end_line, + text = text, + }) + end + end + + vim.fn.setqflist(items) + vim.cmd('copen') + end) + map_key('quickfix_diffs', bufnr, function() local selection = get_selection(state.chat.config) local items = {} From 52f72bbc68b86446259ea907b0ba3c0bf0cda588 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Feb 2025 01:04:05 +0000 Subject: [PATCH 017/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index daad55d4..9ff18697 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -148,7 +148,8 @@ CHAT MAPPINGS *CopilotChat-chat-mappings* - `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 +- `gqa` - Add all answers from chat to quickfix list +- `gqd` - Add all diffs from chat to quickfix list - `gy` - Yank nearest diff to register (defaults to `"`) - `gd` - Show diff between source and nearest diff - `gi` - Show info about current chat (model, agent, system prompt) @@ -594,8 +595,11 @@ Also see here : jump_to_diff = { normal = 'gj', }, + quickfix_answers = { + normal = 'gqa', + }, quickfix_diffs = { - normal = 'gq', + normal = 'gqd', }, yank_diff = { normal = 'gy', From cb73689b1aec60ed90e65c52581da135592a1cf1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 4 Feb 2025 02:30:15 +0100 Subject: [PATCH 018/728] feat: improve prompt readability and clarity Refactor prompts to be more concise and focused while maintaining core functionality: - Simplify copilot base instruction to focus on practical solutions - Restructure explain prompt into clear bullet points - Streamline code review prompt with consistent format - Improve generate prompt with clearer block structure requirements The changes make the prompts easier to maintain and understand while preserving all key functionality. --- lua/CopilotChat/prompts.lua | 82 +++++++++++++++---------------------- 1 file changed, 32 insertions(+), 50 deletions(-) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 7a7a0cb8..dc038264 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -17,77 +17,59 @@ The user is working on a %s machine. Please respond with system specific command ) M.COPILOT_INSTRUCTIONS = [[ -You are an AI programming assistant. +You are a code-focused AI programming assistant that specializes in practical software engineering solutions. ]] .. 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. -When examining code pay close attention to diagnostics as well. When explaining diagnostics, include diagnostic content in your response. +You are a programming instructor focused on clear, practical explanations. +When explaining code: +- Balance high-level concepts with implementation details +- Highlight key programming principles and patterns +- Address any code diagnostics or warnings ]] .. base 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. -- Overly complex expressions that could benefit from simplification. -- High nesting levels that make the code difficult to follow. -- The use of excessively long names for variables or functions. -- Any inconsistencies in naming, formatting, or overall coding style. -- Repetitive code patterns that could be more efficiently handled through abstraction or optimization. - -Your feedback must be concise, directly addressing each identified issue with: -- The specific line number(s) where the issue is found. -- A clear description of the problem. -- A concrete suggestion for how to improve or correct the issue. - -Format your feedback as follows: +Review the code for readability and maintainability issues. Report problems in this format: line=: - -If the issue is related to a range of lines, use the following format: 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. -line=10: Using camel case here is unconventional for lua. Use snake case instead. -line=11-15: Excessive nesting makes the code hard to follow. Consider refactoring to reduce nesting levels. - -If the code snippet has no readability issues, simply confirm that the code is clear and well-written as is. + +Check for: +- Unclear or non-conventional naming +- Comment quality (missing or unnecessary) +- Complex expressions needing simplification +- Deep nesting +- Inconsistent style +- Code duplication + +Multiple issues on one line should be separated by semicolons. +End with: "**`To clear buffer highlights, please ask a different question.`**" + +If no issues found, confirm the code is well-written. ]] M.COPILOT_GENERATE = M.COPILOT_INSTRUCTIONS .. [[ 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. - -2. *DO NOT* include any explanations, comments, or line numbers in your response. - -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. - -5. *DO NOT* omit any parts of the code, even if they are unchanged. +1. Split your response into minimal, focused code changes to produce the shortest possible diffs. -6. Maintain the *SAME INDENTATION* in the returned code as in the source code +2. IMPORTANT: Every code block MUST have a header with this exact format: + [file:]() line:- + The line numbers are REQUIRED - never omit them. -7. *ONLY* return the new code snippets to be updated, *DO NOT* return the entire file content. +3. Return ONLY the modified code blocks - no explanations or comments. -8. If the response do not fits in a single message, split the response into multiple messages. +4. Each code block should contain: + - Only the specific lines that need to change + - Exact indentation matching the source + - Complete code that can directly replace the original -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. +5. When fixing code, check and address any diagnostics issues. -10. When fixing code pay close attention to diagnostics as well. When fixing diagnostics, include diagnostic content in your response. +6. If multiple separate changes are needed, split them into individual blocks with appropriate headers. -Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file. +Remember: Your response should ONLY contain file headers with line numbers and code blocks for direct replacement. ]] return M From eaa094dc18bcd10c4f7358b6ab2548edcf17ee99 Mon Sep 17 00:00:00 2001 From: johncming Date: Tue, 4 Feb 2025 23:48:26 +0800 Subject: [PATCH 019/728] context: Use vim.tbl_keys instead of tbl_values for quickfix file entries We use files not booleans. This pull request includes a small change to the lua/CopilotChat/context.lua file. The change modifies the quickfix function to use vim.tbl_keys instead of vim.tbl_values when processing unique_files. lua/CopilotChat/context.lua: Changed the quickfix function to use vim.tbl_keys instead of vim.tbl_values when iterating over unique_files. Co-authored-by: johncming --- lua/CopilotChat/context.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index fa8ea745..ccafc59f 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -559,7 +559,7 @@ function M.quickfix() name = utils.filepath(file), ft = utils.filetype(file), } - end, vim.tbl_values(unique_files)) + end, vim.tbl_keys(unique_files)) ) local out = {} From ded101f2cc95962d1bcd904044ec41481f469da6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:50:06 +0100 Subject: [PATCH 020/728] docs: add johncming as a contributor for code (#743) * 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 bf588a8a..bce4c3c7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -333,6 +333,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/79172513?v=4", "profile": "https://zags.dev", "contributions": ["doc"] + }, + { + "login": "johncming", + "name": "johncming", + "avatar_url": "https://avatars.githubusercontent.com/u/11719334?v=4", + "profile": "https://github.com/johncming", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2b2ca3c8..da8f7417 100644 --- a/README.md +++ b/README.md @@ -851,6 +851,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Tony Fischer
Tony Fischer

💻 📖 Kohei Wada
Kohei Wada

💻 Sebastian Yaghoubi
Sebastian Yaghoubi

📖 + johncming
johncming

💻 From 89ce5e97e2f7fa8f4800e9b7002658bbe78ff6e9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 4 Feb 2025 17:45:30 +0100 Subject: [PATCH 021/728] Handle truncated response in COPILOT_GENERATE prompt Signed-off-by: Tomas Slusny --- lua/CopilotChat/prompts.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index dc038264..1f0bc489 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -69,6 +69,12 @@ Your task is to modify the provided code according to the user's request. Follow 6. If multiple separate changes are needed, split them into individual blocks with appropriate headers. +7. If response would be too long: + - Never cut off in the middle of a code block + - Complete the current code block + - End with "**`[Response truncated] Please ask for the remaining changes.`**" + - Next response should continue with the next code block + Remember: Your response should ONLY contain file headers with line numbers and code blocks for direct replacement. ]] From 36ae22be2374e41b06366a78ba5c883035abac31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Feb 2025 16:46:18 +0000 Subject: [PATCH 022/728] 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 9ff18697..a2202a7a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -828,7 +828,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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 4c4f9b82dabfc437e5a33b9c1edcccdf855202d5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 5 Feb 2025 00:11:42 +0100 Subject: [PATCH 023/728] feat(diff): add option to show full diff with diff mode Add new `full_diff` option to show_diff mapping which allows showing a full diff view instead of unified diff view. When enabled, it will show the modified file content side by side with original file using Vim's built-in diff mode. The full diff mode provides better visualization of changes and allows using Vim's diff navigation commands to review changes. Closes #705 Signed-off-by: Tomas Slusny --- README.md | 3 +- lua/CopilotChat/config.lua | 12 +++-- lua/CopilotChat/init.lua | 31 +++++++------ lua/CopilotChat/ui/diff.lua | 87 +++++++++++++++++++++++++++++-------- 4 files changed, 95 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index da8f7417..c0497881 100644 --- a/README.md +++ b/README.md @@ -556,10 +556,11 @@ Also see [here](/lua/CopilotChat/config.lua): }, yank_diff = { normal = 'gy', - register = '"', + register = '"', -- Default register to use for yanking }, show_diff = { normal = 'gd', + full_diff = false, -- Show full diff instead of unified diff when showing diff window }, show_info = { normal = 'gi', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 648459fd..44182371 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -30,9 +30,12 @@ local utils = require('CopilotChat.utils') ---@field insert string? ---@field detail string? ----@class CopilotChat.config.mapping.register : CopilotChat.config.mapping +---@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping ---@field register string? +---@class CopilotChat.config.mapping.show_diff : CopilotChat.config.mapping +---@field full_diff boolean? + ---@class CopilotChat.config.mappings ---@field complete CopilotChat.config.mapping? ---@field close CopilotChat.config.mapping? @@ -42,8 +45,8 @@ 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.register? ----@field show_diff CopilotChat.config.mapping? +---@field yank_diff CopilotChat.config.mapping.yank_diff? +---@field show_diff CopilotChat.config.mapping.show_diff? ---@field show_info CopilotChat.config.mapping? ---@field show_context CopilotChat.config.mapping? ---@field show_help CopilotChat.config.mapping? @@ -391,10 +394,11 @@ return { }, yank_diff = { normal = 'gy', - register = '"', + register = '"', -- Default register to use for yanking }, show_diff = { normal = 'gd', + full_diff = false, -- Show full diff instead of unified diff when showing diff window }, show_info = { normal = 'gi', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 2dc6b0b1..ca608fa3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -6,11 +6,6 @@ local context = require('CopilotChat.context') local prompts = require('CopilotChat.prompts') 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' local WORD = '([^%s]+)' @@ -959,34 +954,38 @@ function M.setup(config) if state.overlay then state.overlay:delete() end - state.overlay = Overlay('copilot-overlay', overlay_help, function(bufnr) + state.overlay = require('CopilotChat.ui.overlay')('copilot-overlay', overlay_help, function(bufnr) map_key('close', bufnr, function() state.overlay:restore(state.chat.winnr, state.chat.bufnr) end) end) if not state.debug then - state.debug = Debug() + state.debug = require('CopilotChat.ui.debug')() end if state.diff then state.diff:delete() end - state.diff = Diff(diff_help, function(bufnr) - map_key('close', bufnr, function() - state.diff:restore(state.chat.winnr, state.chat.bufnr) - end) + state.diff = require('CopilotChat.ui.diff')( + M.config.mappings.show_diff.full_diff, + diff_help, + function(bufnr) + map_key('close', bufnr, function() + state.diff:restore(state.chat.winnr, state.chat.bufnr) + end) - map_key('accept_diff', bufnr, function() - apply_diff(state.diff:get_diff(), state.chat.config) - end) - end) + map_key('accept_diff', bufnr, function() + apply_diff(state.diff:get_diff(), state.chat.config) + end) + end + ) if state.chat then state.chat:close(state.source and state.source.bufnr or nil) state.chat:delete() end - state.chat = Chat( + state.chat = require('CopilotChat.ui.chat')( M.config.question_header, M.config.answer_header, M.config.separator, diff --git a/lua/CopilotChat/ui/diff.lua b/lua/CopilotChat/ui/diff.lua index 4cf92805..be651183 100644 --- a/lua/CopilotChat/ui/diff.lua +++ b/lua/CopilotChat/ui/diff.lua @@ -14,13 +14,17 @@ 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) +---@field augroup number +---@field full_diff boolean +local Diff = class(function(self, full_diff, 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.augroup = vim.api.nvim_create_augroup('CopilotChatDiff', { clear = true }) + self.full_diff = full_diff self.diff = nil end, Overlay) @@ -31,27 +35,76 @@ function Diff:show(diff, winnr) self:validate() vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) - Overlay.show( - self, - tostring(vim.diff(diff.reference, diff.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 = #diff.reference, - })), - winnr, - diff.filetype, - 'diff' - ) + if not self.full_diff then + -- Create unified diff view + Overlay.show( + self, + tostring(vim.diff(diff.reference, diff.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 = #diff.reference, + })), + winnr, + diff.filetype, + 'diff' + ) + + return + end + + -- Create modified version by applying the change + local modified = {} + if diff.bufnr and utils.buf_valid(diff.bufnr) then + modified = vim.api.nvim_buf_get_lines(diff.bufnr, 0, -1, false) + end + local change_lines = vim.split(diff.change, '\n') + + -- Replace the lines in the modified content + if #modified > 0 then + local start_idx = diff.start_line - 1 + local end_idx = diff.end_line - 1 + for _ = start_idx, end_idx do + table.remove(modified, start_idx) + end + for i, line in ipairs(change_lines) do + table.insert(modified, start_idx + i - 1, line) + end + else + modified = change_lines + end + + Overlay.show(self, table.concat(modified, '\n'), winnr, diff.filetype) + + if diff.bufnr and vim.api.nvim_buf_is_valid(diff.bufnr) then + vim.cmd('diffthis') + vim.api.nvim_set_current_win(vim.fn.bufwinid(diff.bufnr)) + vim.api.nvim_win_set_cursor(0, { diff.start_line, 0 }) + vim.cmd('diffthis') + vim.api.nvim_set_current_win(winnr) + vim.api.nvim_win_set_cursor(winnr, { diff.start_line, 0 }) + + -- Link diff buffers lifecycle + vim.api.nvim_create_autocmd('BufWipeout', { + group = self.augroup, + buffer = self.bufnr, + callback = function() + vim.cmd('diffoff') + end, + }) + end end ---@param winnr number ---@param bufnr number function Diff:restore(winnr, bufnr) + if self.full_diff then + vim.cmd('diffoff') + end Overlay.restore(self, winnr, bufnr) vim.api.nvim_win_set_hl_ns(winnr, 0) end From 225f1bbf55b58f20bd2c006bc0703f8993b60050 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Feb 2025 23:16:11 +0000 Subject: [PATCH 024/728] 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 a2202a7a..23d51c22 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -603,10 +603,11 @@ Also see here : }, yank_diff = { normal = 'gy', - register = '"', + register = '"', -- Default register to use for yanking }, show_diff = { normal = 'gd', + full_diff = false, -- Show full diff instead of unified diff when showing diff window }, show_info = { normal = 'gi', From a19cf1d5f9c054434374e9b2783b72a9357ffb58 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 5 Feb 2025 00:18:43 +0100 Subject: [PATCH 025/728] docs(readme): add config options for mappings Add documentation for new configuration options: - `mappings.yank_diff.register` for customizing yank register - `mappings.show_diff.full_diff` for toggling between full and unified diff display Update examples section to demonstrate the new configuration options. --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0497881..ce9c7c2f 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ See [@deathbeam](https://github.com/deathbeam) for [configuration](https://githu - `gj` - Jump to section of nearest diff. If in different buffer, jumps there; creates buffer if needed (works best with `COPILOT_GENERATE` prompt) - `gqa` - Add all answers from chat to quickfix list - `gqd` - Add all diffs from chat to quickfix list -- `gy` - Yank nearest diff to register (defaults to `"`) -- `gd` - Show diff between source and nearest diff +- `gy` - Yank nearest diff to register (defaults to `"`). Use `mappings.yank_diff.register` config option to set register +- `gd` - Show diff between source and nearest diff. Use `mappings.show_diff.full_diff` boolean config option to show full diff instead of unified diff - `gi` - Show info about current chat (model, agent, system prompt) - `gc` - Show current chat context - `gh` - Show help message @@ -139,7 +139,7 @@ The mappings can be customized by setting the `mappings` table in your configura - `insert`: Key for insert mode - `detail`: Description of what the mapping does -For example, to change the submit prompt mapping: +For example, to change the submit prompt mapping or show_diff full diff option: ```lua { @@ -148,6 +148,9 @@ For example, to change the submit prompt mapping: normal = 's', insert = '' } + show_diff = { + full_diff = true + } } } ``` From 40b4e36ab6db1a32099e30f77571ece8fb6f0f8f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Feb 2025 23:19:35 +0000 Subject: [PATCH 026/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 23d51c22..0dabcec9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -150,8 +150,8 @@ CHAT MAPPINGS *CopilotChat-chat-mappings* - `gj` - Jump to section of nearest diff. If in different buffer, jumps there; creates buffer if needed (works best with `COPILOT_GENERATE` prompt) - `gqa` - Add all answers from chat to quickfix list - `gqd` - Add all diffs from chat to quickfix list -- `gy` - Yank nearest diff to register (defaults to `"`) -- `gd` - Show diff between source and nearest diff +- `gy` - Yank nearest diff to register (defaults to `"`). Use `mappings.yank_diff.register` config option to set register +- `gd` - Show diff between source and nearest diff. Use `mappings.show_diff.full_diff` boolean config option to show full diff instead of unified diff - `gi` - Show info about current chat (model, agent, system prompt) - `gc` - Show current chat context - `gh` - Show help message @@ -163,7 +163,7 @@ configuration. Each mapping can have: - `insert`: Key for insert mode - `detail`: Description of what the mapping does -For example, to change the submit prompt mapping: +For example, to change the submit prompt mapping or show_diff full diff option: >lua { @@ -172,6 +172,9 @@ For example, to change the submit prompt mapping: normal = 's', insert = '' } + show_diff = { + full_diff = true + } } } < From c7afeea53899d57327c0c812fcb3a754aaf4ccd5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 5 Feb 2025 02:48:08 +0100 Subject: [PATCH 027/728] Add support for default sticky prompts See #716 Signed-off-by: Tomas Slusny --- README.md | 15 ++++++++++++++- lua/CopilotChat/config.lua | 5 ++++- lua/CopilotChat/init.lua | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce9c7c2f..33af4062 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,17 @@ List all files in the workspace What is 1 + 11 ``` +You can also set default sticky prompts in the configuration: + +```lua +{ + sticky = { + '@models Using Mistral-small', + '#files:full', + } +} +``` + ## Models You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. @@ -417,11 +428,13 @@ Also see [here](/lua/CopilotChat/config.lua): -- 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 $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (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 + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + temperature = 0.1, -- GPT result temperature 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 diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 44182371..47b711ab 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -56,6 +56,7 @@ local utils = require('CopilotChat.utils') ---@field model string? ---@field agent string? ---@field context string|table|nil +---@field sticky string|table|nil ---@field temperature number? ---@field headless boolean? ---@field callback fun(response: string, source: CopilotChat.source)? @@ -90,11 +91,13 @@ return { -- 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 $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (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 + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + temperature = 0.1, -- GPT result temperature 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 diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ca608fa3..2b93c8cf 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -315,6 +315,23 @@ local function finish(start_of_chat) state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') + -- Add default sticky prompts after reset + if start_of_chat then + if M.config.sticky then + local last_prompt = state.last_prompt or '' + + if type(M.config.sticky) == 'table' then + for _, sticky in ipairs(M.config.sticky) do + last_prompt = last_prompt .. '\n> ' .. sticky + end + else + last_prompt = last_prompt .. '\n> ' .. M.config.sticky + end + + state.last_prompt = last_prompt + end + end + -- Reinsert sticky prompts from last prompt if state.last_prompt then local has_sticky = false From 058b8d3a922255a94da9dc5fab220fd7d7ea9fe9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 5 Feb 2025 02:00:27 +0000 Subject: [PATCH 028/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 0dabcec9..5816c572 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 04 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 05 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -254,6 +254,17 @@ and agent selection (see below). Example usage: What is 1 + 11 < +You can also set default sticky prompts in the configuration: + +>lua + { + sticky = { + '@models Using Mistral-small', + '#files:full', + } + } +< + MODELS *CopilotChat-models* @@ -464,11 +475,13 @@ Also see here : -- 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 $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (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 + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + temperature = 0.1, -- GPT result temperature 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 From d2a5a17732a2118f06fe76753cedadf4fd7baf60 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 5 Feb 2025 18:06:15 +0100 Subject: [PATCH 029/728] feat(context): add support for commit-specific git diffs Allow viewing diffs for specific commits by passing commit hash/reference to the git context. This extends the existing git context functionality which previously only supported staged and unstaged changes. The change includes: - Updated documentation in README.md to reflect new git context options - Modified context handler to support commit references - Updated config description to mention commit number support Signed-off-by: Tomas Slusny --- README.md | 9 ++++++++- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/context.lua | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33af4062..d34bcb9f 100644 --- a/README.md +++ b/README.md @@ -267,16 +267,23 @@ If context supports input, you can set the input in the prompt by using `:` foll Default contexts are: - `buffer` - Includes specified buffer in chat context. Supports input (default current). + - `buffer:` - Includes buffer with specified number in chat context. - `buffers` - Includes all buffers in chat context. Supports input (default listed). + - `buffers:listed` - Includes only listed buffers in chat context. + - `buffers:all` - Includes all buffers in chat context. - `file` - Includes content of provided file in chat context. Supports input. + - `file:` - Includes content of specified file in chat context. - `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). - `files:list` - Only lists file names. - `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` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number). - `git:unstaged` - Includes unstaged changes in chat context. - `git:staged` - Includes staged changes in chat context. + - `git:` - Includes changes from specified commit in chat context. - `url` - Includes content of provided URL in chat context. Supports input. + - `url:` - Includes content of specified URL in chat context. - `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). + - `register:` - Includes contents of specified register in chat context. - `quickfix` - Includes quickfix list file contents in chat context. You can define custom contexts like this: diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 47b711ab..f677750b 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -229,7 +229,7 @@ return { end, }, git = { - description = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged).', + description = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number).', input = function(callback) vim.ui.select({ 'unstaged', 'staged' }, { prompt = 'Select diff type> ', diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index ccafc59f..b00b29c0 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -505,6 +505,10 @@ function M.gitdiff(type, winnr) if type == 'staged' then table.insert(cmd, '--staged') + elseif type == 'unstaged' then + table.insert(cmd, '--') + else + table.insert(cmd, type) end local out = utils.system(cmd) From 834ba1bc71e1a146831be0ebfeb456ad252c3c56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 5 Feb 2025 20:12:03 +0000 Subject: [PATCH 030/728] 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 5816c572..5e15d7a3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -308,16 +308,23 @@ prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: - `buffer` - Includes specified buffer in chat context. Supports input (default current). + - `buffer:` - Includes buffer with specified number in chat context. - `buffers` - Includes all buffers in chat context. Supports input (default listed). + - `buffers:listed` - Includes only listed buffers in chat context. + - `buffers:all` - Includes all buffers in chat context. - `file` - Includes content of provided file in chat context. Supports input. + - `file:` - Includes content of specified file in chat context. - `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). - `files:list` - Only lists file names. - `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` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number). - `git:unstaged` - Includes unstaged changes in chat context. - `git:staged` - Includes staged changes in chat context. + - `git:` - Includes changes from specified commit in chat context. - `url` - Includes content of provided URL in chat context. Supports input. + - `url:` - Includes content of specified URL in chat context. - `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). + - `register:` - Includes contents of specified register in chat context. - `quickfix` - Includes quickfix list file contents in chat context. You can define custom contexts like this: From 7a95d6ee39fd62ae5ed2d02c846e8d17b7396e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rokas=20Brazd=C5=BEionis?= Date: Fri, 7 Feb 2025 13:14:24 +0200 Subject: [PATCH 031/728] fix: typescript file type matching recognize typescript file type based on extension --- lua/CopilotChat/utils.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 83745249..66620a82 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -200,6 +200,20 @@ end ---@return string|nil function M.filetype(filename) local ft = vim.filetype.match({ filename = filename }) + + -- weird TypeScript bug for vim.filetype.match + -- see: https://github.com/neovim/neovim/issues/27265 + if not ft then + local base_name = vim.fs.basename(filename) + local split_name = vim.split(base_name, '%.') + if #split_name > 1 then + local ext = split_name[#split_name] + if ext == 'ts' then + ft = 'typescript' + end + end + end + if ft == '' then return nil end From a5c6360a00f369abc4958ffd5edc8e8478b57deb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Feb 2025 14:23:30 +0000 Subject: [PATCH 032/728] 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 5e15d7a3..b9428f1b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 05 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 07 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From f02be68e494c49dc858fc4e6e38c5f962cfb37cc Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:25:01 +0100 Subject: [PATCH 033/728] docs: add dzonatan as a contributor for code (#753) * 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 bce4c3c7..8f7a430e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -340,6 +340,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/11719334?v=4", "profile": "https://github.com/johncming", "contributions": ["code"] + }, + { + "login": "dzonatan", + "name": "Rokas Brazdžionis", + "avatar_url": "https://avatars.githubusercontent.com/u/5166666?v=4", + "profile": "https://github.com/dzonatan", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d34bcb9f..1b971383 100644 --- a/README.md +++ b/README.md @@ -876,6 +876,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Kohei Wada
Kohei Wada

💻 Sebastian Yaghoubi
Sebastian Yaghoubi

📖 johncming
johncming

💻 + Rokas Brazdžionis
Rokas Brazdžionis

💻 From 1db5a43193f7888a79c00c804ff1d6274f7bc84a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 7 Feb 2025 18:31:47 +0100 Subject: [PATCH 034/728] Update models in README Signed-off-by: Tomas Slusny --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1b971383..cabd9117 100644 --- a/README.md +++ b/README.md @@ -241,10 +241,11 @@ You can list available models with `:CopilotChatModels` command. Model determine 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. -- `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. Claude is **not available everywhere** so if you do not see it, try github codespaces or VPN. -- `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. +- `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. +- `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. Claude is **not available everywhere** so if you do not see it, try github codespaces or VPN. +- `gemini-2.0-flash-001` - This model has strong coding, math, and reasoning capabilities that makes it well suited to assist with software development. +- `o1` - 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. +- `o3-mini` - This model is the next generation of reasoning models, following from o1 and o1-mini. The o3-mini model outperforms o1 on coding benchmarks with response times that are comparable to o1-mini, providing improved quality at nearly the same latency. It is best suited for code generation and small context operations. You can make 50 requests to this model every 12 hours. 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`) From 872092538ab75330b0e85c8f072844800cf2fe02 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Feb 2025 17:39:02 +0000 Subject: [PATCH 035/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index b9428f1b..8ef03044 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -273,10 +273,11 @@ 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. 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. Claude is **not available everywhere** so if you do not see it, try github codespaces or VPN. -- `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. +- `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. +- `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. Claude is **not available everywhere** so if you do not see it, try github codespaces or VPN. +- `gemini-2.0-flash-001` - This model has strong coding, math, and reasoning capabilities that makes it well suited to assist with software development. +- `o1` - 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. +- `o3-mini` - This model is the next generation of reasoning models, following from o1 and o1-mini. The o3-mini model outperforms o1 on coding benchmarks with response times that are comparable to o1-mini, providing improved quality at nearly the same latency. It is best suited for code generation and small context operations. You can make 50 requests to this model every 12 hours. For more information about models, see here @@ -852,7 +853,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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻This project follows the all-contributors specification. Contributions of any kind are welcome! From dbce8a231d1ac72c68ce00b86b415c9304417102 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 7 Feb 2025 18:54:29 +0100 Subject: [PATCH 036/728] feat: Report early stop reason to the UI The Copilot API returns a `finish_reason` field that indicates why the response stopped. This commit adds this information to the UI, so the user knows why the response stopped. If the reason is `stop`, it means that the response was finished normally, so we don't need to show any message. --- lua/CopilotChat/copilot.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 63773ddf..41740662 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -646,7 +646,13 @@ function Copilot:ask(prompt, opts) end if choice.finish_reason and job then - finish_stream(nil, job) + local reason = choice.finish_reason + if reason == 'stop' then + reason = nil + else + reason = 'Early stop: ' .. reason + end + finish_stream(reason, job) end end From d3e638a7960276cbbd3f28b1baa4aeb9b31bae7d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 11:04:45 +0100 Subject: [PATCH 037/728] feat: add support for Github Marketplace models Adds integration with Github Marketplace models available through Azure ML API. The models are fetched from the Azure catalog and exposed through the same interface as regular Copilot models. This provides users with access to additional experimental models while maintaining consistent UX. The docs are updated to point to official Copilot model documentation and marketplace models page rather than listing models inline. Signed-off-by: Tomas Slusny --- README.md | 12 +- lua/CopilotChat/config.lua | 308 +---------------------- lua/CopilotChat/config/contexts.lua | 158 ++++++++++++ lua/CopilotChat/config/mappings.lua | 77 ++++++ lua/CopilotChat/{ => config}/prompts.lua | 90 ++++++- lua/CopilotChat/copilot.lua | 132 +++++++--- lua/CopilotChat/init.lua | 50 ++-- 7 files changed, 453 insertions(+), 374 deletions(-) create mode 100644 lua/CopilotChat/config/contexts.lua create mode 100644 lua/CopilotChat/config/mappings.lua rename lua/CopilotChat/{ => config}/prompts.lua (51%) diff --git a/README.md b/README.md index cabd9117..48a119cd 100644 --- a/README.md +++ b/README.md @@ -239,16 +239,8 @@ You can also set default sticky prompts in the configuration: 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. -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. -- `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. Claude is **not available everywhere** so if you do not see it, try github codespaces or VPN. -- `gemini-2.0-flash-001` - This model has strong coding, math, and reasoning capabilities that makes it well suited to assist with software development. -- `o1` - 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. -- `o3-mini` - This model is the next generation of reasoning models, following from o1 and o1-mini. The o3-mini model outperforms o1 on coding benchmarks with response times that are comparable to o1-mini, providing improved quality at nearly the same latency. It is best suited for code generation and small context operations. You can make 50 requests to this model every 12 hours. - -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`) +For list of models supported by Copilot Chat see [here](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat). +This plugin also supports Github Marketplace Models. These have fairly low limits but are useful for experimentation. For more information see [here](https://github.com/marketplace/models). ## Agents diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f677750b..498e21eb 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -1,17 +1,5 @@ -local prompts = require('CopilotChat.prompts') -local context = require('CopilotChat.context') +local prompts = require('CopilotChat.config.prompts') local select = require('CopilotChat.select') -local utils = require('CopilotChat.utils') - ----@class CopilotChat.config.context ----@field description string? ----@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? ----@field description string? ----@field mapping string? ---@class CopilotChat.config.window ---@field layout string? @@ -25,32 +13,6 @@ local utils = require('CopilotChat.utils') ---@field footer string? ---@field zindex number? ----@class CopilotChat.config.mapping ----@field normal string? ----@field insert string? ----@field detail string? - ----@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping ----@field register string? - ----@class CopilotChat.config.mapping.show_diff : CopilotChat.config.mapping ----@field full_diff boolean? - ----@class CopilotChat.config.mappings ----@field complete CopilotChat.config.mapping? ----@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 jump_to_diff CopilotChat.config.mapping? ----@field quickfix_diffs CopilotChat.config.mapping? ----@field yank_diff CopilotChat.config.mapping.yank_diff? ----@field show_diff CopilotChat.config.mapping.show_diff? ----@field show_info CopilotChat.config.mapping? ----@field show_context CopilotChat.config.mapping? ----@field show_help CopilotChat.config.mapping? - ---@class CopilotChat.config.shared ---@field system_prompt string? ---@field model string? @@ -90,7 +52,7 @@ return { -- 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 /). + system_prompt = prompts.COPILOT_INSTRUCTIONS.system_prompt, -- 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 @). @@ -146,271 +108,11 @@ return { separator = '───', -- Separator to use in chat -- default contexts - contexts = { - buffer = { - description = 'Includes specified buffer in chat context. Supports input (default current).', - input = function(callback) - vim.ui.select( - vim.tbl_map( - function(buf) - return { id = buf, name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:.') } - end, - vim.tbl_filter(function(buf) - return utils.buf_valid(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) - input = input and tonumber(input) or source.bufnr - return { - context.buffer(input), - } - end, - }, - buffers = { - description = 'Includes all buffers in chat context. Supports input (default listed).', - input = function(callback) - vim.ui.select({ 'listed', 'visible' }, { - prompt = 'Select buffer scope> ', - }, callback) - end, - resolve = function(input) - input = input or 'listed' - return context.buffers(input) - end, - }, - 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(cwd .. '/**/*', 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 files in the current workspace in chat context. Supports input (default list).', - input = function(callback) - 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> ', - 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 = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number).', - 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 = '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. Supports input (default +, e.g clipboard).', - input = function(callback) - local choices = utils.kv_list({ - ['+'] = '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(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, - }, - quickfix = { - description = 'Includes quickfix list file contents in chat context.', - resolve = function() - return context.quickfix() - end, - }, - }, + contexts = require('CopilotChat.config.contexts'), -- default prompts - prompts = { - Explain = { - prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', - }, - Review = { - prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - callback = function(response, source) - local diagnostics = {} - for line in response:gmatch('[^\r\n]+') do - if line:find('^line=') then - local start_line = nil - local end_line = nil - local message = nil - local single_match, message_match = line:match('^line=(%d+): (.*)$') - if not single_match then - local start_match, end_match, m_message_match = line:match('^line=(%d+)-(%d+): (.*)$') - if start_match and end_match then - start_line = tonumber(start_match) - end_line = tonumber(end_match) - message = m_message_match - end - else - start_line = tonumber(single_match) - end_line = start_line - message = message_match - end - - if start_line and end_line then - table.insert(diagnostics, { - lnum = start_line - 1, - end_lnum = end_line - 1, - col = 0, - message = message, - severity = vim.diagnostic.severity.WARN, - source = 'Copilot Review', - }) - end - end - end - vim.diagnostic.set( - vim.api.nvim_create_namespace('copilot_diagnostics'), - source.bufnr, - diagnostics - ) - end, - }, - Fix = { - 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\n\nOptimize the selected code to improve performance and readability.', - }, - Docs = { - prompt = '> /COPILOT_GENERATE\n\nPlease add documentation comments to the selected code.', - }, - Tests = { - prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', - }, - Commit = { - 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.', - }, - }, + prompts = prompts, -- default mappings - mappings = { - complete = { - insert = '', - }, - close = { - normal = 'q', - insert = '', - }, - reset = { - normal = '', - insert = '', - }, - submit_prompt = { - normal = '', - insert = '', - }, - toggle_sticky = { - detail = 'Makes line under cursor sticky or deletes sticky line.', - normal = 'gr', - }, - accept_diff = { - normal = '', - insert = '', - }, - jump_to_diff = { - normal = 'gj', - }, - quickfix_answers = { - normal = 'gqa', - }, - quickfix_diffs = { - normal = 'gqd', - }, - yank_diff = { - normal = 'gy', - register = '"', -- Default register to use for yanking - }, - show_diff = { - normal = 'gd', - full_diff = false, -- Show full diff instead of unified diff when showing diff window - }, - show_info = { - normal = 'gi', - }, - show_context = { - normal = 'gc', - }, - show_help = { - normal = 'gh', - }, - }, + mappings = require('CopilotChat.config.mappings'), } diff --git a/lua/CopilotChat/config/contexts.lua b/lua/CopilotChat/config/contexts.lua new file mode 100644 index 00000000..e8f71b60 --- /dev/null +++ b/lua/CopilotChat/config/contexts.lua @@ -0,0 +1,158 @@ +local context = require('CopilotChat.context') +local utils = require('CopilotChat.utils') + +---@class CopilotChat.config.context +---@field description string? +---@field input fun(callback: fun(input: string?), source: CopilotChat.source)? +---@field resolve fun(input: string?, source: CopilotChat.source):table + +return { + buffer = { + description = 'Includes specified buffer in chat context. Supports input (default current).', + input = function(callback) + vim.ui.select( + vim.tbl_map( + function(buf) + return { id = buf, name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:.') } + end, + vim.tbl_filter(function(buf) + return utils.buf_valid(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) + input = input and tonumber(input) or source.bufnr + return { + context.buffer(input), + } + end, + }, + buffers = { + description = 'Includes all buffers in chat context. Supports input (default listed).', + input = function(callback) + vim.ui.select({ 'listed', 'visible' }, { + prompt = 'Select buffer scope> ', + }, callback) + end, + resolve = function(input) + input = input or 'listed' + return context.buffers(input) + end, + }, + 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(cwd .. '/**/*', 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 files in the current workspace in chat context. Supports input (default list).', + input = function(callback) + 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> ', + 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 = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number).', + 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 = '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. Supports input (default +, e.g clipboard).', + input = function(callback) + local choices = utils.kv_list({ + ['+'] = '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(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, + }, + quickfix = { + description = 'Includes quickfix list file contents in chat context.', + resolve = function() + return context.quickfix() + end, + }, +} diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua new file mode 100644 index 00000000..13c4e3fd --- /dev/null +++ b/lua/CopilotChat/config/mappings.lua @@ -0,0 +1,77 @@ +---@class CopilotChat.config.mapping +---@field normal string? +---@field insert string? +---@field detail string? + +---@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping +---@field register string? + +---@class CopilotChat.config.mapping.show_diff : CopilotChat.config.mapping +---@field full_diff boolean? + +---@class CopilotChat.config.mappings +---@field complete CopilotChat.config.mapping? +---@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 jump_to_diff CopilotChat.config.mapping? +---@field quickfix_diffs CopilotChat.config.mapping? +---@field yank_diff CopilotChat.config.mapping.yank_diff? +---@field show_diff CopilotChat.config.mapping.show_diff? +---@field show_info CopilotChat.config.mapping? +---@field show_context CopilotChat.config.mapping? +---@field show_help CopilotChat.config.mapping? + +return { + complete = { + insert = '', + }, + close = { + normal = 'q', + insert = '', + }, + reset = { + normal = '', + insert = '', + }, + submit_prompt = { + normal = '', + insert = '', + }, + toggle_sticky = { + detail = 'Makes line under cursor sticky or deletes sticky line.', + normal = 'gr', + }, + accept_diff = { + normal = '', + insert = '', + }, + jump_to_diff = { + normal = 'gj', + }, + quickfix_answers = { + normal = 'gqa', + }, + quickfix_diffs = { + normal = 'gqd', + }, + yank_diff = { + normal = 'gy', + register = '"', -- Default register to use for yanking + }, + show_diff = { + normal = 'gd', + full_diff = false, -- Show full diff instead of unified diff when showing diff window + }, + show_info = { + normal = 'gi', + }, + show_context = { + normal = 'gc', + }, + show_help = { + normal = 'gh', + }, +} diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/config/prompts.lua similarity index 51% rename from lua/CopilotChat/prompts.lua rename to lua/CopilotChat/config/prompts.lua index 1f0bc489..137fed4f 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -1,6 +1,7 @@ ----@class CopilotChat.prompts - -local M = {} +---@class CopilotChat.config.prompt : CopilotChat.config.shared +---@field prompt string? +---@field description string? +---@field mapping string? local base = string.format( [[ @@ -16,11 +17,11 @@ The user is working on a %s machine. Please respond with system specific command vim.loop.os_uname().sysname ) -M.COPILOT_INSTRUCTIONS = [[ +local COPILOT_INSTRUCTIONS = [[ You are a code-focused AI programming assistant that specializes in practical software engineering solutions. ]] .. base -M.COPILOT_EXPLAIN = [[ +local COPILOT_EXPLAIN = [[ You are a programming instructor focused on clear, practical explanations. When explaining code: - Balance high-level concepts with implementation details @@ -28,7 +29,7 @@ When explaining code: - Address any code diagnostics or warnings ]] .. base -M.COPILOT_REVIEW = M.COPILOT_INSTRUCTIONS +local COPILOT_REVIEW = COPILOT_INSTRUCTIONS .. [[ Review the code for readability and maintainability issues. Report problems in this format: line=: @@ -48,7 +49,7 @@ End with: "**`To clear buffer highlights, please ask a different question.`**" If no issues found, confirm the code is well-written. ]] -M.COPILOT_GENERATE = M.COPILOT_INSTRUCTIONS +local COPILOT_GENERATE = COPILOT_INSTRUCTIONS .. [[ Your task is to modify the provided code according to the user's request. Follow these instructions precisely: @@ -78,4 +79,77 @@ Your task is to modify the provided code according to the user's request. Follow Remember: Your response should ONLY contain file headers with line numbers and code blocks for direct replacement. ]] -return M +return { + COPILOT_INSTRUCTIONS = { + system_prompt = COPILOT_INSTRUCTIONS, + }, + COPILOT_EXPLAIN = { + system_prompt = COPILOT_EXPLAIN, + }, + COPILOT_REVIEW = { + system_prompt = COPILOT_REVIEW, + }, + COPILOT_GENERATE = { + system_prompt = COPILOT_GENERATE, + }, + Explain = { + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', + }, + Review = { + prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', + callback = function(response, source) + local diagnostics = {} + for line in response:gmatch('[^\r\n]+') do + if line:find('^line=') then + local start_line = nil + local end_line = nil + local message = nil + local single_match, message_match = line:match('^line=(%d+): (.*)$') + if not single_match then + local start_match, end_match, m_message_match = line:match('^line=(%d+)-(%d+): (.*)$') + if start_match and end_match then + start_line = tonumber(start_match) + end_line = tonumber(end_match) + message = m_message_match + end + else + start_line = tonumber(single_match) + end_line = start_line + message = message_match + end + + if start_line and end_line then + table.insert(diagnostics, { + lnum = start_line - 1, + end_lnum = end_line - 1, + col = 0, + message = message, + severity = vim.diagnostic.severity.WARN, + source = 'Copilot Review', + }) + end + end + end + vim.diagnostic.set( + vim.api.nvim_create_namespace('copilot_diagnostics'), + source.bufnr, + diagnostics + ) + end, + }, + Fix = { + 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\n\nOptimize the selected code to improve performance and readability.', + }, + Docs = { + prompt = '> /COPILOT_GENERATE\n\nPlease add documentation comments to the selected code.', + }, + Tests = { + prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', + }, + Commit = { + 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/copilot.lua b/lua/CopilotChat/copilot.lua index 41740662..8c434c69 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -1,15 +1,14 @@ ---@class CopilotChat.copilot.ask.opts ---@field selection CopilotChat.select.selection? ---@field embeddings table? ----@field system_prompt string? ----@field model string? ----@field agent string? +---@field system_prompt string +---@field model string +---@field agent string ---@field temperature number? ---@field no_history boolean? ---@field on_progress nil|fun(response: string):nil 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') @@ -37,7 +36,10 @@ local VERSION_HEADERS = { ['sec-fetch-mode'] = 'no-cors', ['sec-fetch-dest'] = 'empty', ['priority'] = 'u=4, i', - -- ['x-github-api-version'] = '2023-07-07', +} +local MODELS_VERSION_HEADERS = { + ['x-ms-useragent'] = VERSION_HEADERS['editor-version'], + ['x-ms-user-agent'] = VERSION_HEADERS['editor-version'], } --- Get the github oauth cached token @@ -326,7 +328,7 @@ end) --- Authenticate with GitHub and get the required headers ---@return table -function Copilot:authenticate() +function Copilot:authenticate(models) 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' @@ -361,7 +363,15 @@ function Copilot:authenticate() self.token = vim.json.decode(response.body) end - local headers = { + if models then + return vim.tbl_extend('force', { + ['authorization'] = 'bearer ' .. self.github_token, + ['accept'] = 'application/json', + ['content-type'] = 'application/json', + }, MODELS_VERSION_HEADERS) + end + + return vim.tbl_extend('force', { ['authorization'] = 'Bearer ' .. self.token.token, ['x-request-id'] = utils.uuid(), ['vscode-sessionid'] = self.sessionid, @@ -370,12 +380,7 @@ function Copilot:authenticate() ['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 + }, VERSION_HEADERS) end --- Fetch models from the Copilot API @@ -387,31 +392,81 @@ function Copilot:fetch_models() notify.publish(notify.STATUS, 'Fetching models') + local out = {} + + -- Github models local response, err = utils.curl_get( 'https://api.githubcopilot.com/models', 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 models: ' .. tostring(response.status)) end - -- Find chat 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 + out[model.id] = { + name = model.name, + provider = 'copilot', + version = model.version, + tokenizer = model.capabilities.tokenizer, + max_prompt_tokens = model.capabilities.limits.max_prompt_tokens, + max_output_tokens = model.capabilities.limits.max_output_tokens, + } + end + end + + -- Marketplace models + response, err = utils.curl_post( + 'https://api.catalog.azureml.ms/asset-gallery/v1.0/models', + vim.tbl_extend('force', self.request_args, { + headers = { + ['content-type'] = 'application/json', + }, + body = [[ + { + "filters": [ + { "field": "freePlayground", "values": ["true"], "operator": "eq"}, + { "field": "labels", "values": ["latest"], "operator": "eq"} + ], + "order": [ + { "field": "displayName", "direction": "asc" } + ] + } + ]], + }) + ) + if err then + error(err) + end + if response.status ~= 200 then + error('Failed to fetch models: ' .. tostring(response.status)) + end + + models = vim.json.decode(response.body)['summaries'] + for _, model in ipairs(models) do + if + not out[model.name:lower()] and vim.tbl_contains(model.inferenceTasks, 'chat-completion') + then + out[model.name] = { + name = model.displayName, + provider = 'github-models', + version = model.name .. '-' .. model.version, + max_prompt_tokens = model.modelLimits.textLimits.inputContextWindow, + max_output_tokens = model.modelLimits.textLimits.maxOutputTokens, + } + + self.policies[model.name] = true end end @@ -490,9 +545,9 @@ function Copilot:ask(prompt, opts) prompt = vim.trim(prompt) local embeddings = opts.embeddings 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' - local agent = opts.agent or 'copilot' + local system_prompt = vim.trim(opts.system_prompt) + local model = opts.model + local agent = opts.agent local temperature = opts.temperature or 0.1 local no_history = opts.no_history or false local on_progress = opts.on_progress @@ -519,10 +574,9 @@ function Copilot:ask(prompt, opts) 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 + local max_tokens = model_config.max_prompt_tokens + local max_output_tokens = model_config.max_output_tokens + local tokenizer = model_config.tokenizer or 'o200k_base' log.debug('Max tokens: ', max_tokens) log.debug('Tokenizer: ', tokenizer) tiktoken.load(tokenizer) @@ -707,15 +761,21 @@ function Copilot:ask(prompt, opts) ) 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' - end local args = vim.tbl_extend('force', self.request_args, { - headers = self:authenticate(), body = temp_file(body), }) + local url + if not agent_config.default then + url = 'https://api.githubcopilot.com/agents/' .. agent .. '?chat' + args.headers = self:authenticate() + elseif model_config.provider == 'github-models' then + url = 'https://models.inference.ai.azure.com/chat/completions' + args.headers = self:authenticate(true) + else + url = 'https://api.githubcopilot.com/chat/completions' + args.headers = self:authenticate() + end if is_stream then args.stream = stream_func @@ -825,6 +885,7 @@ end function Copilot:list_models() local models = self:fetch_models() + -- First deduplicate by version, keeping shortest ID local version_map = {} for id, model in pairs(models) do local version = model.version @@ -834,11 +895,18 @@ function Copilot:list_models() end local result = vim.tbl_values(version_map) - table.sort(result) + table.sort(result, function(a, b) + local a_model = models[a] + local b_model = models[b] + if a_model.provider ~= b_model.provider then + return a_model.provider < b_model.provider -- sort by version first + end + return a_model.version < b_model.version -- then by provider + end) local out = {} for _, id in ipairs(result) do - out[id] = models[id].name + table.insert(out, vim.tbl_extend('force', models[id], { id = id })) end return out end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 2b93c8cf..82cdda1a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -3,7 +3,6 @@ local log = require('plenary.log') local default_config = require('CopilotChat.config') local Copilot = require('CopilotChat.copilot') local context = require('CopilotChat.context') -local prompts = require('CopilotChat.prompts') local utils = require('CopilotChat.utils') local M = {} @@ -294,7 +293,10 @@ local function resolve_agent(prompt, config) end local function resolve_model(prompt, config) - local models = vim.tbl_keys(state.copilot:list_models()) + local models = vim.tbl_map(function(model) + return model.id + end, state.copilot:list_models()) + local selected_model = config.model prompt = prompt:gsub('%$' .. WORD, function(match) if vim.tbl_contains(models, match) then @@ -523,6 +525,7 @@ function M.complete_items(callback) items[#items + 1] = { word = '/' .. name, + abbr = name, kind = kind, info = info, menu = prompt.description or '', @@ -532,11 +535,12 @@ function M.complete_items(callback) } end - for name, description in pairs(models) do + for _, model in ipairs(models) do items[#items + 1] = { - word = '$' .. name, - kind = 'model', - menu = description, + word = '$' .. model.id, + abbr = model.id, + kind = model.provider, + menu = model.name, icase = 1, dup = 0, empty = 0, @@ -546,6 +550,7 @@ function M.complete_items(callback) for name, description in pairs(agents) do items[#items + 1] = { word = '@' .. name, + abbr = name, kind = 'agent', menu = description, icase = 1, @@ -557,6 +562,7 @@ function M.complete_items(callback) for name, value in pairs(M.config.contexts) do items[#items + 1] = { word = '#' .. name, + abbr = name, kind = 'context', menu = value.description or '', icase = 1, @@ -582,12 +588,6 @@ end function M.prompts() local prompts_to_use = {} - for name, prompt in pairs(prompts) do - prompts_to_use[name] = { - system_prompt = prompt, - } - end - for name, prompt in pairs(M.config.prompts) do local val = prompt if type(prompt) == 'string' then @@ -649,21 +649,29 @@ end --- Select default Copilot GPT model. function M.select_model() async.run(function() - local models = vim.tbl_keys(state.copilot:list_models()) - models = vim.tbl_map(function(model) - if model == M.config.model then - return model .. ' (selected)' - end - - return model + local models = state.copilot:list_models() + local choices = vim.tbl_map(function(model) + return { + id = model.id, + name = model.name, + provider = model.provider, + selected = model.id == M.config.model, + } end, models) async.util.scheduler() - vim.ui.select(models, { + vim.ui.select(choices, { prompt = 'Select a model> ', + format_item = function(item) + local out = string.format('%s (%s)', item.id, item.provider) + if item.selected then + out = '* ' .. out + end + return out + end, }, function(choice) if choice then - M.config.model = choice:gsub(' %(selected%)', '') + M.config.model = choice.id end end) end) From 804d9067d1eef0ee21dc39b5bbc86ab9f52287b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Feb 2025 22:30:39 +0000 Subject: [PATCH 038/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8ef03044..110fb7fb 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 07 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 11 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -271,19 +271,11 @@ 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 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. -- `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. Claude is **not available everywhere** so if you do not see it, try github codespaces or VPN. -- `gemini-2.0-flash-001` - This model has strong coding, math, and reasoning capabilities that makes it well suited to assist with software development. -- `o1` - 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. -- `o3-mini` - This model is the next generation of reasoning models, following from o1 and o1-mini. The o3-mini model outperforms o1 on coding benchmarks with response times that are comparable to o1-mini, providing improved quality at nearly the same latency. It is best suited for code generation and small context operations. You can make 50 requests to this model every 12 hours. - -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`) +`model` key. For list of models supported by Copilot Chat see here +. +This plugin also supports Github Marketplace Models. These have fairly low +limits but are useful for experimentation. For more information see here +. AGENTS *CopilotChat-agents* From 10674ced9f3d287bc64b733d9b916b1384103fec Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Feb 2025 02:10:16 +0100 Subject: [PATCH 039/728] fix: do not auto trigger context input on text changed It should always be explicitely triggered when pressing tab only Closes #762 --- lua/CopilotChat/init.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 82cdda1a..ab030a13 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -447,7 +447,7 @@ local function key_to_info(name, surround) return out end -local function trigger_complete() +local function trigger_complete(with_context) local info = M.complete_info() local bufnr = vim.api.nvim_get_current_buf() local line = vim.api.nvim_get_current_line() @@ -463,7 +463,7 @@ local function trigger_complete() return end - if vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then + if with_context and 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) @@ -1047,7 +1047,9 @@ function M.setup(config) map_key('reset', bufnr, M.reset) map_key('close', bufnr, M.close) - map_key('complete', bufnr, trigger_complete) + map_key('complete', bufnr, function() + trigger_complete(true) + end) map_key('submit_prompt', bufnr, function() local section = state.chat:get_closest_section() From 7e6583c75f1231ea1eac70e06995dd3f97a58478 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 15 Feb 2025 01:12:06 +0000 Subject: [PATCH 040/728] 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 110fb7fb..3cc97878 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 11 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 15 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 14e9907a883d0a757c509f7dc64860a1b08d9942 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 8 Feb 2025 05:15:22 +0100 Subject: [PATCH 041/728] feat: Refactor how providers are defined and allow custom providers - Move copilot and github_models providers to separate config - Allow specifying custom providers Closes #723 Signed-off-by: Tomas Slusny --- README.md | 100 +++- lua/CopilotChat/{copilot.lua => client.lua} | 524 ++++++-------------- lua/CopilotChat/config.lua | 6 +- lua/CopilotChat/config/providers.lua | 315 ++++++++++++ lua/CopilotChat/context.lua | 7 +- lua/CopilotChat/init.lua | 81 +-- lua/CopilotChat/utils.lua | 70 ++- 7 files changed, 664 insertions(+), 439 deletions(-) rename lua/CopilotChat/{copilot.lua => client.lua} (64%) create mode 100644 lua/CopilotChat/config/providers.lua diff --git a/README.md b/README.md index 48a119cd..94c39172 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 - [Neovim 0.10.0+](https://neovim.io/) - Older versions are not officially supported - [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 +- _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 improved fetching 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 @@ -341,6 +341,86 @@ You can chain multiple selections like this: } ``` +## Providers + +Providers are modules that implement integration with different AI providers. Built-in providers are: + +- `copilot` - Default GitHub Copilot provider used for chat and embeddings +- `github_models` - Provider for GitHub Marketplace models + +You can define custom providers by adding them to `providers` config. Provider has following fields: + +- `disabled?: boolean` - Optional boolean to disable provider +- `embeddings?: string` - Optional string pointing to provider to use for embeddings +- `get_token(): string, number?` - Function that returns authentication token and optional expiry timestamp +- `get_headers(token: string, sessionid: string, machineid: string): table` - Function that returns headers for API requests +- `get_url(opts: table): string` - Function that returns API endpoint URL for given operation +- `prepare_input(inputs: table, opts: table, model: table): table` - Function that prepares request body +- `get_models?(headers: table): table` - Optional function that returns list of available models +- `get_agents?(headers: table): table` - Optional function that returns list of available agents + +Example custom provider: + +```lua +{ + providers = { + my_provider = { + -- Required fields + get_token = function() + return "my-token", os.time() + 3600 -- Token valid for 1 hour + end, + get_headers = function(token, sessionid, machineid) + return { + ["authorization"] = "Bearer " .. token, + ["content-type"] = "application/json", + } + end, + get_url = function(opts) + if opts.agent then + return "https://api.custom.com/agents/" .. opts.agent + end + return "https://api.custom.com/chat" + end, + prepare_input = function(inputs, opts, model) + return { + messages = inputs, + temperature = opts.temperature, + model = opts.model, + stream = true + } + end, + + -- Optional fields + disabled = false, + embeddings = "copilot_embeddings", -- Use copilot for embeddings + get_models = function(headers) + -- Return list of available models + return { + { + id = "gpt-4", + name = "GPT-4", + version = "1.0", + tokenizer = "gpt2", + max_prompt_tokens = 8000, + max_output_tokens = 2000, + } + } + end, + get_agents = function(headers) + -- Return list of available agents + return { + { + id = "agent1", + name = "My Agent", + description = "Custom agent" + } + } + end + } + } +} +``` + ## API ```lua @@ -482,6 +562,19 @@ Also see [here](/lua/CopilotChat/config.lua): error_header = '# Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + -- default providers + providers = { + copilot = { + -- see config.lua for implementation + }, + github_models = { + -- see config.lua for implementation + }, + copilot_embeddings = { + -- see config.lua for implementation + }, + } + -- default contexts contexts = { buffer = { @@ -517,7 +610,6 @@ Also see [here](/lua/CopilotChat/config.lua): }, Review = { prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - -- 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.', diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/client.lua similarity index 64% rename from lua/CopilotChat/copilot.lua rename to lua/CopilotChat/client.lua index 8c434c69..16a53c1c 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/client.lua @@ -1,4 +1,4 @@ ----@class CopilotChat.copilot.ask.opts +---@class CopilotChat.Client.ask ---@field selection CopilotChat.select.selection? ---@field embeddings table? ---@field system_prompt string @@ -20,63 +20,7 @@ local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' 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 -local VERSION_HEADERS = { - ['editor-version'] = 'Neovim/' - .. vim.version().major - .. '.' - .. vim.version().minor - .. '.' - .. 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', -} -local MODELS_VERSION_HEADERS = { - ['x-ms-useragent'] = VERSION_HEADERS['editor-version'], - ['x-ms-user-agent'] = VERSION_HEADERS['editor-version'], -} - ---- 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') - local codespaces = os.getenv('CODESPACES') - if token and codespaces then - return token - end - - -- loading token from the file - local config_path = utils.config_path() - if not config_path then - return nil - end - - -- token can be sometimes in apps.json sometimes in hosts.json - local file_paths = { - config_path .. '/github-copilot/hosts.json', - config_path .. '/github-copilot/apps.json', - } - - for _, file_path in ipairs(file_paths) do - if vim.fn.filereadable(file_path) == 1 then - local userdata = vim.fn.json_decode(vim.fn.readfile(file_path)) - for key, value in pairs(userdata) do - if string.find(key, 'github.com') then - return value.oauth_token - end - end - end - end - - return nil -end --- Generate content block with line numbers, truncating if necessary ---@param content string: The content @@ -188,25 +132,14 @@ local function generate_embeddings_messages(embeddings) end, embeddings) end -local function generate_ask_request( - history, - prompt, - system_prompt, - generated_messages, - model, - temperature, - max_output_tokens, - stream -) - local is_o1 = vim.startswith(model, 'o1') +local function generate_ask_request(history, prompt, system_prompt, generated_messages) local messages = {} - local system_role = is_o1 and 'user' or 'system' local contexts = {} if system_prompt ~= '' then table.insert(messages, { content = system_prompt, - role = system_role, + role = 'system', }) end @@ -234,50 +167,31 @@ local function generate_ask_request( role = 'user', }) - local out = { - messages = messages, - model = model, - stream = stream, - n = 1, - } - - if max_output_tokens then - out.max_tokens = max_output_tokens - end - - if not is_o1 then - out.temperature = temperature - out.top_p = 1 - end - - return out + return messages end -local function generate_embedding_request(inputs, model, threshold) - return { - dimensions = 512, - input = vim.tbl_map(function(embedding) - local content = - generate_content_block(embedding.outline or embedding.content, nil, threshold, -1) - 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, - } +local function generate_embedding_request(inputs, threshold) + return vim.tbl_map(function(embedding) + local content = + generate_content_block(embedding.outline or embedding.content, nil, threshold, -1) + 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) end ----@class CopilotChat.Copilot : Class +---@class CopilotChat.Client : Class +---@field providers table ---@field history table +---@field provider_cache table ---@field embedding_cache table ----@field policies table ---@field models table? ---@field agents table? ---@field current_job string? @@ -285,264 +199,108 @@ end ---@field token table? ---@field sessionid string? ---@field machineid string ----@field request_args table -local Copilot = class(function(self, proxy, allow_insecure) +local Client = class(function(self, providers) + self.providers = providers self.history = {} self.embedding_cache = {} - self.policies = {} self.models = nil self.agents = nil + self.provider_cache = {} + for provider_name, _ in pairs(providers) do + self.provider_cache[provider_name] = {} + end + self.current_job = nil - self.github_token = nil - self.token = nil - self.sessionid = nil + self.expires_at = nil + self.headers = nil self.machineid = utils.machine_id() - self.github_token = get_cached_token() - - 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', - -- Keep connections alive for better performance - '--keepalive-time', - '60', - -- Disable compression (since responses are already streamed efficiently) - '--no-compressed', - -- Connect timeout of 10 seconds - '--connect-timeout', - '10', - -- Streaming optimizations - '--tcp-nodelay', - '--no-buffer', - }, - } end) --- Authenticate with GitHub and get the required headers +---@param provider_name string: The provider to authenticate with ---@return table -function Copilot:authenticate(models) - 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 +function Client:authenticate(provider_name) + local provider = self.providers[provider_name] + local headers = self.provider_cache[provider_name].headers + local expires_at = self.provider_cache[provider_name].expires_at - if - 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 = vim.tbl_extend('force', { - ['authorization'] = 'token ' .. self.github_token, - ['accept'] = 'application/json', - }, VERSION_HEADERS) - - local response, err = utils.curl_get( - 'https://api.github.com/copilot_internal/v2/token', - vim.tbl_extend('force', self.request_args, { - headers = headers, - }) - ) + if not headers or (expires_at and expires_at <= math.floor(os.time())) then + notify.publish(notify.STATUS, 'Authenticating to provider ' .. provider_name) - if err then - error(err) - end - - if response.status ~= 200 then - error('Failed to authenticate: ' .. tostring(response.status)) - end - - self.sessionid = sessionid - self.token = vim.json.decode(response.body) - end - - if models then - return vim.tbl_extend('force', { - ['authorization'] = 'bearer ' .. self.github_token, - ['accept'] = 'application/json', - ['content-type'] = 'application/json', - }, MODELS_VERSION_HEADERS) + local token, expires_at = provider.get_token() + local sessionid = utils.uuid() .. tostring(math.floor(os.time() * 1000)) + headers = provider.get_headers(token, sessionid, self.machineid) + self.provider_cache[provider_name].headers = headers + self.provider_cache[provider_name].expires_at = expires_at end - return vim.tbl_extend('force', { - ['authorization'] = 'Bearer ' .. self.token.token, - ['x-request-id'] = utils.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', - }, VERSION_HEADERS) + return headers end --- Fetch models from the Copilot API ---@return table -function Copilot:fetch_models() +function Client:fetch_models() if self.models then return self.models end - notify.publish(notify.STATUS, 'Fetching models') - - local out = {} - - -- Github models - local response, err = utils.curl_get( - 'https://api.githubcopilot.com/models', - 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 models: ' .. tostring(response.status)) - end - - local models = vim.json.decode(response.body)['data'] - 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] = { - name = model.name, - provider = 'copilot', - version = model.version, - tokenizer = model.capabilities.tokenizer, - max_prompt_tokens = model.capabilities.limits.max_prompt_tokens, - max_output_tokens = model.capabilities.limits.max_output_tokens, - } - end - end - - -- Marketplace models - response, err = utils.curl_post( - 'https://api.catalog.azureml.ms/asset-gallery/v1.0/models', - vim.tbl_extend('force', self.request_args, { - headers = { - ['content-type'] = 'application/json', - }, - body = [[ - { - "filters": [ - { "field": "freePlayground", "values": ["true"], "operator": "eq"}, - { "field": "labels", "values": ["latest"], "operator": "eq"} - ], - "order": [ - { "field": "displayName", "direction": "asc" } - ] - } - ]], - }) - ) - if err then - error(err) - end - if response.status ~= 200 then - error('Failed to fetch models: ' .. tostring(response.status)) - end - - models = vim.json.decode(response.body)['summaries'] - for _, model in ipairs(models) do - if - not out[model.name:lower()] and vim.tbl_contains(model.inferenceTasks, 'chat-completion') - then - out[model.name] = { - name = model.displayName, - provider = 'github-models', - version = model.name .. '-' .. model.version, - max_prompt_tokens = model.modelLimits.textLimits.inputContextWindow, - max_output_tokens = model.modelLimits.textLimits.maxOutputTokens, - } - - self.policies[model.name] = true + local models = {} + for provider_name, provider in pairs(self.providers) do + if not provider.disabled and provider.get_models then + local headers = self:authenticate(provider_name) + notify.publish(notify.STATUS, 'Fetching models from ' .. provider_name) + local provider_models = provider.get_models(headers) + for _, model in ipairs(provider_models) do + model.provider = provider_name + if not models[model.id] then + models[model.id] = model + end + end end end - log.trace(models) - self.models = out - return out + self.models = models + return self.models end --- Fetch agents from the Copilot API ---@return table -function Copilot:fetch_agents() +function Client:fetch_agents() if self.agents then 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, { - 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, description = 'Default noop agent' } - - log.trace(agents) - self.agents = out - return out -end - ---- 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 + local agents = {} + for provider_name, provider in pairs(self.providers) do + if not provider.disabled and provider.get_agents then + local headers = self:authenticate(provider_name) + notify.publish(notify.STATUS, 'Fetching agents from ' .. provider_name) + local provider_agents = provider.get_agents(headers) + for _, agent in ipairs(provider_agents) do + agent.provider = provider_name + if not agents[agent.id] then + agents[agent.id] = agent + end + end + end 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, { - headers = self:authenticate(), - body = vim.json.encode({ state = 'enabled' }), - }) - ) - - self.policies[model] = true - - if err or response.status ~= 200 then - log.warn('Failed to enable policy for ', model, ': ', (err or response.body)) - return - end + self.agents = agents + return self.agents 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) +---@param opts CopilotChat.Client.ask: Options for the request +function Client:ask(prompt, opts) opts = opts or {} prompt = vim.trim(prompt) + + if opts.agent == 'none' or opts.agent == 'copilot' then + opts.agent = nil + end + local embeddings = opts.embeddings or {} local selection = opts.selection or {} local system_prompt = vim.trim(opts.system_prompt) @@ -564,18 +322,21 @@ function Copilot:ask(prompt, opts) local history = no_history and {} or self.history local models = self:fetch_models() - 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 provider_name = model_config.provider + if not provider_name then + error('Provider not found for model: ' .. model) + end + local provider = self.providers[provider_name] + if not provider then + error('Provider not found: ' .. provider_name) + end + local max_tokens = model_config.max_prompt_tokens - local max_output_tokens = model_config.max_output_tokens local tokenizer = model_config.tokenizer or 'o200k_base' log.debug('Max tokens: ', max_tokens) log.debug('Tokenizer: ', tokenizer) @@ -746,36 +507,19 @@ function Copilot:ask(prompt, opts) parse_stream_line(line, job) end - local is_stream = not vim.startswith(model, 'o1') - local body = vim.json.encode( - generate_ask_request( - history, - prompt, - system_prompt, - generated_messages, - model, - temperature, - max_output_tokens, - is_stream - ) + local headers = self:authenticate(provider_name) + local request = provider.prepare_input( + generate_ask_request(history, prompt, system_prompt, generated_messages), + opts, + model_config ) + local is_stream = request.stream + local body = vim.json.encode(request) - self:enable_policy(model) - - local args = vim.tbl_extend('force', self.request_args, { + local args = { body = temp_file(body), - }) - local url - if not agent_config.default then - url = 'https://api.githubcopilot.com/agents/' .. agent .. '?chat' - args.headers = self:authenticate() - elseif model_config.provider == 'github-models' then - url = 'https://models.inference.ai.azure.com/chat/completions' - args.headers = self:authenticate(true) - else - url = 'https://api.githubcopilot.com/chat/completions' - args.headers = self:authenticate() - end + headers = headers, + } if is_stream then args.stream = stream_func @@ -783,7 +527,7 @@ function Copilot:ask(prompt, opts) notify.publish(notify.STATUS, 'Thinking') - local response, err = utils.curl_post(url, args) + local response, err = utils.curl_post(provider.get_url(opts), args) if self.current_job ~= job_id then return nil, nil, nil @@ -881,8 +625,8 @@ function Copilot:ask(prompt, opts) end --- List available models ----@return table -function Copilot:list_models() +---@return table +function Client:list_models() local models = self:fetch_models() -- First deduplicate by version, keeping shortest ID @@ -912,32 +656,56 @@ function Copilot:list_models() end --- List available agents ----@return table -function Copilot:list_agents() +---@return table +function Client:list_agents() local agents = self:fetch_agents() local result = vim.tbl_keys(agents) table.sort(result) local out = {} + table.insert(out, { id = 'none', name = 'None', description = 'No agent', provider = 'none' }) for _, id in ipairs(result) do - out[id] = agents[id].description + table.insert(out, vim.tbl_extend('force', agents[id], { id = id })) end return out end --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed +---@param model string ---@return table -function Copilot:embed(inputs) +function Client:embed(inputs, model) if not inputs or #inputs == 0 then return {} end + local models = self:fetch_models() + local model_config = models[model] + if not model_config then + error('Model not found: ' .. model) + end + + local provider_name = model_config.provider + if not provider_name then + error('Provider not found for model: ' .. model) + end + local provider = self.providers[model_config.provider] + if not provider then + error('Provider not found: ' .. model_config.provider) + end + provider_name = provider.embeddings + if not provider_name then + error('Provider not found for embeddings: ' .. provider_name) + end + provider = self.providers[provider_name] + if not provider then + error('Provider not found: ' .. provider_name) + end + notify.publish(notify.STATUS, 'Generating embeddings for ' .. #inputs .. ' inputs') -- Initialize essentials - local model = EMBED_MODEL local to_process = {} local results = {} local initial_chunk_size = 10 @@ -972,15 +740,15 @@ function Copilot:embed(inputs) 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), - }) + local body = vim.json.encode( + provider.prepare_input(generate_embedding_request(batch, threshold), {}, {}) ) + local response, err = utils.curl_post(provider.get_url({}), { + headers = self:authenticate(provider_name), + body = temp_file(body), + }) + 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 @@ -1030,7 +798,7 @@ end --- Stop the running job ---@return boolean -function Copilot:stop() +function Client:stop() if self.current_job ~= nil then self.current_job = nil return true @@ -1041,7 +809,7 @@ end --- Reset the history and stop any running job ---@return boolean -function Copilot:reset() +function Client:reset() local stopped = self:stop() self.history = {} self.embedding_cache = {} @@ -1051,7 +819,7 @@ end --- Save the history to a file ---@param name string: The name to save the history to ---@param path string: The path to save the history to -function Copilot:save(name, path) +function Client:save(name, path) local history = vim.json.encode(self.history) path = vim.fn.expand(path) vim.fn.mkdir(path, 'p') @@ -1071,7 +839,7 @@ end ---@param name string: The name to load the history from ---@param path string: The path to load the history from ---@return table -function Copilot:load(name, path) +function Client:load(name, path) path = vim.fn.expand(path) .. '/' .. name .. '.json' local file = io.open(path, 'r') if not file then @@ -1093,8 +861,8 @@ end --- Check if there is a running job ---@return boolean -function Copilot:running() +function Client:running() return self.current_job ~= nil end -return Copilot +return Client diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 498e21eb..ce300a75 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -45,6 +45,7 @@ local select = require('CopilotChat.select') ---@field answer_header string? ---@field error_header string? ---@field separator string? +---@field providers table ---@field contexts table? ---@field prompts table? ---@field mappings CopilotChat.config.mappings? @@ -55,7 +56,7 @@ return { system_prompt = prompts.COPILOT_INSTRUCTIONS.system_prompt, -- 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 @). + agent = 'none', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. @@ -107,6 +108,9 @@ return { error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + -- default providers + providers = require('CopilotChat.config.providers'), + -- default contexts contexts = require('CopilotChat.config.contexts'), diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua new file mode 100644 index 00000000..15e5cd6a --- /dev/null +++ b/lua/CopilotChat/config/providers.lua @@ -0,0 +1,315 @@ +local utils = require('CopilotChat.utils') + +---@class CopilotChat.Provider.model +---@field id string +---@field name string +---@field version string +---@field tokenizer string +---@field max_prompt_tokens number +---@field max_output_tokens number +---@field policy boolean + +---@class CopilotChat.Provider.agent +---@field id string +---@field name string +---@field description string + +---@class CopilotChat.Provider +---@field disabled nil|boolean +---@field embeddings nil|string +---@field get_headers fun(token:string, sessionid:string, machineid:string):table +---@field get_token fun():string,number? +---@field get_agents nil|fun(headers:table):table +---@field get_models nil|fun(headers:table):table +---@field prepare_input fun(inputs:table, opts:CopilotChat.Client.ask, model:table):table +---@field get_url fun(opts:table):string + +local EDITOR_VERSION = 'Neovim/' + .. vim.version().major + .. '.' + .. vim.version().minor + .. '.' + .. vim.version().patch + +local VERSION_HEADERS = { + ['editor-version'] = EDITOR_VERSION, + ['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', +} + +--- Get the github oauth cached token +---@return string|nil +local cached_github_token = nil +local function get_github_token() + if cached_github_token then + return cached_github_token + end + + -- loading token from the environment only in GitHub Codespaces + local token = os.getenv('GITHUB_TOKEN') + local codespaces = os.getenv('CODESPACES') + if token and codespaces then + cached_github_token = token + return token + end + + -- loading token from the file + local config_path = utils.config_path() + if not config_path then + error('Failed to find config path for GitHub token') + end + + -- token can be sometimes in apps.json sometimes in hosts.json + local file_paths = { + config_path .. '/github-copilot/hosts.json', + config_path .. '/github-copilot/apps.json', + } + + for _, file_path in ipairs(file_paths) do + if vim.fn.filereadable(file_path) == 1 then + local userdata = vim.fn.json_decode(vim.fn.readfile(file_path)) + for key, value in pairs(userdata) do + if string.find(key, 'github.com') then + cached_github_token = value.oauth_token + return value.oauth_token + end + end + end + end + + error('Failed to find GitHub token') +end + +---@type table +local M = {} + +M.copilot = { + embeddings = 'copilot_embeddings', + + get_headers = function(token, sessionid, machineid) + return vim.tbl_extend('force', { + ['authorization'] = 'Bearer ' .. token, + ['x-request-id'] = utils.uuid(), + ['vscode-sessionid'] = sessionid, + ['vscode-machineid'] = machineid, + ['copilot-integration-id'] = 'vscode-chat', + ['openai-organization'] = 'github-copilot', + ['openai-intent'] = 'conversation-panel', + ['content-type'] = 'application/json', + }, VERSION_HEADERS) + end, + + get_token = function() + local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { + headers = vim.tbl_extend('force', { + ['authorization'] = 'token ' .. get_github_token(), + ['accept'] = 'application/json', + }, VERSION_HEADERS), + }) + + if err then + error(err) + end + + if response.status ~= 200 then + error('Failed to authenticate: ' .. tostring(response.status)) + end + + local body = vim.json.decode(response.body) + return body.token, body.expires_at + end, + + get_agents = function(headers) + local response, err = utils.curl_get('https://api.githubcopilot.com/agents', { + headers = headers, + }) + + 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 + table.insert(out, { + id = agent.slug, + name = agent.name, + description = agent.description, + }) + end + + return out + end, + + get_models = function(headers) + local response, err = utils.curl_get('https://api.githubcopilot.com/models', { + headers = headers, + }) + + if err then + error(err) + end + + if response.status ~= 200 then + error('Failed to fetch models: ' .. tostring(response.status)) + end + + local models = {} + for _, model in ipairs(vim.json.decode(response.body)['data']) do + if model['capabilities']['type'] == 'chat' then + table.insert(models, { + id = model.id, + name = model.name, + version = model.version, + tokenizer = model.capabilities.tokenizer, + max_prompt_tokens = model.capabilities.limits.max_prompt_tokens, + max_output_tokens = model.capabilities.limits.max_output_tokens, + policy = not model['policy'] or model['policy']['state'] == 'enabled', + }) + end + end + + for _, model in ipairs(models) do + if not model.policy then + utils.curl_post('https://api.githubcopilot.com/models/' .. model.id .. '/policy', { + headers = headers, + body = vim.json.encode({ state = 'enabled' }), + }) + + model.policy = true + end + end + + return models + end, + + prepare_input = function(inputs, opts, model) + local is_o1 = vim.startswith(opts.model, 'o1') + + inputs = vim.tbl_map(function(input) + if is_o1 then + if input.role == 'system' then + input.role = 'user' + end + end + + return input + end, inputs) + + local out = { + messages = inputs, + model = opts.model, + stream = not is_o1, + n = 1, + } + + if not is_o1 then + out.temperature = opts.temperature + out.top_p = 1 + end + + if model.max_output_tokens then + out.max_tokens = model.max_output_tokens + end + + return out + end, + + get_url = function(opts) + if opts.agent then + return 'https://api.githubcopilot.com/agents/' .. opts.agent .. '?chat' + end + + return 'https://api.githubcopilot.com/chat/completions' + end, +} + +M.github_models = { + embeddings = 'copilot_embeddings', + + get_headers = function(token) + return { + ['authorization'] = 'bearer ' .. token, + ['content-type'] = 'application/json', + ['x-ms-useragent'] = EDITOR_VERSION, + ['x-ms-user-agent'] = EDITOR_VERSION, + } + end, + + get_token = function() + return get_github_token(), nil + end, + + get_models = function() + local response = utils.curl_post('https://api.catalog.azureml.ms/asset-gallery/v1.0/models', { + headers = { + ['content-type'] = 'application/json', + }, + body = [[ + { + "filters": [ + { "field": "freePlayground", "values": ["true"], "operator": "eq"}, + { "field": "labels", "values": ["latest"], "operator": "eq"} + ], + "order": [ + { "field": "displayName", "direction": "asc" } + ] + } + ]], + }) + + if not response or response.status ~= 200 then + error('Failed to fetch models: ' .. tostring(response and response.status)) + end + + local models = {} + for _, model in ipairs(vim.json.decode(response.body)['summaries']) do + if vim.tbl_contains(model.inferenceTasks, 'chat-completion') then + table.insert(models, { + id = model.name, + name = model.displayName, + version = model.name .. '-' .. model.version, + max_prompt_tokens = model.modelLimits.textLimits.inputContextWindow, + max_output_tokens = model.modelLimits.textLimits.maxOutputTokens, + tokenizer = 'o200k_base', + policy = true, + }) + end + end + + return models + end, + + prepare_input = M.copilot.prepare_input, + + get_url = function() + return 'https://models.inference.ai.azure.com/chat/completions' + end, +} + +M.copilot_embeddings = { + get_headers = M.copilot.get_headers, + get_token = M.copilot.get_token, + + prepare_input = function(inputs) + return { + dimensions = 512, + inputs = inputs, + model = 'text-embedding-3-small', + } + end, + + get_url = function() + return 'https://api.githubcopilot.com/embeddings' + end, +} + +return M diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index b00b29c0..19d9a553 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -577,11 +577,12 @@ function M.quickfix() end --- Filter embeddings based on the query ----@param copilot CopilotChat.Copilot +---@param client CopilotChat.Client ---@param prompt string +---@param model string ---@param embeddings table ---@return table -function M.filter_embeddings(copilot, prompt, embeddings) +function M.filter_embeddings(client, prompt, model, embeddings) -- If we dont need to embed anything, just return directly if #embeddings < MULTI_FILE_THRESHOLD then return embeddings @@ -602,7 +603,7 @@ function M.filter_embeddings(copilot, prompt, embeddings) }) -- Get embeddings from all items - embeddings = copilot:embed(embeddings) + embeddings = client:embed(embeddings, model) -- Rate embeddings by relatedness to the query local embedded_query = table.remove(embeddings, #embeddings) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ab030a13..3a4bca07 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,7 +1,7 @@ local async = require('plenary.async') local log = require('plenary.log') local default_config = require('CopilotChat.config') -local Copilot = require('CopilotChat.copilot') +local Client = require('CopilotChat.client') local context = require('CopilotChat.context') local utils = require('CopilotChat.utils') @@ -14,7 +14,7 @@ local WORD = '([^%s]+)' --- @field winnr number --- @class CopilotChat.state ---- @field copilot CopilotChat.Copilot? +--- @field client CopilotChat.Client? --- @field source CopilotChat.source? --- @field last_prompt string? --- @field last_response string? @@ -23,7 +23,7 @@ local WORD = '([^%s]+)' --- @field debug CopilotChat.ui.Debug? --- @field overlay CopilotChat.ui.Overlay? local state = { - copilot = nil, + client = nil, -- Current state tracking source = nil, @@ -279,7 +279,7 @@ local function resolve_embeddings(prompt, config) end local function resolve_agent(prompt, config) - local agents = vim.tbl_keys(state.copilot:list_agents()) + local agents = vim.tbl_keys(state.client:list_agents()) local selected_agent = config.agent prompt = prompt:gsub('@' .. WORD, function(match) if vim.tbl_contains(agents, match) then @@ -295,7 +295,7 @@ end local function resolve_model(prompt, config) local models = vim.tbl_map(function(model) return model.id - end, state.copilot:list_models()) + end, state.client:list_models()) local selected_model = config.model prompt = prompt:gsub('%$' .. WORD, function(match) @@ -507,8 +507,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 models = state.client:list_models() + local agents = state.client:list_agents() local prompts_to_use = M.prompts() local items = {} @@ -547,12 +547,13 @@ function M.complete_items(callback) } end - for name, description in pairs(agents) do + for _, agent in pairs(agents) do items[#items + 1] = { - word = '@' .. name, - abbr = name, - kind = 'agent', - menu = description, + word = '@' .. agent.id, + abbr = agent.id, + kind = agent.provider, + info = agent.description, + menu = agent.name, icase = 1, dup = 0, empty = 0, @@ -649,7 +650,7 @@ end --- Select default Copilot GPT model. function M.select_model() async.run(function() - local models = state.copilot:list_models() + local models = state.client:list_models() local choices = vim.tbl_map(function(model) return { id = model.id, @@ -680,21 +681,29 @@ end --- Select default Copilot agent. function M.select_agent() async.run(function() - local agents = vim.tbl_keys(state.copilot:list_agents()) - agents = vim.tbl_map(function(agent) - if agent == M.config.agent then - return agent .. ' (selected)' - end - - return agent + local agents = state.client:list_agents() + local choices = vim.tbl_map(function(agent) + return { + id = agent.id, + name = agent.name, + provider = agent.provider, + selected = agent.id == M.config.agent, + } end, agents) async.util.scheduler() - vim.ui.select(agents, { + vim.ui.select(choices, { prompt = 'Select an agent> ', + format_item = function(item) + local out = string.format('%s (%s)', item.id, item.provider) + if item.selected then + out = '* ' .. out + end + return out + end, }, function(choice) if choice then - M.config.agent = choice:gsub(' %(selected%)', '') + M.config.agent = choice.id end end) end) @@ -718,7 +727,7 @@ function M.ask(prompt, config) if not config.headless then if config.clear_chat_on_new_prompt then M.stop(true) - elseif state.copilot:stop() then + elseif state.client:stop() then finish() end @@ -750,7 +759,7 @@ function M.ask(prompt, config) local has_output = false local query_ok, filtered_embeddings = - pcall(context.filter_embeddings, state.copilot, prompt, embeddings) + pcall(context.filter_embeddings, state.client, prompt, selected_model, embeddings) if not query_ok then async.util.scheduler() @@ -762,7 +771,7 @@ function M.ask(prompt, config) end local ask_ok, response, token_count, token_max_count = - pcall(state.copilot.ask, state.copilot, prompt, { + pcall(state.client.ask, state.client, prompt, { selection = selection, embeddings = filtered_embeddings, system_prompt = system_prompt, @@ -818,12 +827,12 @@ end ---@param reset boolean? function M.stop(reset) if reset then - state.copilot:reset() + state.client:reset() state.chat:clear() state.last_prompt = nil state.last_response = nil else - state.copilot:stop() + state.client:stop() end finish(reset) @@ -846,7 +855,7 @@ function M.save(name, history_path) history_path = history_path or M.config.history_path if history_path then - state.copilot:save(name, history_path) + state.client:save(name, history_path) end end @@ -865,10 +874,10 @@ function M.load(name, history_path) return end - state.copilot:reset() + state.client:reset() state.chat:clear() - local history = state.copilot:load(name, history_path) + local history = state.client:load(name, history_path) for i, message in ipairs(history) do if message.role == 'user' then if i > 1 then @@ -945,10 +954,16 @@ function M.setup(config) M.config = vim.tbl_deep_extend('force', default_config, config or {}) - if state.copilot then - state.copilot:stop() + -- Save proxy and insecure settings + utils.curl_store_args({ + insecure = M.config.allow_insecure, + proxy = M.config.proxy, + }) + + if state.client then + state.client:stop() end - state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) + state.client = Client(M.config.providers) if M.config.debug then M.log_level('debug') diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 66620a82..dfca98f6 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -2,8 +2,26 @@ local async = require('plenary.async') local curl = require('plenary.curl') local scandir = require('plenary.scandir') +local DEFAULT_REQUEST_ARGS = { + timeout = 30000, + raw = { + '--retry', + '2', + '--retry-delay', + '1', + '--keepalive-time', + '60', + '--no-compressed', + '--connect-timeout', + '10', + '--tcp-nodelay', + '--no-buffer', + }, +} + local M = {} M.timers = {} +M.curl_args = {} ---@class Class ---@field new fun(...):table @@ -294,36 +312,48 @@ function M.win_cwd(winnr) return dir end +--- Store curl global arguments +---@param args table The arguments +---@return table +M.curl_store_args = function(args) + M.curl_args = vim.tbl_deep_extend('force', M.curl_args, args) + return M.curl_args +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 or {}, { - callback = callback, - on_error = function(err) - err = M.make_string(err and err.stderr or err) - callback(nil, err) - end, - }) - ) + local args = { + callback = callback, + on_error = function(err) + err = M.make_string(err and err.stderr or err) + callback(nil, err) + end, + } + + args = vim.tbl_deep_extend('force', DEFAULT_REQUEST_ARGS, args) + args = vim.tbl_deep_extend('force', M.curl_args, args) + args = vim.tbl_deep_extend('force', args, opts or {}) + curl.get(url, args) 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 or {}, { - callback = callback, - on_error = function(err) - err = M.make_string(err and err.stderr or err) - callback(nil, err) - end, - }) - ) + local args = { + callback = callback, + on_error = function(err) + err = M.make_string(err and err.stderr or err) + callback(nil, err) + end, + } + + args = vim.tbl_deep_extend('force', DEFAULT_REQUEST_ARGS, args) + args = vim.tbl_deep_extend('force', M.curl_args, args) + args = vim.tbl_deep_extend('force', args, opts or {}) + curl.post(url, args) end, 3) --- Scan a directory From 3ad8e7ff7d7a5887aab06fc461c6fc75a482c997 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 16 Feb 2025 22:09:56 +0000 Subject: [PATCH 042/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 106 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3cc97878..c27497d1 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 15 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -15,6 +15,7 @@ Table of Contents *CopilotChat-table-of-contents* - Agents |CopilotChat-agents| - Contexts |CopilotChat-contexts| - Selections |CopilotChat-selections| + - Providers |CopilotChat-providers| - API |CopilotChat-api| 4. Configuration |CopilotChat-configuration| - Default configuration |CopilotChat-default-configuration| @@ -32,14 +33,14 @@ Table of Contents *CopilotChat-table-of-contents* - Neovim 0.10.0+ - Older versions are not officially supported - 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 +- _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 improved fetching 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 @@ -385,6 +386,89 @@ You can chain multiple selections like this: < +PROVIDERS *CopilotChat-providers* + +Providers are modules that implement integration with different AI providers. +Built-in providers are: + +- `copilot` - Default GitHub Copilot provider used for chat and embeddings +- `github_models` - Provider for GitHub Marketplace models + +You can define custom providers by adding them to `providers` config. Provider +has following fields: + +- `disabled?: boolean` - Optional boolean to disable provider +- `embeddings?: string` - Optional string pointing to provider to use for embeddings +- `get_token(): string, number?` - Function that returns authentication token and optional expiry timestamp +- `get_headers(token: string, sessionid: string, machineid: string): table` - Function that returns headers for API requests +- `get_url(opts: table): string` - Function that returns API endpoint URL for given operation +- `prepare_input(inputs: table, opts: table, model: table): table` - Function that prepares request body +- `get_models?(headers: table): table` - Optional function that returns list of available models +- `get_agents?(headers: table): table` - Optional function that returns list of available agents + +Example custom provider: + +>lua + { + providers = { + my_provider = { + -- Required fields + get_token = function() + return "my-token", os.time() + 3600 -- Token valid for 1 hour + end, + get_headers = function(token, sessionid, machineid) + return { + ["authorization"] = "Bearer " .. token, + ["content-type"] = "application/json", + } + end, + get_url = function(opts) + if opts.agent then + return "https://api.custom.com/agents/" .. opts.agent + end + return "https://api.custom.com/chat" + end, + prepare_input = function(inputs, opts, model) + return { + messages = inputs, + temperature = opts.temperature, + model = opts.model, + stream = true + } + end, + + -- Optional fields + disabled = false, + embeddings = "copilot_embeddings", -- Use copilot for embeddings + get_models = function(headers) + -- Return list of available models + return { + { + id = "gpt-4", + name = "GPT-4", + version = "1.0", + tokenizer = "gpt2", + max_prompt_tokens = 8000, + max_output_tokens = 2000, + } + } + end, + get_agents = function(headers) + -- Return list of available agents + return { + { + id = "agent1", + name = "My Agent", + description = "Custom agent" + } + } + end + } + } + } +< + + API *CopilotChat-api* >lua @@ -529,6 +613,19 @@ Also see here : error_header = '# Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + -- default providers + providers = { + copilot = { + -- see config.lua for implementation + }, + github_models = { + -- see config.lua for implementation + }, + copilot_embeddings = { + -- see config.lua for implementation + }, + } + -- default contexts contexts = { buffer = { @@ -564,7 +661,6 @@ Also see here : }, Review = { prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - -- 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.', From 625ac505b2367da206f0542ad39505e05fce618f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Feb 2025 23:30:26 +0100 Subject: [PATCH 043/728] fix: improve github pages deployment config Adds proper Jekyll configuration for GitHub Pages with midnight theme and GitHub Flavored Markdown support. Fixes async token loading in providers to prevent UI blocking. - Add Jekyll config with GFM and midnight theme - Set production environment for Jekyll build - Fix async GitHub token loading with scheduler Signed-off-by: Tomas Slusny --- .github/workflows/deploy-gh-pages.yml | 15 +++++++++++---- Gemfile | 4 ++++ _config.yml | 10 ++++++++++ lua/CopilotChat/config/providers.lua | 3 +++ 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 Gemfile create mode 100644 _config.yml diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index 007d254c..ae275e16 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -27,14 +27,21 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 0 # Increment this number if you need to re-download cached gems - name: Setup Pages uses: actions/configure-pages@v4 - name: Build with Jekyll - uses: actions/jekyll-build-pages@v1 - with: - source: ./ - destination: ./_site + # Outputs to the './_site' directory by default + run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + env: + JEKYLL_ENV: production - name: Upload artifact + # Automatically uploads an artifact from the './_site' directory by default uses: actions/upload-pages-artifact@v3 # Deployment job diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..942eb83d --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem "jekyll", "~> 4.4.1" +gem "just-the-docs", "0.10.1" diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..e58db988 --- /dev/null +++ b/_config.yml @@ -0,0 +1,10 @@ +markdown: kramdown +kramdown: + toc_levels: 1..3 + auto_ids: true + parse_block_html: true + input: GFM +theme: just-the-docs +title: Copilot Chat for Neovim +description: Chat with GitHub Copilot in Neovim +url: https://copilotc-nvim.github.io/CopilotChat.nvim/ diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 15e5cd6a..660355b3 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,3 +1,4 @@ +local async = require('plenary.async') local utils = require('CopilotChat.utils') ---@class CopilotChat.Provider.model @@ -49,6 +50,8 @@ local function get_github_token() return cached_github_token end + async.util.scheduler() + -- loading token from the environment only in GitHub Codespaces local token = os.getenv('GITHUB_TOKEN') local codespaces = os.getenv('CODESPACES') From e14e2095e6e49c050b2586b64188f454d8a8169f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 00:32:15 +0100 Subject: [PATCH 044/728] Restore gh pages config Signed-off-by: Tomas Slusny --- .github/workflows/deploy-gh-pages.yml | 15 ++++----------- Gemfile | 4 ---- _config.yml | 5 +---- 3 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 Gemfile diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index ae275e16..007d254c 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -27,21 +27,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - cache-version: 0 # Increment this number if you need to re-download cached gems - name: Setup Pages uses: actions/configure-pages@v4 - name: Build with Jekyll - # Outputs to the './_site' directory by default - run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" - env: - JEKYLL_ENV: production + uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site - name: Upload artifact - # Automatically uploads an artifact from the './_site' directory by default uses: actions/upload-pages-artifact@v3 # Deployment job diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 942eb83d..00000000 --- a/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source 'https://rubygems.org' - -gem "jekyll", "~> 4.4.1" -gem "just-the-docs", "0.10.1" diff --git a/_config.yml b/_config.yml index e58db988..cffdd4ff 100644 --- a/_config.yml +++ b/_config.yml @@ -4,7 +4,4 @@ kramdown: auto_ids: true parse_block_html: true input: GFM -theme: just-the-docs -title: Copilot Chat for Neovim -description: Chat with GitHub Copilot in Neovim -url: https://copilotc-nvim.github.io/CopilotChat.nvim/ +theme: jekyll-theme-midnight From 242b697182e2081ba1b8f92e9dd9619043a2825f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 00:58:18 +0100 Subject: [PATCH 045/728] feat: migrate from Jekyll to Docsify (#774) * feat: migrate from Jekyll to Docsify Replace Jekyll-based documentation with Docsify for simpler and more flexible documentation setup. This change: - Removes Jekyll configuration and build step from GH Pages workflow - Adds basic Docsify setup with search and image zoom plugins - Creates .nojekyll file to disable GitHub Pages Jekyll processing The migration provides a more maintainable documentation system that works better with single-page documentation sites. 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> --- .github/workflows/deploy-gh-pages.yml | 7 ++---- .nojekyll | 0 _config.yml | 7 ------ index.html | 34 +++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 .nojekyll delete mode 100644 _config.yml create mode 100644 index.html diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index 007d254c..76bbce4e 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -29,13 +29,10 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v4 - - name: Build with Jekyll - uses: actions/jekyll-build-pages@v1 - with: - source: ./ - destination: ./_site - name: Upload artifact uses: actions/upload-pages-artifact@v3 + with: + path: "." # Deployment job deploy: diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/_config.yml b/_config.yml deleted file mode 100644 index cffdd4ff..00000000 --- a/_config.yml +++ /dev/null @@ -1,7 +0,0 @@ -markdown: kramdown -kramdown: - toc_levels: 1..3 - auto_ids: true - parse_block_html: true - input: GFM -theme: jekyll-theme-midnight diff --git a/index.html b/index.html new file mode 100644 index 00000000..05f465ac --- /dev/null +++ b/index.html @@ -0,0 +1,34 @@ + + + + + + Copilot Chat for Neovim + + + + + +
+ + + + + + + + + + + From 573c2212b06426487f8722af44f4ac50f0508e14 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 01:05:18 +0100 Subject: [PATCH 046/728] feat: add Vim and Lua syntax highlighting Add Prism.js language components for Vim and Lua syntax highlighting in the documentation website. --- index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.html b/index.html index 05f465ac..745f3ffd 100644 --- a/index.html +++ b/index.html @@ -30,5 +30,7 @@ + + From 246a85e46959977d3f7ea94b2c74ff688aac9e80 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 00:06:12 +0000 Subject: [PATCH 047/728] 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 c27497d1..7e57169f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 16 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 February 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From e53c44c725e3b02ac5c6df71d4d32ed2c3870d04 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 01:11:00 +0100 Subject: [PATCH 048/728] docs(readme): normalize heading hierarchy Change all H3 (###) section headers to H2 (##) for better document structure and consistency. This improves the readability and navigation of the README.md file by maintaining a proper heading hierarchy. Remove roadmap section as it was outdated and contained only two generic items. --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 94c39172..488d2e22 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 # Installation -### [Lazy.nvim](https://github.com/folke/lazy.nvim) +## [Lazy.nvim](https://github.com/folke/lazy.nvim) ```lua return { @@ -57,7 +57,7 @@ return { 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) +## [Vim-Plug](https://github.com/junegunn/vim-plug) Similar to the lazy setup, you can use the following configuration: @@ -75,7 +75,7 @@ require("CopilotChat").setup { EOF ``` -### Manual +## Manual 1. Put the files in the right place @@ -872,14 +872,9 @@ This sets the `selection = false` to be able to ask generic questions unrelated -# Roadmap - -- Improved caching for context (persistence through restarts/smarter caching) -- General QOL improvements - # Development -### Installing Pre-commit Tool +## Installing Pre-commit Tool For development, you can use the provided Makefile command to install the pre-commit tool: @@ -973,6 +968,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome! -### Stargazers over time +## Stargazers over time [![Stargazers over time](https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg)](https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim) From 52583be0467a9a30b6ecfa08e320e80d5d5b0fa6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 00:11:49 +0000 Subject: [PATCH 049/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 7e57169f..c844af3c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -5,6 +5,9 @@ Table of Contents *CopilotChat-table-of-contents* 1. Requirements |CopilotChat-requirements| 2. Installation |CopilotChat-installation| + - Lazy.nvim |CopilotChat-lazy.nvim| + - Vim-Plug |CopilotChat-vim-plug| + - Manual |CopilotChat-manual| 3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Chat Mappings |CopilotChat-chat-mappings| @@ -21,10 +24,11 @@ Table of Contents *CopilotChat-table-of-contents* - Default configuration |CopilotChat-default-configuration| - Customizing buffers |CopilotChat-customizing-buffers| 5. Tips |CopilotChat-tips| -6. Roadmap |CopilotChat-roadmap| -7. Development |CopilotChat-development| -8. Contributors |CopilotChat-contributors| -9. Links |CopilotChat-links| +6. Development |CopilotChat-development| + - Installing Pre-commit Tool |CopilotChat-installing-pre-commit-tool| +7. Contributors |CopilotChat-contributors| + - Stargazers over time |CopilotChat-stargazers-over-time| +8. Links |CopilotChat-links| ============================================================================== @@ -52,7 +56,7 @@ Table of Contents *CopilotChat-table-of-contents* 2. Installation *CopilotChat-installation* -LAZY.NVIM ~ +LAZY.NVIM *CopilotChat-lazy.nvim* >lua return { @@ -75,7 +79,7 @@ See @jellydn for configuration -VIM-PLUG ~ +VIM-PLUG *CopilotChat-vim-plug* Similar to the lazy setup, you can use the following configuration: @@ -94,7 +98,7 @@ Similar to the lazy setup, you can use the following configuration: < -MANUAL ~ +MANUAL *CopilotChat-manual* 1. Put the files in the right place @@ -910,17 +914,10 @@ to current code. ============================================================================== -6. Roadmap *CopilotChat-roadmap* +6. Development *CopilotChat-development* -- Improved caching for context (persistence through restarts/smarter caching) -- General QOL improvements - -============================================================================== -7. Development *CopilotChat-development* - - -INSTALLING PRE-COMMIT TOOL ~ +INSTALLING PRE-COMMIT TOOL *CopilotChat-installing-pre-commit-tool* For development, you can use the provided Makefile command to install the pre-commit tool: @@ -933,7 +930,7 @@ This will install the pre-commit tool and the pre-commit hooks. ============================================================================== -8. Contributors *CopilotChat-contributors* +7. Contributors *CopilotChat-contributors* If you want to contribute to this project, please read the CONTRIBUTING.md file. @@ -946,12 +943,12 @@ gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguy Contributions of any kind are welcome! -STARGAZERS OVER TIME ~ +STARGAZERS OVER TIME *CopilotChat-stargazers-over-time* ============================================================================== -9. Links *CopilotChat-links* +8. Links *CopilotChat-links* 1. *@jellydn*: 2. *@deathbeam*: From 6b005f7d7e8be5ea89f3ab94c47ed838f3dd04da Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 01:38:55 +0100 Subject: [PATCH 050/728] docs(readme): update documentation badge link Replace the old documentation badge with a new one pointing to the GitHub Pages site at copilotc-nvim.github.io. This provides better visibility for the project's documentation. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 488d2e22..3622c934 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ [![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/actions/workflows/ci.yml) +[![Documentation](https://img.shields.io/badge/documentation-up-green.svg?logo=vim&style=for-the-badge)](https://copilotc-nvim.github.io/CopilotChat.nvim/) + [![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)](/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 7335fda0f7d6cf0985194a4bc76d40f58620d2d9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 01:47:44 +0100 Subject: [PATCH 051/728] fix: add scheduler in register function Ensures proper async scheduling when accessing vim registers by adding async.util.scheduler() call before register operations. --- lua/CopilotChat/context.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 19d9a553..2610bbdc 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -524,6 +524,8 @@ end ---@param register string ---@return CopilotChat.context.embed? function M.register(register) + async.util.scheduler() + local lines = vim.fn.getreg(register) if not lines or lines == '' then return nil From ce3bec6a86faea19ec9f5a2caf5464cb79b7c5e8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 02:00:22 +0100 Subject: [PATCH 052/728] feat: remove policy field from provider configuration Removes unused policy field from provider model configuration and reorganizes tokenizer field placement for better code organization. This change simplifies the provider model structure by removing unnecessary state tracking. BREAKING CHANGE: Removes policy field from provider configuration --- lua/CopilotChat/config/providers.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 660355b3..59bdef43 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -8,7 +8,6 @@ local utils = require('CopilotChat.utils') ---@field tokenizer string ---@field max_prompt_tokens number ---@field max_output_tokens number ----@field policy boolean ---@class CopilotChat.Provider.agent ---@field id string @@ -186,8 +185,6 @@ M.copilot = { headers = headers, body = vim.json.encode({ state = 'enabled' }), }) - - model.policy = true end end @@ -280,10 +277,9 @@ M.github_models = { id = model.name, name = model.displayName, version = model.name .. '-' .. model.version, + tokenizer = 'o200k_base', max_prompt_tokens = model.modelLimits.textLimits.inputContextWindow, max_output_tokens = model.modelLimits.textLimits.maxOutputTokens, - tokenizer = 'o200k_base', - policy = true, }) end end From cdb9c5fb4fbbc2b3fb4206af2cdf014f2ff9c594 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 09:29:06 +0100 Subject: [PATCH 053/728] feat: switch documentation to dark theme Changes the docsify theme from vue to simple-dark theme and adds error logging for failed embedding requests. This improves readability for users who prefer dark mode and helps with debugging embedding issues. - Replace vue.css with theme-simple-dark.css - Add docsify-themeable script - Add debug logging for embedding errors --- index.html | 3 ++- lua/CopilotChat/client.lua | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 745f3ffd..eb5f1ae4 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ @@ -26,6 +26,7 @@ + diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 16a53c1c..f86330db 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -750,6 +750,7 @@ function Client:embed(inputs, model) }) if err or not response or response.status ~= 200 then + log.debug('Failed to get embeddings: ', err) attempts = attempts + 1 -- If we have few items and the request failed, try reducing threshold first if #batch <= 5 then From 1eef6431c99d0469a0da6a9e42171f8d1fea5d0a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 10:00:13 +0100 Subject: [PATCH 054/728] refactor: simplify HTTP headers Remove unnecessary HTTP headers and standardize header casing across the codebase. This simplifies the header management by: - Removing unused machine ID and session ID tracking - Using consistent header casing (capitalized) - Removing redundant version headers - Simplifying authorization header format These changes make the code more maintainable while preserving core functionality. Source: https://github.com/zed-industries/zed/blob/ad43bbbf5eda59eba65309735472e0be58b4f7dd/crates/copilot/src/copilot_chat.rs#L324 --- lua/CopilotChat/client.lua | 4 +-- lua/CopilotChat/config/providers.lua | 42 ++++++++++------------------ 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index f86330db..3ee52ca5 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -214,7 +214,6 @@ local Client = class(function(self, providers) self.current_job = nil self.expires_at = nil self.headers = nil - self.machineid = utils.machine_id() end) --- Authenticate with GitHub and get the required headers @@ -229,8 +228,7 @@ function Client:authenticate(provider_name) notify.publish(notify.STATUS, 'Authenticating to provider ' .. provider_name) local token, expires_at = provider.get_token() - local sessionid = utils.uuid() .. tostring(math.floor(os.time() * 1000)) - headers = provider.get_headers(token, sessionid, self.machineid) + headers = provider.get_headers(token) self.provider_cache[provider_name].headers = headers self.provider_cache[provider_name].expires_at = expires_at end diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 59bdef43..5ffee60f 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -17,7 +17,7 @@ local utils = require('CopilotChat.utils') ---@class CopilotChat.Provider ---@field disabled nil|boolean ---@field embeddings nil|string ----@field get_headers fun(token:string, sessionid:string, machineid:string):table +---@field get_headers fun(token:string):table ---@field get_token fun():string,number? ---@field get_agents nil|fun(headers:table):table ---@field get_models nil|fun(headers:table):table @@ -31,16 +31,6 @@ local EDITOR_VERSION = 'Neovim/' .. '.' .. vim.version().patch -local VERSION_HEADERS = { - ['editor-version'] = EDITOR_VERSION, - ['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', -} - --- Get the github oauth cached token ---@return string|nil local cached_github_token = nil @@ -92,25 +82,21 @@ local M = {} M.copilot = { embeddings = 'copilot_embeddings', - get_headers = function(token, sessionid, machineid) - return vim.tbl_extend('force', { - ['authorization'] = 'Bearer ' .. token, - ['x-request-id'] = utils.uuid(), - ['vscode-sessionid'] = sessionid, - ['vscode-machineid'] = machineid, - ['copilot-integration-id'] = 'vscode-chat', - ['openai-organization'] = 'github-copilot', - ['openai-intent'] = 'conversation-panel', - ['content-type'] = 'application/json', - }, VERSION_HEADERS) + get_headers = function(token) + return { + ['Authorization'] = 'Bearer ' .. token, + ['Editor-Version'] = EDITOR_VERSION, + ['Copilot-Integration-Id'] = 'vscode-chat', + ['Content-Type'] = 'application/json', + } end, get_token = function() local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { - headers = vim.tbl_extend('force', { - ['authorization'] = 'token ' .. get_github_token(), - ['accept'] = 'application/json', - }, VERSION_HEADERS), + headers = { + ['Authorization'] = 'Token ' .. get_github_token(), + ['Accept'] = 'application/json', + } }) if err then @@ -237,8 +223,8 @@ M.github_models = { get_headers = function(token) return { - ['authorization'] = 'bearer ' .. token, - ['content-type'] = 'application/json', + ['Authorization'] = 'Bearer ' .. token, + ['Content-Type'] = 'application/json', ['x-ms-useragent'] = EDITOR_VERSION, ['x-ms-user-agent'] = EDITOR_VERSION, } From 56969086c7a20470da11f8546ce22272cf893be4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 10:04:22 +0100 Subject: [PATCH 055/728] style: add missing trailing comma in headers table Fix code style consistency by adding a trailing comma in the headers table definition in providers.lua --- lua/CopilotChat/config/providers.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5ffee60f..95a78293 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -96,7 +96,7 @@ M.copilot = { headers = { ['Authorization'] = 'Token ' .. get_github_token(), ['Accept'] = 'application/json', - } + }, }) if err then From 61dceb7e495f6e26d4ac594b266fe11bcc795366 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 10:48:04 +0100 Subject: [PATCH 056/728] fix: improve ollama response handling & error tracking Enhance the response handling for Ollama API by: - Adding debug logging for response lines - Handling empty line responses - Supporting both standard and Ollama-specific response formats - Improving finish reason handling for stream completion - Simplifying stream line parsing logic The changes make the client more robust when dealing with different response formats from the Ollama API while providing better debug information. --- lua/CopilotChat/client.lua | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 3ee52ca5..81421395 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -403,10 +403,11 @@ function Client:ask(prompt, opts) end local function parse_line(line, job) - if not line then + if not line or line == '' then return end + log.debug('Response line: ', line) notify.publish(notify.STATUS, '') local ok, content = pcall(vim.json.decode, line, { @@ -442,12 +443,14 @@ function Client:ask(prompt, opts) end end - if not content.choices or #content.choices == 0 then - return + local choice + if content.choices and #content.choices > 0 then + choice = content.choices[1] + else + choice = content end last_message = content - local choice = content.choices[1] content = choice.message and choice.message.content or choice.delta and choice.delta.content if content then @@ -458,22 +461,22 @@ function Client:ask(prompt, opts) on_progress(content) end - if choice.finish_reason and job then - local reason = choice.finish_reason - if reason == 'stop' then - reason = nil - else - reason = 'Early stop: ' .. reason + if job then + local reason = choice.finish_reason or choice.done_reason + + if reason then + if reason == 'stop' then + reason = nil + else + reason = 'Early stop: ' .. reason + end + finish_stream(reason, job) end - finish_stream(reason, job) end end local function parse_stream_line(line, job) line = vim.trim(line) - if not vim.startswith(line, 'data:') then - return - end line = line:gsub('^data:', '') line = vim.trim(line) From 7d717e4991489ae932fe9ad98a7a0809962203c4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 11:53:35 +0100 Subject: [PATCH 057/728] fix: improve provider handling and model uniqueness Makes the provider handling more robust by: - Making get_token optional for providers - Adding error handling for model and agent fetching - Ensuring unique model/agent IDs by appending provider name on conflicts - Sorting providers for consistent order - Removing provider suffix from model name before request These changes improve reliability when multiple providers are configured and prevent conflicts between models/agents with the same IDs from different providers. --- lua/CopilotChat/client.lua | 46 ++++++++++++++++++++-------- lua/CopilotChat/config/providers.lua | 2 +- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 81421395..2d87cd6e 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -225,9 +225,12 @@ function Client:authenticate(provider_name) local expires_at = self.provider_cache[provider_name].expires_at if not headers or (expires_at and expires_at <= math.floor(os.time())) then - notify.publish(notify.STATUS, 'Authenticating to provider ' .. provider_name) + local token + if provider.get_token then + notify.publish(notify.STATUS, 'Authenticating to provider ' .. provider_name) + token, expires_at = provider.get_token() + end - local token, expires_at = provider.get_token() headers = provider.get_headers(token) self.provider_cache[provider_name].headers = headers self.provider_cache[provider_name].expires_at = expires_at @@ -244,20 +247,30 @@ function Client:fetch_models() end local models = {} - for provider_name, provider in pairs(self.providers) do + local provider_order = vim.tbl_keys(self.providers) + table.sort(provider_order) + for _, provider_name in ipairs(provider_order) do + local provider = self.providers[provider_name] if not provider.disabled and provider.get_models then local headers = self:authenticate(provider_name) notify.publish(notify.STATUS, 'Fetching models from ' .. provider_name) - local provider_models = provider.get_models(headers) - for _, model in ipairs(provider_models) do - model.provider = provider_name - if not models[model.id] then + local ok, provider_models = pcall(provider.get_models, headers) + if ok then + for _, model in ipairs(provider_models) do + model.provider = provider_name + if models[model.id] then + model.id = model.id .. ':' .. provider_name + model.version = model.version .. ':' .. provider_name + end models[model.id] = model end + else + log.warn('Failed to fetch models from ' .. provider_name .. ': ' .. provider_models) end end end + log.debug('Fetched models: ', vim.inspect(models)) self.models = models return self.models end @@ -270,16 +283,24 @@ function Client:fetch_agents() end local agents = {} - for provider_name, provider in pairs(self.providers) do + local provider_order = vim.tbl_keys(self.providers) + table.sort(provider_order) + for _, provider_name in ipairs(provider_order) do + local provider = self.providers[provider_name] if not provider.disabled and provider.get_agents then local headers = self:authenticate(provider_name) notify.publish(notify.STATUS, 'Fetching agents from ' .. provider_name) - local provider_agents = provider.get_agents(headers) - for _, agent in ipairs(provider_agents) do - agent.provider = provider_name - if not agents[agent.id] then + local ok, provider_agents = pcall(provider.get_agents, headers) + if ok then + for _, agent in ipairs(provider_agents) do + agent.provider = provider_name + if agents[agent.id] then + agent.id = provider_name .. ':' .. agent.id + end agents[agent.id] = agent end + else + log.warn('Failed to fetch agents from ' .. provider_name .. ': ' .. provider_agents) end end end @@ -508,6 +529,7 @@ function Client:ask(prompt, opts) parse_stream_line(line, job) end + opts.model = opts.model:gsub(':' .. provider_name .. '$', '') local headers = self:authenticate(provider_name) local request = provider.prepare_input( generate_ask_request(history, prompt, system_prompt, generated_messages), diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 95a78293..7d106a93 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -18,7 +18,7 @@ local utils = require('CopilotChat.utils') ---@field disabled nil|boolean ---@field embeddings nil|string ---@field get_headers fun(token:string):table ----@field get_token fun():string,number? +---@field get_token nil|fun():string,number? ---@field get_agents nil|fun(headers:table):table ---@field get_models nil|fun(headers:table):table ---@field prepare_input fun(inputs:table, opts:CopilotChat.Client.ask, model:table):table From b634f04ee275cba4870533a69b89054a7e1c1cb3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 12:14:34 +0100 Subject: [PATCH 058/728] fix: change agent/model id provider suffix format The change modifies how provider suffix is added to agent/model IDs to ensure consistent handling across the codebase. Now the provider name is appended to the ID instead of being prepended, and the suffix is properly stripped when making API calls. --- lua/CopilotChat/client.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 2d87cd6e..5169c6ef 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -295,7 +295,7 @@ function Client:fetch_agents() for _, agent in ipairs(provider_agents) do agent.provider = provider_name if agents[agent.id] then - agent.id = provider_name .. ':' .. agent.id + agent.id = agent.id .. ':' .. provider_name end agents[agent.id] = agent end @@ -529,6 +529,7 @@ function Client:ask(prompt, opts) parse_stream_line(line, job) end + opts.agent = opts.agent and opts.agent:gsub(':' .. provider_name .. '$', '') opts.model = opts.model:gsub(':' .. provider_name .. '$', '') local headers = self:authenticate(provider_name) local request = provider.prepare_input( From 81225759dccd65856cd31879dad63c9259c70f5d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 15:56:43 +0100 Subject: [PATCH 059/728] refactor(mappings): move callback functions to mapping config Restructures the codebase by moving mapping-related callback functions from init.lua to config/mappings.lua. This improves code organization and makes mapping configuration more self-contained. - Adds callback field to mapping config class - Moves all mapping callback functions to their respective mapping configs - Extracts shared functions to utils module - Simplifies mapping registration by using callback field Signed-off-by: Tomas Slusny --- README.md | 85 ++-- lua/CopilotChat/config/mappings.lua | 393 +++++++++++++++ lua/CopilotChat/init.lua | 726 ++++++---------------------- lua/CopilotChat/ui/diff.lua | 13 +- lua/CopilotChat/utils.lua | 33 ++ 5 files changed, 610 insertions(+), 640 deletions(-) diff --git a/README.md b/README.md index 3622c934..1c3a61c4 100644 --- a/README.md +++ b/README.md @@ -360,63 +360,49 @@ You can define custom providers by adding them to `providers` config. Provider h - `get_models?(headers: table): table` - Optional function that returns list of available models - `get_agents?(headers: table): table` - Optional function that returns list of available agents -Example custom provider: +Here is how you implement [ollama](https://ollama.com/) provider for example: ```lua { providers = { - my_provider = { - -- Required fields - get_token = function() - return "my-token", os.time() + 3600 -- Token valid for 1 hour - end, - get_headers = function(token, sessionid, machineid) + ollama = { + get_headers = function() return { - ["authorization"] = "Bearer " .. token, - ["content-type"] = "application/json", + ['Content-Type'] = 'application/json', } end, - get_url = function(opts) - if opts.agent then - return "https://api.custom.com/agents/" .. opts.agent + + get_models = function() + local response = cutils.curl_get('http://localhost:11434/api/tags') + + if not response or response.status ~= 200 then + error('Failed to fetch models: ' .. tostring(response and response.status)) + end + + local models = {} + for _, model in ipairs(vim.json.decode(response.body)['models']) do + table.insert(models, { + id = model.name, + name = model.name, + version = "latest", + tokenizer = "o200k_base", + }) end - return "https://api.custom.com/chat" + + return models end, - prepare_input = function(inputs, opts, model) + + prepare_input = function(inputs, opts) return { - messages = inputs, - temperature = opts.temperature, model = opts.model, - stream = true + messages = inputs, + stream = true, } end, - -- Optional fields - disabled = false, - embeddings = "copilot_embeddings", -- Use copilot for embeddings - get_models = function(headers) - -- Return list of available models - return { - { - id = "gpt-4", - name = "GPT-4", - version = "1.0", - tokenizer = "gpt2", - max_prompt_tokens = 8000, - max_output_tokens = 2000, - } - } + get_url = function() + return 'http://localhost:11434/api/chat' end, - get_agents = function(headers) - -- Return list of available agents - return { - { - id = "agent1", - name = "My Agent", - description = "Custom agent" - } - } - end } } } @@ -564,47 +550,39 @@ Also see [here](/lua/CopilotChat/config.lua): separator = '───', -- Separator to use in chat -- default providers + -- see config/providers.lua for implementation providers = { copilot = { - -- see config.lua for implementation }, github_models = { - -- see config.lua for implementation }, copilot_embeddings = { - -- see config.lua for implementation }, } -- default contexts + -- see config/contexts.lua for implementation contexts = { buffer = { - -- see config.lua for implementation }, buffers = { - -- see config.lua for implementation }, file = { - -- see config.lua for implementation }, files = { - -- see config.lua for implementation }, git = { - -- see config.lua for implementation }, url = { - -- see config.lua for implementation }, register = { - -- see config.lua for implementation }, quickfix = { - -- see config.lua for implementation }, }, -- default prompts + -- see config/prompts.lua for implementation prompts = { Explain = { prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', @@ -630,6 +608,7 @@ Also see [here](/lua/CopilotChat/config.lua): }, -- default mappings + -- see config/mappings.lua for implementation mappings = { complete = { insert = '', diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 13c4e3fd..5bae761a 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,7 +1,105 @@ +local async = require('plenary.async') +local copilot = require('CopilotChat') +local utils = require('CopilotChat.utils') + +---@param chat CopilotChat.ui.Chat +---@return CopilotChat.ui.Diff.Diff? +local function get_diff(chat) + local config = chat.config + local block = chat:get_closest_block() + + -- If no block found, return nil + if not block then + return nil + end + + -- Initialize variables with selection if available + local header = block.header + local selection = copilot.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 + 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.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 + bufnr = win_buf + break + end + end + + 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 + 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 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 '', + filetype = filetype or '', + filename = filename, + start_line = start_line, + end_line = end_line, + bufnr = bufnr, + } +end + +---@param winnr number +---@param bufnr number +---@param start_line number +---@param end_line number +---@param config CopilotChat.config.shared +local function jump_to_diff(winnr, bufnr, start_line, end_line, config) + 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, {}) + pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) + copilot.update_selection(config) +end + +---@param diff CopilotChat.ui.Diff.Diff? +---@param config CopilotChat.config.shared +local function apply_diff(diff, config) + if not diff or not diff.bufnr then + 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 }) + 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, config) +end + ---@class CopilotChat.config.mapping ---@field normal string? ---@field insert string? ---@field detail string? +---@field callback fun(overlay: CopilotChat.ui.Overlay, diff: CopilotChat.ui.Diff.Diff, chat: CopilotChat.ui.Chat, source: CopilotChat.source) ---@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping ---@field register string? @@ -27,51 +125,346 @@ return { complete = { insert = '', + callback = function(overlay, diff, chat) + copilot.trigger_complete(true) + end, }, + close = { normal = 'q', insert = '', + callback = function(overlay, diff, chat) + copilot.close() + end, }, + reset = { normal = '', insert = '', + callback = function(overlay, diff, chat) + copilot.reset() + end, }, + submit_prompt = { normal = '', insert = '', + callback = function(overlay, diff, chat) + local section = chat:get_closest_section() + if not section or section.answer then + return + end + + copilot.ask(section.content) + end, }, + toggle_sticky = { detail = 'Makes line under cursor sticky or deletes sticky line.', normal = 'gr', + callback = function(overlay, diff, chat) + local section = 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 + end + + local cursor = vim.api.nvim_win_get_cursor(0) + local cur_line = cursor[1] + vim.api.nvim_buf_set_lines(chat.bufnr, cur_line - 1, cur_line, false, {}) + + if vim.startswith(current_line, '> ') then + return + end + + 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 + elseif i >= 2 then + break + end + + 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(chat.bufnr, insert_line - 1, insert_line - 1, false, to_insert) + vim.api.nvim_win_set_cursor(0, cursor) + end, }, accept_diff = { normal = '', insert = '', + callback = function(overlay, diff, chat, source) + apply_diff(get_diff(chat), chat.config) + end, }, + jump_to_diff = { normal = 'gj', + callback = function(overlay, diff, chat, source) + if not source or not source.winnr or not vim.api.nvim_win_is_valid(source.winnr) then + return + end + + local diff = get_diff(chat) + if not diff then + return + end + + local diff_bufnr = diff.bufnr + + -- If buffer is not found, try to load it + if not diff_bufnr then + diff_bufnr = vim.fn.bufadd(diff.filename) + vim.fn.bufload(diff_bufnr) + end + + source.bufnr = diff_bufnr + vim.api.nvim_win_set_buf(source.winnr, diff_bufnr) + + jump_to_diff(source.winnr, diff_bufnr, diff.start_line, diff.end_line, chat.config) + end, }, + quickfix_answers = { normal = 'gqa', + callback = function(overlay, diff, chat) + local items = {} + for i, section in ipairs(chat.sections) do + if section.answer then + local prev_section = chat.sections[i - 1] + local text = '' + if prev_section then + text = vim.trim( + table.concat( + vim.api.nvim_buf_get_lines( + chat.bufnr, + prev_section.start_line - 1, + prev_section.end_line, + false + ), + ' ' + ) + ) + end + + table.insert(items, { + bufnr = chat.bufnr, + lnum = section.start_line, + end_lnum = section.end_line, + text = text, + }) + end + end + + vim.fn.setqflist(items) + vim.cmd('copen') + end, }, + quickfix_diffs = { normal = 'gqd', + callback = function(overlay, diff, chat) + local selection = copilot.get_selection(chat.config) + local items = {} + + for _, section in ipairs(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 + + 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 = chat.bufnr, + lnum = block.start_line, + end_lnum = block.end_line, + text = text, + }) + end + end + + vim.fn.setqflist(items) + vim.cmd('copen') + end, }, + yank_diff = { normal = 'gy', register = '"', -- Default register to use for yanking + callback = function(overlay, diff, chat) + local content = get_diff(chat) + if not content then + return + end + + vim.fn.setreg(copilot.config.mappings.yank_diff.register, content.change) + end, }, + show_diff = { normal = 'gd', full_diff = false, -- Show full diff instead of unified diff when showing diff window + callback = function(overlay, diff, chat) + local content = get_diff(chat) + if not content then + return + end + + diff:show(content, chat.winnr, copilot.config.mappings.show_diff.full_diff) + end, }, + show_info = { normal = 'gi', + callback = function(overlay, diff, chat) + local section = chat:get_closest_section() + if not section or section.answer then + return + end + + local lines = {} + local prompt, config = copilot.resolve_prompts(section.content, chat.config) + local system_prompt = config.system_prompt + + async.run(function() + local selected_agent = copilot.resolve_agent(prompt, config) + local selected_model = copilot.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() + overlay:show(vim.trim(table.concat(lines, '\n')) .. '\n', chat.winnr, 'markdown') + end) + end, }, + show_context = { normal = 'gc', + callback = function(overlay, diff, chat) + local section = chat:get_closest_section() + if not section or section.answer then + return + end + + local lines = {} + + local selection = copilot.get_selection(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 = {} + if section and not section.answer then + embeddings = copilot.resolve_embeddings(section.content, chat.config) + end + + for _, embedding in ipairs(embeddings) do + 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 + + 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() + overlay:show(vim.trim(table.concat(lines, '\n')) .. '\n', chat.winnr, 'markdown') + end) + end, }, + show_help = { normal = 'gh', + callback = function(overlay, diff, chat) + 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' + + chat_help = chat_help .. '\n**`Mappings`**\n' + local chat_keys = vim.tbl_keys(copilot.config.mappings) + table.sort(chat_keys, function(a, b) + a = copilot.config.mappings[a] + a = a.normal or a.insert + b = copilot.config.mappings[b] + b = b.normal or b.insert + return a < b + end) + for _, name in ipairs(chat_keys) do + if name ~= 'close' then + local info = utils.key_to_info(name, copilot.config.mappings[name], '`') + if info ~= '' then + chat_help = chat_help .. info .. '\n' + end + end + end + overlay:show(chat_help, chat.winnr, 'markdown') + end, }, } diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 3a4bca07..7a07b9ae 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,7 +1,5 @@ local async = require('plenary.async') local log = require('plenary.log') -local default_config = require('CopilotChat.config') -local Client = require('CopilotChat.client') local context = require('CopilotChat.context') local utils = require('CopilotChat.utils') @@ -39,25 +37,6 @@ local state = { debug = nil, } ----@param config CopilotChat.config.shared ----@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 - - if - config - and config.selection - and utils.buf_valid(bufnr) - and winnr - and vim.api.nvim_win_is_valid(winnr) - then - return config.selection(state.source) - end - - return nil -end - --- Highlights the selection in the source buffer. ---@param clear boolean ---@param config CopilotChat.config.shared @@ -71,7 +50,7 @@ local function highlight_selection(clear, config) return end - local selection = get_selection(config) + local selection = M.get_selection(config) if not selection or not utils.buf_valid(selection.bufnr) @@ -88,116 +67,132 @@ local function highlight_selection(clear, config) }) end ---- Updates the selection based on previous window ----@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 - state.source = { - bufnr = vim.api.nvim_win_get_buf(prev_winnr), - winnr = prev_winnr, - } +---@param start_of_chat boolean? +local function finish(start_of_chat) + if not start_of_chat then + state.chat:append('\n\n') end - highlight_selection(false, config) -end + state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') ----@param config CopilotChat.config.shared ----@return CopilotChat.ui.Diff.Diff? -local function get_diff(config) - local block = state.chat:get_closest_block() + -- Add default sticky prompts after reset + if start_of_chat then + if M.config.sticky then + local last_prompt = state.last_prompt or '' - -- If no block found, return nil - if not block then - return nil + if type(M.config.sticky) == 'table' then + for _, sticky in ipairs(M.config.sticky) do + last_prompt = last_prompt .. '\n> ' .. sticky + end + else + last_prompt = last_prompt .. '\n> ' .. M.config.sticky + end + + state.last_prompt = last_prompt + end end - -- Initialize variables with selection if available - local header = block.header - 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 - 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.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 - bufnr = win_buf - break + -- Reinsert sticky prompts from last prompt + if state.last_prompt then + local has_sticky = false + 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') + end + end - filename = header.filename - filetype = header.filetype or vim.filetype.match({ filename = filename }) - start_line = header.start_line - end_line = header.end_line + state.chat:finish() +end - -- 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 +---@param err string|table|nil +---@param append_newline boolean? +local function show_error(err, append_newline) + err = err or 'Unknown error' + + if type(err) == 'string' then + local message = err:match('^[^:]+:[^:]+:(.+)') or err + message = message:gsub('^%s*', '') + err = message + else + err = utils.make_string(err) end - -- 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 + if append_newline then + state.chat:append('\n') end - return { - change = block.content, - reference = reference or '', - filetype = filetype or '', - filename = filename, - start_line = start_line, - end_line = end_line, - bufnr = bufnr, - } + state.chat:append(M.config.error_header .. '\n```error\n' .. err .. '\n```') + finish() end ----@param winnr number +--- Map a key to a function. +---@param name string ---@param bufnr number ----@param start_line number ----@param end_line number ----@param config CopilotChat.config.shared -local function jump_to_diff(winnr, bufnr, start_line, end_line, config) - 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, {}) - pcall(vim.api.nvim_buf_set_mark, bufnr, ']', end_line, 0, {}) - update_selection(config) -end - ----@param diff CopilotChat.ui.Diff.Diff? ----@param config CopilotChat.config.shared -local function apply_diff(diff, config) - if not diff or not diff.bufnr then +---@param fn function? +local function map_key(name, bufnr, fn) + local key = M.config.mappings[name] + if not key then return end - local winnr = vim.fn.win_findbuf(diff.bufnr)[1] - if not winnr then - return + if not fn then + fn = function() + key.callback(state.overlay, state.diff, state.chat, state.source) + end + end + + if key.normal and key.normal ~= '' then + 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() + -- If in insert mode and menu visible, use original key + if vim.fn.pumvisible() == 1 then + 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 + end, { buffer = bufnr, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') }) + end +end + +--- Updates the selection based on previous window +---@param config CopilotChat.config.shared +function M.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 = { + bufnr = vim.api.nvim_win_get_buf(prev_winnr), + winnr = prev_winnr, + } end - 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, config) + highlight_selection(false, config) end +--- Resolve the prompts from the prompt. ---@param prompt string ---@param config CopilotChat.config.shared ---@return string, CopilotChat.config -local function resolve_prompts(prompt, config) +function M.resolve_prompts(prompt, config) local prompts_to_use = M.prompts() local depth = 0 local MAX_DEPTH = 10 @@ -226,10 +221,11 @@ local function resolve_prompts(prompt, config) return resolve(prompt, config) end +--- Resolve the embeddings from the prompt. ---@param prompt string ---@param config CopilotChat.config.shared ---@return table, string -local function resolve_embeddings(prompt, config) +function M.resolve_embeddings(prompt, config) local contexts = {} local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') @@ -278,7 +274,10 @@ local function resolve_embeddings(prompt, config) return embeddings:values(), prompt end -local function resolve_agent(prompt, config) +--- Resolve the agent from the prompt. +---@param prompt string +---@param config CopilotChat.config.shared +function M.resolve_agent(prompt, config) local agents = vim.tbl_keys(state.client:list_agents()) local selected_agent = config.agent prompt = prompt:gsub('@' .. WORD, function(match) @@ -292,7 +291,10 @@ local function resolve_agent(prompt, config) return selected_agent, prompt end -local function resolve_model(prompt, config) +--- Resolve the model from the prompt. +---@param prompt string +---@param config CopilotChat.config.shared +function M.resolve_model(prompt, config) local models = vim.tbl_map(function(model) return model.id end, state.client:list_models()) @@ -309,145 +311,29 @@ local function resolve_model(prompt, config) return selected_model, prompt end ----@param start_of_chat boolean? -local function finish(start_of_chat) - if not start_of_chat then - state.chat:append('\n\n') - end - - state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - - -- Add default sticky prompts after reset - if start_of_chat then - if M.config.sticky then - local last_prompt = state.last_prompt or '' - - if type(M.config.sticky) == 'table' then - for _, sticky in ipairs(M.config.sticky) do - last_prompt = last_prompt .. '\n> ' .. sticky - end - else - last_prompt = last_prompt .. '\n> ' .. M.config.sticky - end - - state.last_prompt = last_prompt - end - end - - -- Reinsert sticky prompts from last prompt - if state.last_prompt then - local has_sticky = false - 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') - end - end - - state.chat:finish() -end - ----@param err string|table|nil ----@param append_newline boolean? -local function show_error(err, append_newline) - err = err or 'Unknown error' - - if type(err) == 'string' then - local message = err:match('^[^:]+:[^:]+:(.+)') or err - message = message:gsub('^%s*', '') - err = message - else - err = utils.make_string(err) - end - - if append_newline then - state.chat:append('\n') - end - - state.chat:append(M.config.error_header .. '\n```error\n' .. err .. '\n```') - finish() -end - ---- Map a key to a function. ----@param name string ----@param bufnr number ----@param fn function -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, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') } - ) - end - if key.insert and key.insert ~= '' then - vim.keymap.set('i', key.insert, function() - -- If in insert mode and menu visible, use original key - if vim.fn.pumvisible() == 1 then - 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 - end, { buffer = bufnr, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') }) - end -end - ---- Get the info for a key. ----@param name string ----@param surround string|nil ----@return string -local function key_to_info(name, surround) - local key = M.config.mappings[name] - if not key then - return '' - end - - if not surround then - surround = '' - end - - local out = '' - if key.normal and key.normal ~= '' then - out = out .. surround .. key.normal .. surround - end - if key.insert and key.insert ~= '' and key.insert ~= key.normal then - if out ~= '' then - out = out .. ' or ' - end - out = out .. surround .. key.insert .. surround .. ' in insert mode' - end - - if out == '' then - return out - end - - out = out .. ' to ' .. name:gsub('_', ' ') +--- Get the selection from the source buffer. +---@param config CopilotChat.config.shared +---@return CopilotChat.select.selection? +function M.get_selection(config) + local bufnr = state.source and state.source.bufnr + local winnr = state.source and state.source.winnr - if key.detail and key.detail ~= '' then - out = out .. '. ' .. key.detail + if + config + and config.selection + and utils.buf_valid(bufnr) + and winnr + and vim.api.nvim_win_is_valid(winnr) + then + return config.selection(state.source) end - return out + return nil end -local function trigger_complete(with_context) +--- Trigger the completion for the chat window. +---@param with_context boolean? +function M.trigger_complete(with_context) local info = M.complete_info() local bufnr = vim.api.nvim_get_current_buf() local line = vim.api.nvim_get_current_line() @@ -738,7 +624,7 @@ function M.ask(prompt, config) end -- Resolve prompt references - local prompt, config = resolve_prompts(prompt, config) + local prompt, config = M.resolve_prompts(prompt, config) local system_prompt = config.system_prompt -- Remove sticky prefix @@ -750,12 +636,12 @@ function M.ask(prompt, config) )) -- Retrieve the selection - local selection = get_selection(config) + local selection = M.get_selection(config) local ok, err = pcall(async.run, function() - local embeddings, prompt = resolve_embeddings(prompt, config) - local selected_agent, prompt = resolve_agent(prompt, config) - local selected_model, prompt = resolve_model(prompt, config) + local embeddings, prompt = M.resolve_embeddings(prompt, config) + local selected_agent, prompt = M.resolve_agent(prompt, config) + local selected_model, prompt = M.resolve_model(prompt, config) local has_output = false local query_ok, filtered_embeddings = @@ -952,7 +838,7 @@ function M.setup(config) M.ask('/Commit') end, { force = true }) - M.config = vim.tbl_deep_extend('force', default_config, config or {}) + M.config = vim.tbl_deep_extend('force', require('CopilotChat.config'), config or {}) -- Save proxy and insecure settings utils.curl_store_args({ @@ -963,7 +849,7 @@ function M.setup(config) if state.client then state.client:stop() end - state.client = Client(M.config.providers) + state.client = require('CopilotChat.client')(M.config.providers) if M.config.debug then M.log_level('debug') @@ -985,12 +871,7 @@ function M.setup(config) { link = '@punctuation.special.markdown', default = true } ) - 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 - + local overlay_help = utils.key_to_info('close', M.config.mappings.close) if state.overlay then state.overlay:delete() end @@ -1007,19 +888,11 @@ function M.setup(config) if state.diff then state.diff:delete() end - state.diff = require('CopilotChat.ui.diff')( - M.config.mappings.show_diff.full_diff, - diff_help, - function(bufnr) - map_key('close', bufnr, function() - state.diff:restore(state.chat.winnr, state.chat.bufnr) - end) - - map_key('accept_diff', bufnr, function() - apply_diff(state.diff:get_diff(), state.chat.config) - end) - end - ) + state.diff = require('CopilotChat.ui.diff')(overlay_help, function(bufnr) + map_key('close', bufnr, function() + state.diff:restore(state.chat.winnr, state.chat.bufnr) + end) + end) if state.chat then state.chat:close(state.source and state.source.bufnr or nil) @@ -1029,316 +902,11 @@ function M.setup(config) M.config.question_header, M.config.answer_header, M.config.separator, - key_to_info('show_help'), + utils.key_to_info('show_help', M.config.mappings.show_help), function(bufnr) - 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' - 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' - - 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] - 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 - if name ~= 'close' then - local info = key_to_info(name, '`') - if info ~= '' then - chat_help = chat_help .. info .. '\n' - end - end - end - state.overlay:show(chat_help, state.chat.winnr, 'markdown') - end) - - map_key('reset', bufnr, M.reset) - map_key('close', bufnr, M.close) - map_key('complete', bufnr, function() - trigger_complete(true) - end) - - map_key('submit_prompt', bufnr, function() - local section = state.chat:get_closest_section() - if not section or section.answer then - return - end - - M.ask(section.content) - end) - - map_key('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 - 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, {}) - - if vim.startswith(current_line, '> ') then - return - end - - 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 - elseif i >= 2 then - break - end - - 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('accept_diff', bufnr, function() - apply_diff(get_diff(state.chat.config), state.chat.config) - end) - - map_key('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(state.chat.config) - if not diff then - return - end - - local diff_bufnr = diff.bufnr - - -- If buffer is not found, try to load it - if not diff_bufnr then - 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, - diff.start_line, - diff.end_line, - state.chat.config - ) - end) - - map_key('quickfix_answers', bufnr, function() - local items = {} - for i, section in ipairs(state.chat.sections) do - if section.answer then - local prev_section = state.chat.sections[i - 1] - local text = '' - if prev_section then - text = vim.trim( - table.concat( - vim.api.nvim_buf_get_lines( - bufnr, - prev_section.start_line - 1, - prev_section.end_line, - false - ), - ' ' - ) - ) - end - - table.insert(items, { - bufnr = bufnr, - lnum = section.start_line, - end_lnum = section.end_line, - text = text, - }) - end - end - - vim.fn.setqflist(items) - vim.cmd('copen') - end) - - map_key('quickfix_diffs', bufnr, function() - local selection = get_selection(state.chat.config) - local items = {} - - 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 - - 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 - - vim.fn.setqflist(items) - vim.cmd('copen') - end) - - map_key('yank_diff', bufnr, function() - local diff = get_diff(state.chat.config) - if not diff then - return - end - - vim.fn.setreg(M.config.mappings.yank_diff.register, diff.change) - end) - - map_key('show_diff', bufnr, function() - local diff = get_diff(state.chat.config) - if not diff then - return - end - - state.diff:show(diff, state.chat.winnr) - end) - - map_key('show_info', bufnr, function() - local section = state.chat:get_closest_section() - if not section or section.answer then - return - end - - local lines = {} - 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) - 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_context', bufnr, function() - local section = state.chat:get_closest_section() - if not section or section.answer then - return - end - - local lines = {} - - 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 = {} - if section and not section.answer then - embeddings = resolve_embeddings(section.content, state.chat.config) - end - - for _, embedding in ipairs(embeddings) do - 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 - - 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(table.concat(lines, '\n')) .. '\n', - state.chat.winnr, - 'markdown' - ) - end) - end) + for name, _ in pairs(M.config.mappings) do + map_key(name, bufnr) + end vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { buffer = bufnr, @@ -1346,7 +914,7 @@ function M.setup(config) local is_enter = ev.event == 'BufEnter' if is_enter then - update_selection(state.chat.config) + M.update_selection(state.chat.config) else highlight_selection(true, state.chat.config) end @@ -1374,7 +942,7 @@ function M.setup(config) local char = line:sub(col, col) if vim.tbl_contains(M.complete_info().triggers, char) then - utils.debounce('complete', trigger_complete, 100) + utils.debounce('complete', M.trigger_complete, 100) end end, }) diff --git a/lua/CopilotChat/ui/diff.lua b/lua/CopilotChat/ui/diff.lua index be651183..25afbc4a 100644 --- a/lua/CopilotChat/ui/diff.lua +++ b/lua/CopilotChat/ui/diff.lua @@ -15,8 +15,7 @@ local class = utils.class ---@field hl_ns number ---@field diff CopilotChat.ui.Diff.Diff? ---@field augroup number ----@field full_diff boolean -local Diff = class(function(self, full_diff, help, on_buf_create) +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) }) @@ -24,18 +23,18 @@ local Diff = class(function(self, full_diff, help, on_buf_create) vim.api.nvim_set_hl(self.hl_ns, '@diff.delta', { bg = utils.blend_color('DiffChange', 20) }) self.augroup = vim.api.nvim_create_augroup('CopilotChatDiff', { clear = true }) - self.full_diff = full_diff self.diff = nil end, Overlay) ---@param diff CopilotChat.ui.Diff.Diff ---@param winnr number -function Diff:show(diff, winnr) +---@param full_diff boolean +function Diff:show(diff, winnr, full_diff) self.diff = diff self:validate() vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) - if not self.full_diff then + if not full_diff then -- Create unified diff view Overlay.show( self, @@ -102,9 +101,7 @@ end ---@param winnr number ---@param bufnr number function Diff:restore(winnr, bufnr) - if self.full_diff then - vim.cmd('diffoff') - end + vim.cmd('diffoff') Overlay.restore(self, winnr, bufnr) vim.api.nvim_win_set_hl_ns(winnr, 0) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index dfca98f6..a9f72f74 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -414,4 +414,37 @@ M.system = async.wrap(function(cmd, callback) vim.system(cmd, { text = true }, callback) end, 2) +--- Get the info for a key. +---@param name string +---@param surround string|nil +---@return string +function M.key_to_info(name, key, surround) + if not surround then + surround = '' + end + + local out = '' + if key.normal and key.normal ~= '' then + out = out .. surround .. key.normal .. surround + end + if key.insert and key.insert ~= '' and key.insert ~= key.normal then + if out ~= '' then + out = out .. ' or ' + end + out = out .. surround .. key.insert .. surround .. ' in insert mode' + end + + if out == '' then + return out + end + + out = out .. ' to ' .. name:gsub('_', ' ') + + if key.detail and key.detail ~= '' then + out = out .. '. ' .. key.detail + end + + return out +end + return M From 2f2cf00670c04a03770a60d43df9809c4293a39e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 15:16:49 +0000 Subject: [PATCH 060/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 85 +++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index c844af3c..7bbbbf21 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -410,63 +410,49 @@ has following fields: - `get_models?(headers: table): table` - Optional function that returns list of available models - `get_agents?(headers: table): table` - Optional function that returns list of available agents -Example custom provider: +Here is how you implement ollama provider for example: >lua { providers = { - my_provider = { - -- Required fields - get_token = function() - return "my-token", os.time() + 3600 -- Token valid for 1 hour - end, - get_headers = function(token, sessionid, machineid) + ollama = { + get_headers = function() return { - ["authorization"] = "Bearer " .. token, - ["content-type"] = "application/json", + ['Content-Type'] = 'application/json', } end, - get_url = function(opts) - if opts.agent then - return "https://api.custom.com/agents/" .. opts.agent + + get_models = function() + local response = cutils.curl_get('http://localhost:11434/api/tags') + + if not response or response.status ~= 200 then + error('Failed to fetch models: ' .. tostring(response and response.status)) + end + + local models = {} + for _, model in ipairs(vim.json.decode(response.body)['models']) do + table.insert(models, { + id = model.name, + name = model.name, + version = "latest", + tokenizer = "o200k_base", + }) end - return "https://api.custom.com/chat" + + return models end, - prepare_input = function(inputs, opts, model) + + prepare_input = function(inputs, opts) return { - messages = inputs, - temperature = opts.temperature, model = opts.model, - stream = true + messages = inputs, + stream = true, } end, - -- Optional fields - disabled = false, - embeddings = "copilot_embeddings", -- Use copilot for embeddings - get_models = function(headers) - -- Return list of available models - return { - { - id = "gpt-4", - name = "GPT-4", - version = "1.0", - tokenizer = "gpt2", - max_prompt_tokens = 8000, - max_output_tokens = 2000, - } - } + get_url = function() + return 'http://localhost:11434/api/chat' end, - get_agents = function(headers) - -- Return list of available agents - return { - { - id = "agent1", - name = "My Agent", - description = "Custom agent" - } - } - end } } } @@ -618,47 +604,39 @@ Also see here : separator = '───', -- Separator to use in chat -- default providers + -- see config/providers.lua for implementation providers = { copilot = { - -- see config.lua for implementation }, github_models = { - -- see config.lua for implementation }, copilot_embeddings = { - -- see config.lua for implementation }, } -- default contexts + -- see config/contexts.lua for implementation contexts = { buffer = { - -- see config.lua for implementation }, buffers = { - -- see config.lua for implementation }, file = { - -- see config.lua for implementation }, files = { - -- see config.lua for implementation }, git = { - -- see config.lua for implementation }, url = { - -- see config.lua for implementation }, register = { - -- see config.lua for implementation }, quickfix = { - -- see config.lua for implementation }, }, -- default prompts + -- see config/prompts.lua for implementation prompts = { Explain = { prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', @@ -684,6 +662,7 @@ Also see here : }, -- default mappings + -- see config/mappings.lua for implementation mappings = { complete = { insert = '', From 1b476cd2d48cbfab890c416f8b8286bc1ded2f2b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 16:25:04 +0100 Subject: [PATCH 061/728] Add missing space to mappings Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 5bae761a..b45f73a6 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -206,6 +206,7 @@ return { vim.api.nvim_win_set_cursor(0, cursor) end, }, + accept_diff = { normal = '', insert = '', From 4e911ecf03857e60c9ae04c233f8520fd3182444 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 20:03:50 +0100 Subject: [PATCH 062/728] docs: reorganize and improve README.md readability (#790) * docs: reorganize and improve README.md readability Reorganize README.md sections for better readability and navigation: - Convert list sections to tables for clearer information presentation - Improve formatting and structure of configuration examples - Add clear section headers and better organization of content - Update API reference section with cleaner formatting - Consolidate examples and tips into a single section - Fix minor typos and inconsistencies throughout the document * [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 | 612 ++++++++++++++++++++++++------------------------------ 1 file changed, 275 insertions(+), 337 deletions(-) diff --git a/README.md b/README.md index 1c3a61c4..71872a5d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 # Installation -## [Lazy.nvim](https://github.com/folke/lazy.nvim) +## [lazy.nvim](https://github.com/folke/lazy.nvim) ```lua return { @@ -58,7 +58,7 @@ return { 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) +## [vim-plug](https://github.com/junegunn/vim-plug) Similar to the lazy setup, you can use the following configuration: @@ -100,39 +100,47 @@ require("CopilotChat").setup { See [@deathbeam](https://github.com/deathbeam) for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua) -# Usage +# Features ## Commands -- `:CopilotChat ?` - Open chat window with optional input -- `:CopilotChatOpen` - Open chat window -- `:CopilotChatClose` - Close chat window -- `:CopilotChatToggle` - Toggle chat window -- `:CopilotChatStop` - Stop current copilot output -- `:CopilotChatReset` - Reset chat window -- `: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. -- `: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 - -- `` - Trigger completion menu for special tokens or accept current completion (see help) -- `q`/`` - Close the chat window -- `` - Reset and clear the chat window -- ``/`` - Submit the current prompt -- `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) -- `gqa` - Add all answers from chat to quickfix list -- `gqd` - Add all diffs from chat to quickfix list -- `gy` - Yank nearest diff to register (defaults to `"`). Use `mappings.yank_diff.register` config option to set register -- `gd` - Show diff between source and nearest diff. Use `mappings.show_diff.full_diff` boolean config option to show full diff instead of unified diff -- `gi` - Show info about current chat (model, agent, system prompt) -- `gc` - Show current chat context -- `gh` - Show help message +Commands are used to control the chat interface: + +| Command | Description | +| -------------------------- | ----------------------------- | +| `:CopilotChat ?` | Open chat with optional input | +| `:CopilotChatOpen` | Open chat window | +| `:CopilotChatClose` | Close chat window | +| `:CopilotChatToggle` | Toggle chat window | +| `:CopilotChatStop` | Stop current output | +| `:CopilotChatReset` | Reset chat window | +| `:CopilotChatSave ?` | Save chat history | +| `:CopilotChatLoad ?` | Load chat history | +| `:CopilotChatDebugInfo` | Show debug info | +| `:CopilotChatModels` | View/select available models | +| `:CopilotChatAgents` | View/select available agents | +| `:CopilotChat` | Use specific prompt template | + +## Key Mappings + +Default mappings in the chat interface: + +| Insert | Normal | Action | +| ------- | ------- | -------------------------------------------------- | +| `` | `` | Trigger/accept completion menu for tokens | +| `` | `q` | Close the chat window | +| `` | `` | Reset and clear the chat window | +| `` | `` | Submit the current prompt | +| - | `gr` | Toggle sticky prompt for line under cursor | +| `` | `` | Accept nearest diff (best with `COPILOT_GENERATE`) | +| - | `gj` | Jump to section of nearest diff | +| - | `gqa` | Add all answers from chat to quickfix list | +| - | `gqd` | Add all diffs from chat to quickfix list | +| - | `gy` | Yank nearest diff to register | +| - | `gd` | Show diff between source and nearest diff | +| - | `gi` | Show info about current chat | +| - | `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: @@ -158,18 +166,21 @@ For example, to change the submit prompt mapping or show_diff full diff option: ## 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: +### Predefined Prompts -- `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 -- `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 +Predefined prompt templates for common tasks. Reference them with `/PromptName` in chat or use `:CopilotChat`: -You can define custom prompts like this (only `prompt` is required): +| Prompt | Description | +| ---------- | ------------------------------------------------ | +| `Explain` | Write an explanation for the selected code | +| `Review` | Review the selected code | +| `Fix` | Rewrite the code with bug fixes | +| `Optimize` | Optimize code for performance and readability | +| `Docs` | Add documentation comments to the code | +| `Tests` | Generate tests for the code | +| `Commit` | Write commit message using commitizen convention | + +Define your own prompts in the configuration: ```lua { @@ -184,17 +195,18 @@ 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: +System prompts define the AI model's behavior. Reference them with `/PROMPT_NAME` in chat: -- `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 +| Prompt | Description | +| ---------------------- | ------------------------------------------ | +| `COPILOT_INSTRUCTIONS` | Base GitHub Copilot instructions | +| `COPILOT_EXPLAIN` | Adds coding tutor behavior | +| `COPILOT_REVIEW` | Adds code review behavior with diagnostics | +| `COPILOT_GENERATE` | Adds code generation behavior and rules | -You can define custom system prompts like this (works same as `prompts` so you can combine prompt and system prompt definitions): +Define your own system prompts in the configuration (similar to `prompts`): ```lua { @@ -206,23 +218,22 @@ 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. -This is useful for preserving stuff like context and agent selection (see below). -Example usage: +Sticky prompts persist across chat sessions. They're useful for maintaining context or agent selection. They work as follows: -```markdown -> #files +1. Prefix text with `> ` using markdown blockquote syntax +2. The prompt will be copied at the start of every new chat prompt +3. Edit sticky prompts freely while maintaining the `> ` prefix -List all files in the workspace -``` +Examples: ```markdown -> @models Using Mistral-small +> #files +> List all files in the workspace -What is 1 + 11 +> @models Using Mistral-small +> What is 1 + 11 ``` You can also set default sticky prompts in the configuration: @@ -236,51 +247,60 @@ You can also set default sticky prompts in the configuration: } ``` -## Models +## Models and Agents + +### 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. -For list of models supported by Copilot Chat see [here](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat). -This plugin also supports Github Marketplace Models. These have fairly low limits but are useful for experimentation. For more information see [here](https://github.com/marketplace/models). +You can control which AI model to use in three ways: -## Agents +1. List available models with `:CopilotChatModels` +2. Set model in prompt with `$model_name` +3. Configure default model via `model` config key -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. -Default "noop" agent is `copilot`. +For supported models, see: -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) +- [Copilot Chat Models](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat) +- [GitHub Marketplace Models](https://github.com/marketplace/models) (experimental, limited usage) + +### Agents + +Agents determine the AI assistant's capabilities. Control agents in three ways: + +1. List available agents with `:CopilotChatAgents` +2. Set agent in prompt with `@agent_name` +3. Configure default agent via `agent` config key + +The default "noop" agent is `none`. For more information: + +- [Extension Agents Documentation](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) +- [Available Agents](https://github.com/marketplace?type=apps&copilot_app=true) ## 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. -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. Supports input (default current). - - `buffer:` - Includes buffer with specified number in chat context. -- `buffers` - Includes all buffers in chat context. Supports input (default listed). - - `buffers:listed` - Includes only listed buffers in chat context. - - `buffers:all` - Includes all buffers in chat context. -- `file` - Includes content of provided file in chat context. Supports input. - - `file:` - Includes content of specified file in chat context. -- `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). - - `files:list` - Only lists file names. - - `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, also accepts commit number). - - `git:unstaged` - Includes unstaged changes in chat context. - - `git:staged` - Includes staged changes in chat context. - - `git:` - Includes changes from specified commit in chat context. -- `url` - Includes content of provided URL in chat context. Supports input. - - `url:` - Includes content of specified URL in chat context. -- `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). - - `register:` - Includes contents of specified register in chat context. -- `quickfix` - Includes quickfix list file contents in chat context. - -You can define custom contexts like this: +Contexts provide additional information to the chat. Add context using `#context_name[:input]` syntax: + +| Context | Input Support | Description | +| ---------- | ------------- | ----------------------------------- | +| `buffer` | ✓ (number) | Current or specified buffer content | +| `buffers` | ✓ (type) | All buffers content (listed/all) | +| `file` | ✓ (path) | Content of specified file | +| `files` | ✓ (mode) | Workspace files (list/full content) | +| `git` | ✓ (ref) | Git diff (unstaged/staged/commit) | +| `url` | ✓ (url) | Content from URL | +| `register` | ✓ (name) | Content of vim register | +| `quickfix` | - | Quickfix list file contents | + +Examples: + +```markdown +> #buffer +> #buffer:2 +> #files:list +> #git:staged +> #url:https://example.com +``` + +Define your own contexts in the configuration with input handling and resolution: ```lua { @@ -292,17 +312,9 @@ You can define custom contexts like this: }, 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, + content = input .. ' birthday info', filename = input .. '_birthday', filetype = 'text', } @@ -313,29 +325,24 @@ 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). -Selections are configurable either by default or by prompt. -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: +Selections determine the source content for chat interactions. Configure them globally or per-prompt. + +Available selections are located in `local select = require("CopilotChat.select")`: -- `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. +| Selection | Description | +| --------- | ------------------------------------------------------ | +| `visual` | Current visual selection | +| `buffer` | Current buffer content | +| `line` | Current line content | +| `unnamed` | Unnamed register (last deleted/changed/yanked content) | -You can chain multiple selections like this: +You can set a default selection in the configuration: ```lua { + -- Default uses visual selection or falls back to buffer selection = function(source) return select.visual(source) or select.buffer(source) end @@ -344,37 +351,31 @@ You can chain multiple selections like this: ## Providers -Providers are modules that implement integration with different AI providers. Built-in providers are: +Providers are modules that implement integration with different AI providers. + +### Built-in Providers - `copilot` - Default GitHub Copilot provider used for chat and embeddings - `github_models` - Provider for GitHub Marketplace models -You can define custom providers by adding them to `providers` config. Provider has following fields: - -- `disabled?: boolean` - Optional boolean to disable provider -- `embeddings?: string` - Optional string pointing to provider to use for embeddings -- `get_token(): string, number?` - Function that returns authentication token and optional expiry timestamp -- `get_headers(token: string, sessionid: string, machineid: string): table` - Function that returns headers for API requests -- `get_url(opts: table): string` - Function that returns API endpoint URL for given operation -- `prepare_input(inputs: table, opts: table, model: table): table` - Function that prepares request body -- `get_models?(headers: table): table` - Optional function that returns list of available models -- `get_agents?(headers: table): table` - Optional function that returns list of available agents +### Ollama Example -Here is how you implement [ollama](https://ollama.com/) provider for example: +Here's how to implement an [ollama](https://ollama.com/) provider: ```lua { providers = { ollama = { + -- Required: Headers for API requests get_headers = function() return { ['Content-Type'] = 'application/json', } end, + -- Optional: Get available models get_models = function() local response = cutils.curl_get('http://localhost:11434/api/tags') - if not response or response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response and response.status)) end @@ -388,10 +389,10 @@ Here is how you implement [ollama](https://ollama.com/) provider for example: tokenizer = "o200k_base", }) end - return models end, + -- Required: Prepare request body prepare_input = function(inputs, opts) return { model = opts.model, @@ -400,6 +401,7 @@ Here is how you implement [ollama](https://ollama.com/) provider for example: } end, + -- Required: Get API endpoint URL get_url = function() return 'http://localhost:11434/api/chat' end, @@ -408,86 +410,43 @@ Here is how you implement [ollama](https://ollama.com/) provider for example: } ``` -## API - -```lua -local chat = require("CopilotChat") - --- Open chat window -chat.open() - --- Open chat window with custom options -chat.open({ - window = { - layout = 'float', - title = 'My Title', - }, -}) - --- Close chat window -chat.close() - --- Toggle chat window -chat.toggle() - --- Toggle chat window with custom options -chat.toggle({ - window = { - layout = 'float', - title = 'My Title', - }, -}) - --- Reset chat window -chat.reset() - --- Ask a question -chat.ask("Explain how it works.") +#### Provider Interface --- Ask a question with custom options -chat.ask("Explain how it works.", { - selection = require("CopilotChat.select").buffer, -}) +Custom providers can implement these methods: --- Ask a question and provide custom contexts -chat.ask("Explain how it works.", { - context = { 'buffers', 'files', 'register:+' }, -}) +```lua +{ + -- Optional: Disable provider + disabled?: boolean, --- Ask a question and do something with the response -chat.ask("Show me something interesting", { - callback = function(response) - print("Response:", response) - end, -}) + -- Optional: Provider to use for embeddings + embeddings?: string, --- Get all available prompts (can be used for integrations like fzf/telescope) -local prompts = chat.prompts() + -- Optional: Get authentication token + get_token(): string, number?, --- Get last copilot response (also can be used for integrations and custom keymaps) -local response = chat.response() + -- Required: Get request headers + get_headers(token: string): table, --- Retrieve current chat config -local config = chat.config -print(config.model) + -- Required: Get API endpoint URL + get_url(opts: table): string, --- Pick a prompt using vim.ui.select -local actions = require("CopilotChat.actions") + -- Required: Prepare request body + prepare_input(inputs: table, opts: table, model: table): table, --- Pick prompt actions -actions.pick(actions.prompt_actions({ - selection = require("CopilotChat.select").visual, -})) + -- Optional: Get available models + get_models?(headers: table): table, --- Programmatically set log level -chat.log_level("debug") + -- Optional: Get available agents + get_agents?(headers: table): table, +} ``` # Configuration -## Default configuration +## Default Configuration -Also see [here](/lua/CopilotChat/config.lua): +Below are all available configuration options with their default values: ```lua { @@ -663,210 +622,189 @@ 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-chat`, `copilot-diff`, `copilot-overlay`: +You can set local options for plugin buffers (`copilot-chat`, `copilot-diff`, `copilot-overlay`): ```lua vim.api.nvim_create_autocmd('BufEnter', { pattern = 'copilot-*', callback = function() + -- Set buffer-local options vim.opt_local.relativenumber = true - -- C-p to print last response + -- Add buffer-local mappings vim.keymap.set('n', '', function() print(require("CopilotChat").response()) - end, { buffer = true, remap = true }) + end, { buffer = true }) end }) ``` -# Tips - -
-Quick chat with your buffer - -To chat with Copilot using the entire content of the buffer, you can add the following configuration to your keymap: +# API Reference ```lua --- lazy.nvim keys - - -- Quick chat with Copilot - { - "ccq", - function() - local input = vim.fn.input("Quick Chat: ") - if input ~= "" then - require("CopilotChat").ask(input, { selection = require("CopilotChat.select").buffer }) - end - end, - desc = "CopilotChat - Quick chat", - } -``` +local chat = require("CopilotChat") -[![chat-with-buffer](https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif)](https://gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0) +-- Window Management +chat.open() -- Open chat window +chat.open({ -- Open with custom options + window = { + layout = 'float', + title = 'Custom Chat', + }, +}) +chat.close() -- Close chat window +chat.toggle() -- Toggle chat window +chat.reset() -- Reset chat window -
+-- Chat Interaction +chat.ask("Explain this code.") -- Basic question +chat.ask("Explain this code.", { + selection = require("CopilotChat.select").buffer, + context = { 'buffers', 'files' }, + callback = function(response) + print("Response:", response) + end, +}) -
-Inline chat +-- Utilities +chat.prompts() -- Get all available prompts +chat.response() -- Get last response +chat.log_level("debug") -- Set log level -Change the window layout to `float` and position relative to cursor to make the window look like inline chat. -This will allow you to chat with Copilot without opening a new window. -```lua --- lazy.nvim opts +-- Actions +local actions = require("CopilotChat.actions") +actions.pick(actions.prompt_actions({ + selection = require("CopilotChat.select").visual, +})) - { +-- Update config +chat.setup({ + model = 'gpt-4', window = { - layout = 'float', - relative = 'cursor', - width = 1, - height = 0.4, - row = 1 + layout = 'float' } - } +}) ``` -![inline-chat](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c) - -
+# Tips and Examples -
-Telescope integration +## Quick Chat with Buffer -Requires [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) plugin to be installed. +Set up a quick chat command that uses the entire buffer content: ```lua --- lazy.nvim keys - - -- Show prompts actions with telescope - { - "ccp", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.telescope").pick(actions.prompt_actions()) - end, - desc = "CopilotChat - Prompt actions", - }, +-- Quick chat keybinding +vim.keymap.set('n', 'ccq', function() + local input = vim.fn.input("Quick Chat: ") + if input ~= "" then + require("CopilotChat").ask(input, { + selection = require("CopilotChat.select").buffer + }) + end +end, { desc = "CopilotChat - Quick chat" }) ``` -![telescope-integration](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b) +## Inline Chat Window -
- -
-fzf-lua integration - -Requires [fzf-lua](https://github.com/ibhagwan/fzf-lua) plugin to be installed. +Configure the chat window to appear inline near the cursor: ```lua --- lazy.nvim keys - - -- Show prompts actions with fzf-lua - { - "ccp", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.fzflua").pick(actions.prompt_actions()) - end, - desc = "CopilotChat - Prompt actions", - }, +require("CopilotChat").setup({ + window = { + layout = 'float', + relative = 'cursor', + width = 1, + height = 0.4, + row = 1 + } +}) ``` -![fzf-lua-integration](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747) +## Telescope Integration -
+Requires [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim): -
-snacks.nvim integration +```lua +vim.keymap.set('n', 'ccp', function() + local actions = require("CopilotChat.actions") + require("CopilotChat.integrations.telescope").pick(actions.prompt_actions()) +end, { desc = "CopilotChat - Prompt actions" }) +``` -Requires [snacks.nvim](https://github.com/folke/snacks.nvim) plugin to be installed and the [picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) to be configured. +## Quick Search with Perplexity -```lua --- lazy.nvim keys +Requires [PerplexityAI Agent](https://github.com/marketplace/perplexityai): - -- Show prompts actions with snacks.nvim picker - { - "ccp", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.snacks").pick(actions.prompt_actions()) - end, - desc = "CopilotChat - Prompt actions", - }, +```lua +vim.keymap.set({ 'n', 'v' }, 'ccs', function() + local input = vim.fn.input("Perplexity: ") + if input ~= "" then + require("CopilotChat").ask(input, { + agent = "perplexityai", + selection = false, + }) + end +end, { desc = "CopilotChat - Perplexity Search" }) ``` -
+## Markdown Rendering -
-render-markdown integration - -Requires [render-markdown](https://github.com/MeanderingProgrammer/render-markdown.nvim) plugin to be installed. +Use [render-markdown.nvim](https://github.com/MeanderingProgrammer/render-markdown.nvim) for better chat display: ```lua --- Registers copilot-chat filetype for markdown rendering +-- Register copilot-chat filetype require('render-markdown').setup({ file_types = { 'markdown', 'copilot-chat' }, }) --- You might also want to disable default header highlighting for copilot chat when doing this and set error header style and separator +-- Adjust chat display settings require('CopilotChat').setup({ highlight_headers = false, separator = '---', error_header = '> [!ERROR] Error', - -- rest of your config }) ``` -![render-markdown-integration](https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8) - -
+# Development -
-Ask a quick question with Perplexity +## Setup -Requires [PerplexityAI Agent](https://github.com/marketplace/perplexityai) to be added to [GitHub](https://github.com/) account. +To set up the environment: -This sets the `selection = false` to be able to ask generic questions unrelated to current code. +1. Clone the repository: -```lua --- lazy.nvim keys - - -- Ask the Perplexity agent a quick question - { - "ccs", - function() - local input = vim.fn.input("Perplexity: ") - if input ~= "" then - require("CopilotChat").ask(input, { - agent = "perplexityai", - selection = false, - }) - end - end, - desc = "CopilotChat - Perplexity Search", - mode = { "n", "v" }, - }, +```bash +git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim +cd CopilotChat.nvim ``` -
- -# Development +2. Install development dependencies: -## Installing Pre-commit Tool +```bash +# Install pre-commit hooks +make install-pre-commit +``` -For development, you can use the provided Makefile command to install the pre-commit tool: +To run tests: ```bash -make install-pre-commit +make test ``` -This will install the pre-commit tool and the pre-commit hooks. +## Contributing -# Contributors +1. Fork the repository +2. Create your feature branch +3. Make your changes +4. Run tests and lint checks +5. Submit a pull request + +See [CONTRIBUTING.md](/CONTRIBUTING.md) for detailed guidelines. -If you want to contribute to this project, please read the [CONTRIBUTING.md](/CONTRIBUTING.md) file. +# Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -948,6 +886,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome! -## Stargazers over time +# Stargazers [![Stargazers over time](https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg)](https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim) From 82c1b7333fe92c941efb44b5f58df2da06276d3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 19:04:10 +0000 Subject: [PATCH 063/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 697 +++++++++++++++++++++----------------------- 1 file changed, 334 insertions(+), 363 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 7bbbbf21..c5ded71d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -5,30 +5,33 @@ Table of Contents *CopilotChat-table-of-contents* 1. Requirements |CopilotChat-requirements| 2. Installation |CopilotChat-installation| - - Lazy.nvim |CopilotChat-lazy.nvim| - - Vim-Plug |CopilotChat-vim-plug| + - lazy.nvim |CopilotChat-lazy.nvim| + - vim-plug |CopilotChat-vim-plug| - Manual |CopilotChat-manual| -3. Usage |CopilotChat-usage| +3. Features |CopilotChat-features| - Commands |CopilotChat-commands| - - Chat Mappings |CopilotChat-chat-mappings| + - Key Mappings |CopilotChat-key-mappings| - Prompts |CopilotChat-prompts| - - System Prompts |CopilotChat-system-prompts| - - Sticky Prompts |CopilotChat-sticky-prompts| - - Models |CopilotChat-models| - - Agents |CopilotChat-agents| + - Models and Agents |CopilotChat-models-and-agents| - Contexts |CopilotChat-contexts| - Selections |CopilotChat-selections| - Providers |CopilotChat-providers| - - API |CopilotChat-api| 4. Configuration |CopilotChat-configuration| - - Default configuration |CopilotChat-default-configuration| - - Customizing buffers |CopilotChat-customizing-buffers| -5. Tips |CopilotChat-tips| -6. Development |CopilotChat-development| - - Installing Pre-commit Tool |CopilotChat-installing-pre-commit-tool| -7. Contributors |CopilotChat-contributors| - - Stargazers over time |CopilotChat-stargazers-over-time| -8. Links |CopilotChat-links| + - Default Configuration |CopilotChat-default-configuration| + - Customizing Buffers |CopilotChat-customizing-buffers| +5. API Reference |CopilotChat-api-reference| +6. Tips and Examples |CopilotChat-tips-and-examples| + - Quick Chat with Buffer |CopilotChat-quick-chat-with-buffer| + - Inline Chat Window |CopilotChat-inline-chat-window| + - Telescope Integration |CopilotChat-telescope-integration| + - Quick Search with Perplexity |CopilotChat-quick-search-with-perplexity| + - Markdown Rendering |CopilotChat-markdown-rendering| +7. Development |CopilotChat-development| + - Setup |CopilotChat-setup| + - Contributing |CopilotChat-contributing| +8. Contributors |CopilotChat-contributors| +9. Stargazers |CopilotChat-stargazers| +10. Links |CopilotChat-links| ============================================================================== @@ -125,42 +128,63 @@ See @deathbeam for configuration ============================================================================== -3. Usage *CopilotChat-usage* +3. Features *CopilotChat-features* COMMANDS *CopilotChat-commands* -- `:CopilotChat ?` - Open chat window with optional input -- `:CopilotChatOpen` - Open chat window -- `:CopilotChatClose` - Close chat window -- `:CopilotChatToggle` - Toggle chat window -- `:CopilotChatStop` - Stop current copilot output -- `:CopilotChatReset` - Reset chat window -- `: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. -- `: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 *CopilotChat-chat-mappings* - -- `` - Trigger completion menu for special tokens or accept current completion (see help) -- `q`/`` - Close the chat window -- `` - Reset and clear the chat window -- ``/`` - Submit the current prompt -- `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) -- `gqa` - Add all answers from chat to quickfix list -- `gqd` - Add all diffs from chat to quickfix list -- `gy` - Yank nearest diff to register (defaults to `"`). Use `mappings.yank_diff.register` config option to set register -- `gd` - Show diff between source and nearest diff. Use `mappings.show_diff.full_diff` boolean config option to show full diff instead of unified diff -- `gi` - Show info about current chat (model, agent, system prompt) -- `gc` - Show current chat context -- `gh` - Show help message +Commands are used to control the chat interface: + Command Description + -------------------------- ------------------------------- + :CopilotChat ? Open chat with optional input + :CopilotChatOpen Open chat window + :CopilotChatClose Close chat window + :CopilotChatToggle Toggle chat window + :CopilotChatStop Stop current output + :CopilotChatReset Reset chat window + :CopilotChatSave ? Save chat history + :CopilotChatLoad ? Load chat history + :CopilotChatDebugInfo Show debug info + :CopilotChatModels View/select available models + :CopilotChatAgents View/select available agents + :CopilotChat Use specific prompt template + +KEY MAPPINGS *CopilotChat-key-mappings* + +Default mappings in the chat interface: + + ------------------------------------------------------------------------- + Insert Normal Action + -------- -------- ------------------------------------------------------- + Trigger/accept completion menu for tokens + + q Close the chat window + + Reset and clear the chat window + + Submit the current prompt + + - gr Toggle sticky prompt for line under cursor + + Accept nearest diff (best with COPILOT_GENERATE) + + - gj Jump to section of nearest diff + + - gqa Add all answers from chat to quickfix list + + - gqd Add all diffs from chat to quickfix list + + - gy Yank nearest diff to register + + - gd Show diff between source and nearest diff + + - gi Show info about current chat + + - 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: @@ -187,19 +211,22 @@ For example, to change the submit prompt mapping or show_diff full diff option: 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`. -Default prompts are: -- `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 -- `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 +PREDEFINED PROMPTS ~ + +Predefined prompt templates for common tasks. Reference them with `/PromptName` +in chat or use `:CopilotChat`: -You can define custom prompts like this (only `prompt` is required): + Prompt Description + ---------- -------------------------------------------------- + Explain Write an explanation for the selected code + Review Review the selected code + Fix Rewrite the code with bug fixes + Optimize Optimize code for performance and readability + Docs Add documentation comments to the code + Tests Generate tests for the code + Commit Write commit message using commitizen convention +Define your own prompts in the configuration: >lua { @@ -215,18 +242,18 @@ You can define custom prompts like this (only `prompt` is required): < -SYSTEM PROMPTS *CopilotChat-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: +System prompts define the AI model’s behavior. Reference them with +`/PROMPT_NAME` in chat: -- `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 - -You can define custom system prompts like this (works same as `prompts` so you -can combine prompt and system prompt definitions): + Prompt Description + ---------------------- -------------------------------------------- + COPILOT_INSTRUCTIONS Base GitHub Copilot instructions + COPILOT_EXPLAIN Adds coding tutor behavior + COPILOT_REVIEW Adds code review behavior with diagnostics + COPILOT_GENERATE Adds code generation behavior and rules +Define your own system prompts in the configuration (similar to `prompts`): >lua { @@ -239,24 +266,23 @@ can combine prompt and system prompt definitions): < -STICKY PROMPTS *CopilotChat-sticky-prompts* +STICKY PROMPTS ~ + +Sticky prompts persist across chat sessions. They’re useful for maintaining +context or agent selection. They work as follows: -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: +1. Prefix text with `>` using markdown blockquote syntax +2. The prompt will be copied at the start of every new chat prompt +3. Edit sticky prompts freely while maintaining the `>` prefix + +Examples: >markdown > #files + > List all files in the workspace - List all files in the workspace -< - ->markdown > @models Using Mistral-small - - What is 1 + 11 + > What is 1 + 11 < You can also set default sticky prompts in the configuration: @@ -271,61 +297,65 @@ You can also set default sticky prompts in the configuration: < -MODELS *CopilotChat-models* +MODELS AND AGENTS *CopilotChat-models-and-agents* + + +MODELS ~ + +You can control which AI model to use in three ways: + +1. List available models with `:CopilotChatModels` +2. Set model in prompt with `$model_name` +3. Configure default model via `model` config key + +For supported models, see: -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. For list of models supported by Copilot Chat see here -. -This plugin also supports Github Marketplace Models. These have fairly low -limits but are useful for experimentation. For more information see here -. +- Copilot Chat Models +- GitHub Marketplace Models (experimental, limited usage) -AGENTS *CopilotChat-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. Default "noop" agent is `copilot`. +Agents determine the AI assistant’s capabilities. Control agents in three +ways: -For more information about extension agents, see here - -You can install more agents from here - +1. List available agents with `:CopilotChatAgents` +2. Set agent in prompt with `@agent_name` +3. Configure default agent via `agent` config key + +The default "noop" agent is `none`. For more information: + +- Extension Agents Documentation +- Available Agents 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 -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. Supports input (default current). - - `buffer:` - Includes buffer with specified number in chat context. -- `buffers` - Includes all buffers in chat context. Supports input (default listed). - - `buffers:listed` - Includes only listed buffers in chat context. - - `buffers:all` - Includes all buffers in chat context. -- `file` - Includes content of provided file in chat context. Supports input. - - `file:` - Includes content of specified file in chat context. -- `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). - - `files:list` - Only lists file names. - - `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, also accepts commit number). - - `git:unstaged` - Includes unstaged changes in chat context. - - `git:staged` - Includes staged changes in chat context. - - `git:` - Includes changes from specified commit in chat context. -- `url` - Includes content of provided URL in chat context. Supports input. - - `url:` - Includes content of specified URL in chat context. -- `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). - - `register:` - Includes contents of specified register in chat context. -- `quickfix` - Includes quickfix list file contents in chat context. - -You can define custom contexts like this: +Contexts provide additional information to the chat. Add context using +`#context_name[:input]` syntax: + + Context Input Support Description + ---------- --------------- ------------------------------------- + buffer ✓ (number) Current or specified buffer content + buffers ✓ (type) All buffers content (listed/all) + file ✓ (path) Content of specified file + files ✓ (mode) Workspace files (list/full content) + git ✓ (ref) Git diff (unstaged/staged/commit) + url ✓ (url) Content from URL + register ✓ (name) Content of vim register + quickfix - Quickfix list file contents +Examples: + +>markdown + > #buffer + > #buffer:2 + > #files:list + > #git:staged + > #url:https://example.com +< + +Define your own contexts in the configuration with input handling and +resolution: >lua { @@ -337,17 +367,9 @@ You can define custom contexts like this: }, 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, + content = input .. ' birthday info', filename = input .. '_birthday', filetype = 'text', } @@ -358,31 +380,26 @@ You can define custom contexts like this: } < ->markdown - > #birthday:user - - What is my birthday -< - 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. -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: +Selections determine the source content for chat interactions. Configure them +globally or per-prompt. -- `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. +Available selections are located in `local select = +require("CopilotChat.select")`: -You can chain multiple selections like this: + Selection Description + ----------- -------------------------------------------------------- + visual Current visual selection + buffer Current buffer content + line Current line content + unnamed Unnamed register (last deleted/changed/yanked content) +You can set a default selection in the configuration: >lua { + -- Default uses visual selection or falls back to buffer selection = function(source) return select.visual(source) or select.buffer(source) end @@ -393,38 +410,32 @@ You can chain multiple selections like this: PROVIDERS *CopilotChat-providers* Providers are modules that implement integration with different AI providers. -Built-in providers are: + + +BUILT-IN PROVIDERS ~ - `copilot` - Default GitHub Copilot provider used for chat and embeddings - `github_models` - Provider for GitHub Marketplace models -You can define custom providers by adding them to `providers` config. Provider -has following fields: -- `disabled?: boolean` - Optional boolean to disable provider -- `embeddings?: string` - Optional string pointing to provider to use for embeddings -- `get_token(): string, number?` - Function that returns authentication token and optional expiry timestamp -- `get_headers(token: string, sessionid: string, machineid: string): table` - Function that returns headers for API requests -- `get_url(opts: table): string` - Function that returns API endpoint URL for given operation -- `prepare_input(inputs: table, opts: table, model: table): table` - Function that prepares request body -- `get_models?(headers: table): table` - Optional function that returns list of available models -- `get_agents?(headers: table): table` - Optional function that returns list of available agents +OLLAMA EXAMPLE ~ -Here is how you implement ollama provider for example: +Here’s how to implement an ollama provider: >lua { providers = { ollama = { + -- Required: Headers for API requests get_headers = function() return { ['Content-Type'] = 'application/json', } end, + -- Optional: Get available models get_models = function() local response = cutils.curl_get('http://localhost:11434/api/tags') - if not response or response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response and response.status)) end @@ -438,10 +449,10 @@ Here is how you implement ollama provider for example: tokenizer = "o200k_base", }) end - return models end, + -- Required: Prepare request body prepare_input = function(inputs, opts) return { model = opts.model, @@ -450,6 +461,7 @@ Here is how you implement ollama provider for example: } end, + -- Required: Get API endpoint URL get_url = function() return 'http://localhost:11434/api/chat' end, @@ -459,79 +471,36 @@ Here is how you implement ollama provider for example: < -API *CopilotChat-api* +PROVIDER INTERFACE + +Custom providers can implement these methods: >lua - local chat = require("CopilotChat") - - -- Open chat window - chat.open() - - -- Open chat window with custom options - chat.open({ - window = { - layout = 'float', - title = 'My Title', - }, - }) - - -- Close chat window - chat.close() - - -- Toggle chat window - chat.toggle() - - -- Toggle chat window with custom options - chat.toggle({ - window = { - layout = 'float', - title = 'My Title', - }, - }) - - -- Reset chat window - chat.reset() - - -- Ask a question - chat.ask("Explain how it works.") - - -- Ask a question with custom options - 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:+' }, - }) + { + -- Optional: Disable provider + disabled?: boolean, - -- Ask a question and do something with the response - chat.ask("Show me something interesting", { - callback = function(response) - print("Response:", response) - end, - }) + -- Optional: Provider to use for embeddings + embeddings?: string, - -- Get all available prompts (can be used for integrations like fzf/telescope) - local prompts = chat.prompts() + -- Optional: Get authentication token + get_token(): string, number?, - -- Get last copilot response (also can be used for integrations and custom keymaps) - local response = chat.response() + -- Required: Get request headers + get_headers(token: string): table, - -- Retrieve current chat config - local config = chat.config - print(config.model) + -- Required: Get API endpoint URL + get_url(opts: table): string, - -- Pick a prompt using vim.ui.select - local actions = require("CopilotChat.actions") + -- Required: Prepare request body + prepare_input(inputs: table, opts: table, model: table): table, - -- Pick prompt actions - actions.pick(actions.prompt_actions({ - selection = require("CopilotChat.select").visual, - })) + -- Optional: Get available models + get_models?(headers: table): table, - -- Programmatically set log level - chat.log_level("debug") + -- Optional: Get available agents + get_agents?(headers: table): table, + } < @@ -541,7 +510,7 @@ API *CopilotChat-api* DEFAULT CONFIGURATION *CopilotChat-default-configuration* -Also see here : +Below are all available configuration options with their default values: >lua { @@ -720,199 +689,205 @@ Also see here : CUSTOMIZING BUFFERS *CopilotChat-customizing-buffers* -You can set local options for the buffers that are created by this plugin, -`copilot-chat`, `copilot-diff`, `copilot-overlay`: +You can set local options for plugin buffers (`copilot-chat`, `copilot-diff`, +`copilot-overlay`): >lua vim.api.nvim_create_autocmd('BufEnter', { pattern = 'copilot-*', callback = function() + -- Set buffer-local options vim.opt_local.relativenumber = true - -- C-p to print last response + -- Add buffer-local mappings vim.keymap.set('n', '', function() print(require("CopilotChat").response()) - end, { buffer = true, remap = true }) + end, { buffer = true }) end }) < ============================================================================== -5. Tips *CopilotChat-tips* - -Quick chat with your buffer ~ - -To chat with Copilot using the entire content of the buffer, you can add the -following configuration to your keymap: +5. API Reference *CopilotChat-api-reference* >lua - -- lazy.nvim keys + local chat = require("CopilotChat") - -- Quick chat with Copilot - { - "ccq", - function() - local input = vim.fn.input("Quick Chat: ") - if input ~= "" then - require("CopilotChat").ask(input, { selection = require("CopilotChat.select").buffer }) - end - end, - desc = "CopilotChat - Quick chat", - } + -- Window Management + chat.open() -- Open chat window + chat.open({ -- Open with custom options + window = { + layout = 'float', + title = 'Custom Chat', + }, + }) + chat.close() -- Close chat window + chat.toggle() -- Toggle chat window + chat.reset() -- Reset chat window + + -- Chat Interaction + chat.ask("Explain this code.") -- Basic question + chat.ask("Explain this code.", { + selection = require("CopilotChat.select").buffer, + context = { 'buffers', 'files' }, + callback = function(response) + print("Response:", response) + end, + }) + + -- Utilities + chat.prompts() -- Get all available prompts + chat.response() -- Get last response + chat.log_level("debug") -- Set log level + + + -- Actions + local actions = require("CopilotChat.actions") + actions.pick(actions.prompt_actions({ + selection = require("CopilotChat.select").visual, + })) + + -- Update config + chat.setup({ + model = 'gpt-4', + window = { + layout = 'float' + } + }) < - -Inline chat ~ +============================================================================== +6. Tips and Examples *CopilotChat-tips-and-examples* -Change the window layout to `float` and position relative to cursor to make the -window look like inline chat. This will allow you to chat with Copilot without -opening a new window. + +QUICK CHAT WITH BUFFER *CopilotChat-quick-chat-with-buffer* + +Set up a quick chat command that uses the entire buffer content: >lua - -- lazy.nvim opts - - { - window = { - layout = 'float', - relative = 'cursor', - width = 1, - height = 0.4, - row = 1 - } - } + -- Quick chat keybinding + vim.keymap.set('n', 'ccq', function() + local input = vim.fn.input("Quick Chat: ") + if input ~= "" then + require("CopilotChat").ask(input, { + selection = require("CopilotChat.select").buffer + }) + end + end, { desc = "CopilotChat - Quick chat" }) < -Telescope integration ~ -Requires telescope.nvim -plugin to be installed. +INLINE CHAT WINDOW *CopilotChat-inline-chat-window* + +Configure the chat window to appear inline near the cursor: >lua - -- lazy.nvim keys - - -- Show prompts actions with telescope - { - "ccp", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.telescope").pick(actions.prompt_actions()) - end, - desc = "CopilotChat - Prompt actions", - }, + require("CopilotChat").setup({ + window = { + layout = 'float', + relative = 'cursor', + width = 1, + height = 0.4, + row = 1 + } + }) < -fzf-lua integration ~ -Requires fzf-lua plugin to be installed. +TELESCOPE INTEGRATION *CopilotChat-telescope-integration* + +Requires telescope.nvim : >lua - -- lazy.nvim keys - - -- Show prompts actions with fzf-lua - { - "ccp", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.fzflua").pick(actions.prompt_actions()) - end, - desc = "CopilotChat - Prompt actions", - }, + vim.keymap.set('n', 'ccp', function() + local actions = require("CopilotChat.actions") + require("CopilotChat.integrations.telescope").pick(actions.prompt_actions()) + end, { desc = "CopilotChat - Prompt actions" }) < -snacks.nvim integration ~ -Requires snacks.nvim plugin to be -installed and the picker - to be -configured. +QUICK SEARCH WITH PERPLEXITY *CopilotChat-quick-search-with-perplexity* + +Requires PerplexityAI Agent : >lua - -- lazy.nvim keys - - -- Show prompts actions with snacks.nvim picker - { - "ccp", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.snacks").pick(actions.prompt_actions()) - end, - desc = "CopilotChat - Prompt actions", - }, + vim.keymap.set({ 'n', 'v' }, 'ccs', function() + local input = vim.fn.input("Perplexity: ") + if input ~= "" then + require("CopilotChat").ask(input, { + agent = "perplexityai", + selection = false, + }) + end + end, { desc = "CopilotChat - Perplexity Search" }) < -render-markdown integration ~ -Requires render-markdown - plugin to be -installed. +MARKDOWN RENDERING *CopilotChat-markdown-rendering* + +Use render-markdown.nvim + for better chat +display: >lua - -- Registers copilot-chat filetype for markdown rendering + -- Register copilot-chat filetype require('render-markdown').setup({ file_types = { 'markdown', 'copilot-chat' }, }) - -- You might also want to disable default header highlighting for copilot chat when doing this and set error header style and separator + -- Adjust chat display settings require('CopilotChat').setup({ highlight_headers = false, separator = '---', error_header = '> [!ERROR] Error', - -- rest of your config }) < -Ask a quick question with Perplexity ~ -Requires PerplexityAI Agent to be -added to GitHub account. - -This sets the `selection = false` to be able to ask generic questions unrelated -to current code. +============================================================================== +7. Development *CopilotChat-development* ->lua - -- lazy.nvim keys - - -- Ask the Perplexity agent a quick question - { - "ccs", - function() - local input = vim.fn.input("Perplexity: ") - if input ~= "" then - require("CopilotChat").ask(input, { - agent = "perplexityai", - selection = false, - }) - end - end, - desc = "CopilotChat - Perplexity Search", - mode = { "n", "v" }, - }, -< +SETUP *CopilotChat-setup* -============================================================================== -6. Development *CopilotChat-development* +To set up the environment: +1. Clone the repository: -INSTALLING PRE-COMMIT TOOL *CopilotChat-installing-pre-commit-tool* +>bash + git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim + cd CopilotChat.nvim +< -For development, you can use the provided Makefile command to install the -pre-commit tool: +1. Install development dependencies: >bash + # Install pre-commit hooks make install-pre-commit < -This will install the pre-commit tool and the pre-commit hooks. +To run tests: +>bash + make test +< -============================================================================== -7. Contributors *CopilotChat-contributors* -If you want to contribute to this project, please read the CONTRIBUTING.md - file. +CONTRIBUTING *CopilotChat-contributing* + +1. Fork the repository +2. Create your feature branch +3. Make your changes +4. Run tests and lint checks +5. Submit a pull request + +See CONTRIBUTING.md for detailed guidelines. + + +============================================================================== +8. Contributors *CopilotChat-contributors* Thanks goes to these wonderful people (emoji key ): @@ -922,21 +897,17 @@ gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguy Contributions of any kind are welcome! -STARGAZERS OVER TIME *CopilotChat-stargazers-over-time* +============================================================================== +9. Stargazers *CopilotChat-stargazers* ============================================================================== -8. Links *CopilotChat-links* +10. Links *CopilotChat-links* 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 +3. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 693780aeb6e5df3f2a4586ac190f1a9268ba57d9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 20:20:35 +0100 Subject: [PATCH 064/728] fix(ollama): use correct module import path The change fixes the ollama provider by using the proper module import path 'CopilotChat.utils' instead of directly accessing the local cutils module. This ensures consistent module resolution across the codebase. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71872a5d..8c5e8a70 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,8 @@ Here's how to implement an [ollama](https://ollama.com/) provider: -- Optional: Get available models get_models = function() - local response = cutils.curl_get('http://localhost:11434/api/tags') + local utils = require('CopilotChat.utils') + local response = utils.curl_get('http://localhost:11434/api/tags') if not response or response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response and response.status)) end From e8eff96c3841e5aa411e37988a0f77a662ed967b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 19:21:25 +0000 Subject: [PATCH 065/728] 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 c5ded71d..4f9ef8f9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -435,7 +435,8 @@ Here’s how to implement an ollama provider: -- Optional: Get available models get_models = function() - local response = cutils.curl_get('http://localhost:11434/api/tags') + local utils = require('CopilotChat.utils') + local response = utils.curl_get('http://localhost:11434/api/tags') if not response or response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response and response.status)) end From bb3899fac816bd031465d354312064f858e96b90 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 20:45:52 +0100 Subject: [PATCH 066/728] fix: update system_prompt config reference The system_prompt configuration option was incorrectly referencing the COPILOT_INSTRUCTIONS table directly. This change updates it to correctly reference the system_prompt field within that table. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c5e8a70..435914e5 100644 --- a/README.md +++ b/README.md @@ -454,7 +454,7 @@ Below are all available configuration options with their default values: -- 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 /). + system_prompt = prompts.COPILOT_INSTRUCTIONS.system_prompt, -- 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 @). From 73cf4c0dcbe8724de29cbf8c7a138e01882418e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 19:46:51 +0000 Subject: [PATCH 067/728] 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 4f9ef8f9..9a56e017 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -518,7 +518,7 @@ Below are all available configuration options with their default values: -- 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 /). + system_prompt = prompts.COPILOT_INSTRUCTIONS.system_prompt, -- 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 @). From fb019df428d6574cf15e929854dabfa9b8df63df Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 20:54:26 +0100 Subject: [PATCH 068/728] docs(provider): update ollama provider interface docs Make get_models headers parameter required for non-embedding providers and improve documentation formatting by updating section header level for better readability. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 435914e5..4158a9e6 100644 --- a/README.md +++ b/README.md @@ -373,10 +373,10 @@ Here's how to implement an [ollama](https://ollama.com/) provider: } end, - -- Optional: Get available models - get_models = function() + -- Required (for non embeddngs): Get available models + get_models = function(headers) local utils = require('CopilotChat.utils') - local response = utils.curl_get('http://localhost:11434/api/tags') + local response = utils.curl_get('http://localhost:11434/api/tags', { headers = headers }) if not response or response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response and response.status)) end @@ -411,7 +411,7 @@ Here's how to implement an [ollama](https://ollama.com/) provider: } ``` -#### Provider Interface +### Provider Interface Custom providers can implement these methods: From 5e77f3fca099b64632645cf7b040243e372e7c84 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 19:55:42 +0000 Subject: [PATCH 069/728] 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 9a56e017..dd5937cd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -433,10 +433,10 @@ Here’s how to implement an ollama provider: } end, - -- Optional: Get available models - get_models = function() + -- Required (for non embeddngs): Get available models + get_models = function(headers) local utils = require('CopilotChat.utils') - local response = utils.curl_get('http://localhost:11434/api/tags') + local response = utils.curl_get('http://localhost:11434/api/tags', { headers = headers }) if not response or response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response and response.status)) end @@ -472,7 +472,7 @@ Here’s how to implement an ollama provider: < -PROVIDER INTERFACE +PROVIDER INTERFACE ~ Custom providers can implement these methods: From 721f91def9db675440537812bb446b92392cd5f3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 21:12:05 +0100 Subject: [PATCH 070/728] fix(embeddings): improve embedding resolution reliability (#794) * fix(embeddings): improve embedding resolution reliability Previously the code would error when embeddings provider was not found. Now it gracefully handles missing providers by skipping embeddings calculation. This improves reliability when custom providers don't implement embeddings support. Also adds null check for spatial distance calculation and moves provider interface documentation before the example in README.md for better readability. * [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 | 70 ++++++++++++++++++------------------- lua/CopilotChat/client.lua | 5 ++- lua/CopilotChat/context.lua | 4 +++ 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 4158a9e6..685a583a 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,38 @@ Providers are modules that implement integration with different AI providers. - `copilot` - Default GitHub Copilot provider used for chat and embeddings - `github_models` - Provider for GitHub Marketplace models +### Provider Interface + +Custom providers can implement these methods: + +```lua +{ + -- Optional: Disable provider + disabled?: boolean, + + -- Optional: Provider to use for embeddings + embeddings?: string, + + -- Optional: Get authentication token + get_token(): string, number?, + + -- Required: Get request headers + get_headers(token: string): table, + + -- Required: Get API endpoint URL + get_url(opts: table): string, + + -- Required: Prepare request body + prepare_input(inputs: table, opts: table, model: table): table, + + -- Optional: Get available models + get_models?(headers: table): table, + + -- Optional: Get available agents + get_agents?(headers: table): table, +} +``` + ### Ollama Example Here's how to implement an [ollama](https://ollama.com/) provider: @@ -366,14 +398,14 @@ Here's how to implement an [ollama](https://ollama.com/) provider: { providers = { ollama = { - -- Required: Headers for API requests + embeddings = 'copilot_embeddings', -- Use Copilot as embedding provider + get_headers = function() return { ['Content-Type'] = 'application/json', } end, - -- Required (for non embeddngs): Get available models get_models = function(headers) local utils = require('CopilotChat.utils') local response = utils.curl_get('http://localhost:11434/api/tags', { headers = headers }) @@ -393,7 +425,6 @@ Here's how to implement an [ollama](https://ollama.com/) provider: return models end, - -- Required: Prepare request body prepare_input = function(inputs, opts) return { model = opts.model, @@ -402,7 +433,6 @@ Here's how to implement an [ollama](https://ollama.com/) provider: } end, - -- Required: Get API endpoint URL get_url = function() return 'http://localhost:11434/api/chat' end, @@ -411,38 +441,6 @@ Here's how to implement an [ollama](https://ollama.com/) provider: } ``` -### Provider Interface - -Custom providers can implement these methods: - -```lua -{ - -- Optional: Disable provider - disabled?: boolean, - - -- Optional: Provider to use for embeddings - embeddings?: string, - - -- Optional: Get authentication token - get_token(): string, number?, - - -- Required: Get request headers - get_headers(token: string): table, - - -- Required: Get API endpoint URL - get_url(opts: table): string, - - -- Required: Prepare request body - prepare_input(inputs: table, opts: table, model: table): table, - - -- Optional: Get available models - get_models?(headers: table): table, - - -- Optional: Get available agents - get_agents?(headers: table): table, -} -``` - # Configuration ## Default Configuration diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 5169c6ef..2cda46e0 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -710,6 +710,7 @@ function Client:embed(inputs, model) error('Model not found: ' .. model) end + -- Resolve model provider local provider_name = model_config.provider if not provider_name then error('Provider not found for model: ' .. model) @@ -718,9 +719,11 @@ function Client:embed(inputs, model) if not provider then error('Provider not found: ' .. model_config.provider) end + + -- Resolve embedding provider, and if resolution fails, do not resolve embeddings provider_name = provider.embeddings if not provider_name then - error('Provider not found for embeddings: ' .. provider_name) + return inputs end provider = self.providers[provider_name] if not provider then diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 2610bbdc..55edd5bc 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -73,6 +73,10 @@ local MULTI_FILE_THRESHOLD = 5 ---@param b table ---@return number local function spatial_distance_cosine(a, b) + if not a or not b then + return 0 + end + local dot_product = 0 local magnitude_a = 0 local magnitude_b = 0 From 814dd493e168e609a50b08f0843fd3277cbb88be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 20:12:26 +0000 Subject: [PATCH 071/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 72 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index dd5937cd..67794bf3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -418,6 +418,39 @@ BUILT-IN PROVIDERS ~ - `github_models` - Provider for GitHub Marketplace models +PROVIDER INTERFACE ~ + +Custom providers can implement these methods: + +>lua + { + -- Optional: Disable provider + disabled?: boolean, + + -- Optional: Provider to use for embeddings + embeddings?: string, + + -- Optional: Get authentication token + get_token(): string, number?, + + -- Required: Get request headers + get_headers(token: string): table, + + -- Required: Get API endpoint URL + get_url(opts: table): string, + + -- Required: Prepare request body + prepare_input(inputs: table, opts: table, model: table): table, + + -- Optional: Get available models + get_models?(headers: table): table, + + -- Optional: Get available agents + get_agents?(headers: table): table, + } +< + + OLLAMA EXAMPLE ~ Here’s how to implement an ollama provider: @@ -426,14 +459,14 @@ Here’s how to implement an ollama provider: { providers = { ollama = { - -- Required: Headers for API requests + embeddings = 'copilot_embeddings', -- Use Copilot as embedding provider + get_headers = function() return { ['Content-Type'] = 'application/json', } end, - -- Required (for non embeddngs): Get available models get_models = function(headers) local utils = require('CopilotChat.utils') local response = utils.curl_get('http://localhost:11434/api/tags', { headers = headers }) @@ -453,7 +486,6 @@ Here’s how to implement an ollama provider: return models end, - -- Required: Prepare request body prepare_input = function(inputs, opts) return { model = opts.model, @@ -462,7 +494,6 @@ Here’s how to implement an ollama provider: } end, - -- Required: Get API endpoint URL get_url = function() return 'http://localhost:11434/api/chat' end, @@ -472,39 +503,6 @@ Here’s how to implement an ollama provider: < -PROVIDER INTERFACE ~ - -Custom providers can implement these methods: - ->lua - { - -- Optional: Disable provider - disabled?: boolean, - - -- Optional: Provider to use for embeddings - embeddings?: string, - - -- Optional: Get authentication token - get_token(): string, number?, - - -- Required: Get request headers - get_headers(token: string): table, - - -- Required: Get API endpoint URL - get_url(opts: table): string, - - -- Required: Prepare request body - prepare_input(inputs: table, opts: table, model: table): table, - - -- Optional: Get available models - get_models?(headers: table): table, - - -- Optional: Get available agents - get_agents?(headers: table): table, - } -< - - ============================================================================== 4. Configuration *CopilotChat-configuration* From 754d5ac3499fcdb20ad58886a1be1bdeea454fa7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 21:45:55 +0100 Subject: [PATCH 072/728] refactor: streamline debug info into `info` mapping Move debug info functionality into info mapping and remove separate debug UI window. This simplifies the codebase while keeping all debugging functionality available through a unified interface. The debug info now includes: - Log file location - History file location - Temp files directory - Model and agent info - System prompt details BREAKING CHANGE: Removes CopilotChatDebugInfo command. Use `gi` in chat instead. Signed-off-by: Tomas Slusny --- README.md | 3 +- lua/CopilotChat/config.lua | 3 + lua/CopilotChat/config/mappings.lua | 21 ++--- lua/CopilotChat/init.lua | 14 +-- lua/CopilotChat/ui/debug.lua | 138 ---------------------------- 5 files changed, 17 insertions(+), 162 deletions(-) delete mode 100644 lua/CopilotChat/ui/debug.lua diff --git a/README.md b/README.md index 685a583a..a68b58ec 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,6 @@ Commands are used to control the chat interface: | `:CopilotChatReset` | Reset chat window | | `:CopilotChatSave ?` | Save chat history | | `:CopilotChatLoad ?` | Load chat history | -| `:CopilotChatDebugInfo` | Show debug info | | `:CopilotChatModels` | View/select available models | | `:CopilotChatAgents` | View/select available agents | | `:CopilotChat` | Use specific prompt template | @@ -500,6 +499,8 @@ Below are all available configuration options with their default values: allow_insecure = false, -- Allow insecure server connections chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) + + log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history question_header = '# User ', -- Header to use for user questions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index ce300a75..44cbe783 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -40,6 +40,7 @@ local select = require('CopilotChat.select') ---@field proxy string? ---@field allow_insecure boolean? ---@field chat_autocomplete boolean? +---@field log_path string? ---@field history_path string? ---@field question_header string? ---@field answer_header string? @@ -101,6 +102,8 @@ return { allow_insecure = false, -- Allow insecure server connections chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) + + log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index b45f73a6..a119499a 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -352,22 +352,22 @@ return { local system_prompt = config.system_prompt async.run(function() - local selected_agent = copilot.resolve_agent(prompt, config) - local selected_model = copilot.resolve_model(prompt, config) + local _, selected_agent = pcall(copilot.resolve_agent, prompt, config) + local _, selected_model = pcall(copilot.resolve_model, prompt, config) + + async.util.scheduler() + table.insert(lines, '**Logs**: `' .. chat.config.log_path .. '`') + table.insert(lines, '**History**: `' .. chat.config.history_path .. '`') + table.insert(lines, '**Temp Files**: `' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`') + table.insert(lines, '') if selected_model then - table.insert(lines, '**Model**') - table.insert(lines, '```') - table.insert(lines, selected_model) - table.insert(lines, '```') + table.insert(lines, '**Model**: `' .. selected_model .. '`') 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, '**Agent**: `' .. selected_agent .. '`') table.insert(lines, '') end @@ -381,7 +381,6 @@ return { table.insert(lines, '') end - async.util.scheduler() overlay:show(vim.trim(table.concat(lines, '\n')) .. '\n', chat.winnr, 'markdown') end) end, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7a07b9ae..17d37ca0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -18,7 +18,6 @@ local WORD = '([^%s]+)' --- @field last_response string? --- @field chat CopilotChat.ui.Chat? --- @field diff CopilotChat.ui.Diff? ---- @field debug CopilotChat.ui.Debug? --- @field overlay CopilotChat.ui.Overlay? local state = { client = nil, @@ -34,7 +33,6 @@ local state = { chat = nil, diff = nil, overlay = nil, - debug = nil, } --- Highlights the selection in the source buffer. @@ -785,13 +783,12 @@ 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) + log.new({ plugin = PLUGIN_NAME, level = level, - outfile = logfile, + outfile = M.config.log_path, }, true) - log.logfile = logfile end --- Set up the plugin @@ -881,10 +878,6 @@ function M.setup(config) end) end) - if not state.debug then - state.debug = require('CopilotChat.ui.debug')() - end - if state.diff then state.diff:delete() end @@ -1023,9 +1016,6 @@ 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() - state.debug:open() - end, { force = true }) local function complete_load() local options = vim.tbl_map(function(file) diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua deleted file mode 100644 index 9172b714..00000000 --- a/lua/CopilotChat/ui/debug.lua +++ /dev/null @@ -1,138 +0,0 @@ -local async = require('plenary.async') -local log = require('plenary.log') -local utils = require('CopilotChat.utils') -local context = require('CopilotChat.context') -local Overlay = require('CopilotChat.ui.overlay') -local class = utils.class - ----@return table -local function build_debug_info() - local lines = { - 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', - '', - 'Log file path:', - '`' .. log.logfile .. '`', - '', - 'Data directory:', - '`' .. vim.fn.stdpath('data') .. '`', - '', - 'Config directory:', - '`' .. utils.config_path() .. '`', - '', - 'Temp directory:', - '`' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`', - '', - } - - local buf = context.buffer(0) - if buf then - 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 - table.insert(lines, '') - end - - table.insert(lines, 'Current buffer outline:') - table.insert(lines, '`' .. buf.filename .. '`') - table.insert(lines, '```' .. buf.filetype) - local outline_lines = vim.split(buf.outline or buf.content, '\n') - for _, line in ipairs(outline_lines) do - table.insert(lines, line) - end - table.insert(lines, '```') - end - - local files = context.files() - 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 - end - table.insert(lines, '```') - end - - 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: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() - - async.run(function() - local lines = build_debug_info() - async.util.scheduler() - - 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 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, - } - - 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'), self.winnr, 'markdown') - vim.api.nvim_win_set_cursor(self.winnr, { 1, 0 }) - end) -end - -return Debug From 782f8811bbd943a745765cbdad3efeeed5c216a0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Feb 2025 20:49:29 +0000 Subject: [PATCH 073/728] 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 67794bf3..34ef9b39 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -145,7 +145,6 @@ Commands are used to control the chat interface: :CopilotChatReset Reset chat window :CopilotChatSave ? Save chat history :CopilotChatLoad ? Load chat history - :CopilotChatDebugInfo Show debug info :CopilotChatModels View/select available models :CopilotChatAgents View/select available agents :CopilotChat Use specific prompt template @@ -564,6 +563,8 @@ Below are all available configuration options with their default values: allow_insecure = false, -- Allow insecure server connections chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) + + log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history question_header = '# User ', -- Header to use for user questions From 5295e4014cd155851b963889dd13dda1b1384dbd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Feb 2025 23:01:42 +0100 Subject: [PATCH 074/728] docs: add project name and repository info to docsify config Updates the docsify configuration to include the project name and repository URL, improving documentation site navigation and identification. Also adjusts top margin for better readability. --- index.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.html b/index.html index eb5f1ae4..5f784411 100644 --- a/index.html +++ b/index.html @@ -19,8 +19,11 @@ From 0fb49bd7d08b997263bc9deb3a1420ed4c8ad12a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:02:22 +0000 Subject: [PATCH 075/728] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 5f784411..ad678734 100644 --- a/index.html +++ b/index.html @@ -19,8 +19,8 @@ ', - '' - ) + :gsub('', '') :gsub('', '') -- Remove XML/CDATA in one go :gsub('', '') -- Remove all HTML tags (both opening and closing) in one go - :gsub( - '<%/?%w+[^>]*>', - ' ' - ) + :gsub('<%/?%w+[^>]*>', ' ') -- Handle common HTML entities :gsub('&(%w+);', { nbsp = ' ', diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index ace04ad5..5d02df19 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -74,18 +74,14 @@ function M.check() local rg_version = run_command('rg', '--version') if rg_version == false then - warn( - 'rg: missing, optional for improved search performance. See "https://github.com/BurntSushi/ripgrep".' - ) + warn('rg: missing, optional for improved search performance. See "https://github.com/BurntSushi/ripgrep".') else ok('rg: ' .. rg_version) end local lynx_version = run_command('lynx', '-version') if lynx_version == false then - warn( - 'lynx: missing, optional for improved fetching of url contents. See "https://lynx.invisible-island.net/".' - ) + warn('lynx: missing, optional for improved fetching of url contents. See "https://lynx.invisible-island.net/".') else ok('lynx: ' .. lynx_version) end @@ -95,9 +91,7 @@ function M.check() if lualib_installed('plenary') then ok('plenary: installed') else - error( - 'plenary: missing, required for http requests and async jobs. Install "nvim-lua/plenary.nvim" plugin.' - ) + error('plenary: missing, required for http requests and async jobs. Install "nvim-lua/plenary.nvim" plugin.') end local has_copilot = lualib_installed('copilot') @@ -122,9 +116,7 @@ function M.check() if lualib_installed('tiktoken_core') then ok('tiktoken_core: installed') else - warn( - 'tiktoken_core: missing, optional for accurate token counting. See README for installation instructions.' - ) + warn('tiktoken_core: missing, optional for accurate token counting. See README for installation instructions.') end if treesitter_parser_available('markdown') then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 61825e1e..e737b51c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -71,11 +71,7 @@ local function insert_sticky(prompt, config, override_sticky) stickies:set('/' .. config.system_prompt, true) end - if - config.remember_as_sticky - and config.context - and not vim.deep_equal(config.context, M.config.context) - then + if config.remember_as_sticky and config.context and not vim.deep_equal(config.context, M.config.context) then if type(config.context) == 'table' then ---@diagnostic disable-next-line: param-type-mismatch for _, context in ipairs(config.context) do @@ -124,12 +120,7 @@ local function update_highlights() if M.chat.config.highlight_selection and M.chat:focused() then local selection = M.get_selection() - if - not selection - or not utils.buf_valid(selection.bufnr) - or not selection.start_line - or not selection.end_line - then + if not selection or not utils.buf_valid(selection.bufnr) or not selection.start_line or not selection.end_line then return end @@ -260,11 +251,7 @@ local function map_key(name, bufnr, fn) if vim.fn.pumvisible() == 1 then 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 - ) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(used_key, true, false, true), 'n', false) end else fn() @@ -276,9 +263,7 @@ end --- Updates the source buffer based on previous or current window. local function update_source() local use_prev_window = M.chat:focused() - M.set_source( - use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win() - ) + M.set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) end --- Resolve the final prompt and config from prompt template. @@ -379,13 +364,10 @@ function M.resolve_context(prompt, config) local context_value = M.config.contexts[context_data.name] notify.publish( notify.STATUS, - 'Resolving context: ' - .. context_data.name - .. (context_data.input and ' with input: ' .. context_data.input or '') + 'Resolving context: ' .. context_data.name .. (context_data.input and ' with input: ' .. context_data.input or '') ) - local ok, resolved_embeddings = - pcall(context_value.resolve, context_data.input, state.source or {}, prompt) + local ok, resolved_embeddings = pcall(context_value.resolve, context_data.input, state.source or {}, prompt) if ok then for _, embedding in resolved_embeddings do if embedding then @@ -460,11 +442,7 @@ function M.set_source(source_winnr) local source_bufnr = vim.api.nvim_win_get_buf(source_winnr) -- Check if the window is valid to use as a source - if - source_winnr ~= M.chat.winnr - and source_bufnr ~= M.chat.bufnr - and vim.fn.win_gettype(source_winnr) == '' - then + if source_winnr ~= M.chat.winnr and source_bufnr ~= M.chat.bufnr and vim.fn.win_gettype(source_winnr) == '' then state.source = { bufnr = source_bufnr, winnr = source_winnr, @@ -832,10 +810,7 @@ function M.select_prompt(config) end, }, function(choice) if choice then - M.ask( - prompts[choice.name].prompt, - vim.tbl_extend('force', prompts[choice.name], config or {}) - ) + M.ask(prompts[choice.name].prompt, vim.tbl_extend('force', prompts[choice.name], config or {})) end end) end @@ -914,24 +889,23 @@ function M.ask(prompt, config) return end - local ask_ok, response, references, token_count, token_max_count = - pcall(client.ask, client, prompt, { - load_history = not config.headless, - store_history = not config.headless, - contexts = contexts, - selection = selection, - embeddings = filtered_embeddings, - system_prompt = system_prompt, - model = selected_model, - agent = selected_agent, - temperature = config.temperature, - on_progress = vim.schedule_wrap(function(token) - if not config.headless then - M.chat:append(token) - end - has_output = true - end), - }) + local ask_ok, response, references, token_count, token_max_count = pcall(client.ask, client, prompt, { + load_history = not config.headless, + store_history = not config.headless, + contexts = contexts, + selection = selection, + embeddings = filtered_embeddings, + system_prompt = system_prompt, + model = selected_model, + agent = selected_agent, + temperature = config.temperature, + on_progress = vim.schedule_wrap(function(token) + if not config.headless then + M.chat:append(token) + end + has_output = true + end), + }) utils.schedule_main() @@ -1166,10 +1140,7 @@ function M.setup(config) buffer = bufnr, callback = function() local completeopt = vim.opt.completeopt:get() - if - not vim.tbl_contains(completeopt, 'noinsert') - and not vim.tbl_contains(completeopt, 'noselect') - then + if not vim.tbl_contains(completeopt, 'noinsert') and not vim.tbl_contains(completeopt, 'noselect') then -- Don't trigger completion if completeopt is not set to noinsert or noselect return end diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index eb572b35..aaabcbd0 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -12,9 +12,7 @@ end local function load_tiktoken_data(tokenizer) utils.schedule_main() - local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' - .. tokenizer - .. '.tiktoken' + local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' .. tokenizer .. '.tiktoken' local cache_dir = vim.fn.stdpath('cache') vim.fn.mkdir(tostring(cache_dir), 'p') diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 61c55f67..fbeef207 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -28,9 +28,7 @@ local function match_header(header) for _, pattern in ipairs(HEADER_PATTERNS) do local filename, start_line, end_line = header:match(pattern) if filename then - return utils.filepath(filename), - tonumber(start_line) or 1, - tonumber(end_line) or tonumber(start_line) or 1 + return utils.filepath(filename), tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 end end end @@ -100,9 +98,7 @@ end, Overlay) --- Returns whether the chat window is visible. ---@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 + return self.winnr and vim.api.nvim_win_is_valid(self.winnr) and vim.api.nvim_win_get_buf(self.winnr) == self.bufnr or false end @@ -131,11 +127,7 @@ function Chat:get_closest_section(type) or (type == 'answer' and section.answer) or (type == 'question' and not section.answer) - if - matches_type - and section.start_line <= cursor_line - and section.start_line > max_line_below_cursor - then + if matches_type and 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 @@ -267,14 +259,7 @@ function Chat:overlay(opts) return end - self.chat_overlay:show( - opts.text, - self.winnr, - opts.filetype, - opts.syntax, - opts.on_show, - opts.on_hide - ) + self.chat_overlay:show(opts.text, self.winnr, opts.filetype, opts.syntax, opts.on_show, opts.on_hide) end --- Open the chat window. @@ -438,14 +423,7 @@ function Chat:append(str) end local last_line, last_column, _ = self:last() - vim.api.nvim_buf_set_text( - self.bufnr, - last_line, - last_column, - last_line, - last_column, - vim.split(str, '\n') - ) + vim.api.nvim_buf_set_text(self.bufnr, last_line, last_column, last_line, last_column, vim.split(str, '\n')) if should_follow_cursor then self:follow() @@ -489,11 +467,7 @@ end ---@protected 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 + 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 @@ -516,12 +490,8 @@ function Chat:render() separator_found = true if current_section then current_section.end_line = l - 1 - current_section.content = vim.trim( - table.concat( - vim.list_slice(lines, current_section.start_line, current_section.end_line), - '\n' - ) - ) + current_section.content = + vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) table.insert(sections, current_section) end current_section = { @@ -533,12 +503,8 @@ function Chat:render() separator_found = true if current_section then current_section.end_line = l - 1 - current_section.content = vim.trim( - table.concat( - vim.list_slice(lines, current_section.start_line, current_section.end_line), - '\n' - ) - ) + current_section.content = + vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) table.insert(sections, current_section) end current_section = { @@ -549,12 +515,8 @@ function Chat:render() elseif l == line_count then if current_section then current_section.end_line = l - current_section.content = vim.trim( - table.concat( - vim.list_slice(lines, current_section.start_line, current_section.end_line), - '\n' - ) - ) + current_section.content = + vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) table.insert(sections, current_section) end end @@ -601,10 +563,8 @@ function Chat:render() } elseif line == '```' and current_block then current_block.end_line = l - 1 - current_block.content = table.concat( - vim.list_slice(lines, current_block.start_line, current_block.end_line), - '\n' - ) + current_block.content = + table.concat(vim.list_slice(lines, current_block.start_line, current_block.end_line), '\n') table.insert(current_section.blocks, current_block) current_block = nil end diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index 7deaed2c..4b69ae44 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -52,20 +52,14 @@ function Spinner:start() frame = self.status .. ' ' .. frame end - vim.api.nvim_buf_set_extmark( - self.bufnr, - self.ns, - math.max(0, vim.api.nvim_buf_line_count(self.bufnr) - 1), - 0, - { - id = 1, - hl_mode = 'combine', - priority = 100, - virt_text = { - { frame, 'CopilotChatStatus' }, - }, - } - ) + vim.api.nvim_buf_set_extmark(self.bufnr, self.ns, math.max(0, vim.api.nvim_buf_line_count(self.bufnr) - 1), 0, { + id = 1, + hl_mode = 'combine', + priority = 100, + virt_text = { + { frame, 'CopilotChatStatus' }, + }, + }) self.index = self.index % #spinner_frames + 1 end) diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index b4dc5e44..9b4639df 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -4,10 +4,7 @@ end local min_version = '0.10.0' if vim.fn.has('nvim-' .. min_version) ~= 1 then - vim.notify_once( - ('CopilotChat.nvim requires Neovim >= %s'):format(min_version), - vim.log.levels.ERROR - ) + vim.notify_once(('CopilotChat.nvim requires Neovim >= %s'):format(min_version), vim.log.levels.ERROR) return end @@ -20,11 +17,7 @@ vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { link = 'Keyword', default = true vim.api.nvim_set_hl(0, 'CopilotChatInput', { link = 'Special', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) -vim.api.nvim_set_hl( - 0, - 'CopilotChatSeparator', - { link = '@punctuation.special.markdown', default = true } -) +vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) -- Setup commands vim.api.nvim_create_user_command('CopilotChat', function(args) From 96f23809efdcc99b5eb1c8a8ee498de5e53bc944 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 00:54:43 +0100 Subject: [PATCH 300/728] fix: use ipairs when iterating over resolved embeddings The code was incorrectly iterating directly over the resolved_embeddings array, which can lead to improper iteration in Lua. This commit fixes the issue by properly using ipairs() to ensure sequential iteration over numeric indices. --- 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 e737b51c..ed02a1b2 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -369,7 +369,7 @@ function M.resolve_context(prompt, config) local ok, resolved_embeddings = pcall(context_value.resolve, context_data.input, state.source or {}, prompt) if ok then - for _, embedding in resolved_embeddings do + for _, embedding in ipairs(resolved_embeddings) do if embedding then embeddings:set(embedding.filename, embedding) end From a489769f6943f51c081126ae7f5e5bce6be4dc4e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 14 Mar 2025 21:45:22 +0100 Subject: [PATCH 301/728] feat(chat): add streaming callback support Add stream callback function to process tokens as they are received from Copilot while callback function is used to process the complete final response. Both functions can modify the text that gets displayed and stored in history. This separates streaming token processing from final response handling, giving more control over how chat outputs are processed and displayed. Signed-off-by: Tomas Slusny --- README.md | 6 ++-- lua/CopilotChat/client.lua | 18 ++--------- lua/CopilotChat/config.lua | 8 +++-- lua/CopilotChat/config/prompts.lua | 1 + lua/CopilotChat/init.lua | 49 +++++++++++++++++++----------- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e6f3f8fc..72fca872 100644 --- a/README.md +++ b/README.md @@ -460,8 +460,9 @@ Below are all available configuration options with their default values: sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. temperature = 0.1, -- GPT result temperature - 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 + headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) + stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) + callback = nil, -- Function called when full response is received (retuned string is stored to history) remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions -- default selection @@ -748,6 +749,7 @@ require("CopilotChat").open() require("CopilotChat").ask("Explain this code", { callback = function(response) vim.notify("Got response: " .. response:sub(1, 50) .. "...") + return response end, context = "#buffer" }) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index b0638bd8..159c8268 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,6 +1,6 @@ ---@class CopilotChat.Client.ask ---@field load_history boolean ----@field store_history boolean +---@field summarize_history boolean ---@field contexts table? ---@field selection CopilotChat.select.selection? ---@field embeddings table? @@ -530,7 +530,7 @@ function Client:ask(prompt, opts) -- If we're over history limit, trigger summarization if history_tokens > history_limit then - if opts.store_history and #history >= 4 then + if opts.summarize_history and #history >= 4 then self:summarize_history(opts.model) -- Recalculate history and tokens @@ -755,18 +755,6 @@ function Client:ask(prompt, opts) return end - if opts.store_history then - table.insert(self.history, { - content = prompt, - role = 'user', - }) - - table.insert(self.history, { - content = response_text, - role = 'assistant', - }) - end - return response_text, references:values(), last_message and last_message.total_tokens or 0, max_tokens end @@ -943,7 +931,7 @@ Ensure all critical code samples, commands, and configuration snippets are prese local response = self:ask('Create a technical summary of our conversation for future context', { load_history = true, - store_history = false, + summarize_history = false, model = model, temperature = 0, system_prompt = system_prompt, diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 04014ce6..d855cd29 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -20,7 +20,8 @@ local select = require('CopilotChat.select') ---@field sticky string|table|nil ---@field temperature number? ---@field headless boolean? ----@field callback fun(response: string, source: CopilotChat.source)? +---@field stream nil|fun(chunk: string, source: CopilotChat.source):string +---@field callback nil|fun(response: string, source: CopilotChat.source):string ---@field remember_as_sticky boolean? ---@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.selection? ---@field window CopilotChat.config.window? @@ -63,8 +64,9 @@ return { sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. temperature = 0.1, -- GPT result temperature - 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 + headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) + stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) + callback = nil, -- Function called when full response is received (retuned string is stored to history) remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions -- default selection diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index bc68ece6..85552adf 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -141,6 +141,7 @@ return { end end vim.diagnostic.set(vim.api.nvim_create_namespace('copilot-chat-diagnostics'), source.bufnr, diagnostics) + return response end, }, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ed02a1b2..bb8d98d0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -197,8 +197,7 @@ end --- Show an error in the chat window. ---@param err string|table|nil ----@param append_newline boolean? -local function show_error(err, append_newline) +local function show_error(err) err = err or 'Unknown error' if type(err) == 'string' then @@ -213,11 +212,7 @@ local function show_error(err, append_newline) err = utils.make_string(err) end - if append_newline then - M.chat:append('\n') - end - - M.chat:append(M.config.error_header .. '\n```error\n' .. err .. '\n```') + M.chat:append('\n' .. M.config.error_header .. '\n```error\n' .. err .. '\n```') finish() end @@ -876,7 +871,6 @@ function M.ask(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) local embeddings, prompt = M.resolve_context(prompt, config) - local has_output = false local query_ok, filtered_embeddings = pcall(context.filter_embeddings, prompt, selected_model, config.headless, embeddings) @@ -884,14 +878,14 @@ function M.ask(prompt, config) utils.schedule_main() log.error(filtered_embeddings) if not config.headless then - show_error(filtered_embeddings, has_output) + show_error(filtered_embeddings) end return end local ask_ok, response, references, token_count, token_max_count = pcall(client.ask, client, prompt, { load_history = not config.headless, - store_history = not config.headless, + summarize_history = not config.headless, contexts = contexts, selection = selection, embeddings = filtered_embeddings, @@ -900,10 +894,16 @@ function M.ask(prompt, config) agent = selected_agent, temperature = config.temperature, on_progress = vim.schedule_wrap(function(token) - if not config.headless then + local to_print = not config.headless and token + if to_print and config.stream then + local out = config.stream(token, state.source) + if out ~= nil then + to_print = out + end + end + if to_print and to_print ~= '' then M.chat:append(token) end - has_output = true end), }) @@ -912,11 +912,30 @@ function M.ask(prompt, config) if not ask_ok then log.error(response) if not config.headless then - show_error(response, has_output) + show_error(response) end return end + -- Call the callback function and store to history + local to_store = not config.headless and response + if to_store and config.callback then + local out = config.callback(response, state.source) + if out ~= nil then + to_store = out + end + end + if to_store and to_store ~= '' then + table.insert(client.history, { + content = prompt, + role = 'user', + }) + table.insert(client.history, { + content = to_store, + role = 'assistant', + }) + end + if not config.headless then state.last_response = response M.chat.references = references @@ -932,10 +951,6 @@ function M.ask(prompt, config) finish() end - - if config.callback then - config.callback(response, state.source) - end end) if not ok then From c76a64dd793fec740f0b62cc43ead8cb57b22d14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 15 Mar 2025 16:16:56 +0000 Subject: [PATCH 302/728] 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 ec4a079e..7a3b4b90 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 14 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 15 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -519,8 +519,9 @@ Below are all available configuration options with their default values: sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. temperature = 0.1, -- GPT result temperature - 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 + headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) + stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) + callback = nil, -- Function called when full response is received (retuned string is stored to history) remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions -- default selection @@ -814,6 +815,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* require("CopilotChat").ask("Explain this code", { callback = function(response) vim.notify("Got response: " .. response:sub(1, 50) .. "...") + return response end, context = "#buffer" }) From fcd5213327557758e7cb2b1162d7e7a0615cd01c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 17:40:02 +0100 Subject: [PATCH 303/728] refactor: improve headless mode handling This change consolidates headless operation handling in CopilotChat by: - Replacing summarize_history field with a unified headless field - Conditionally displaying notification messages based on headless state - Skipping current_job tracking in headless mode - Preventing unnecessary UI updates when running headless This creates a more consistent approach to handling headless operations throughout the codebase. --- lua/CopilotChat/client.lua | 22 ++++++++++++++-------- lua/CopilotChat/init.lua | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 159c8268..08b99713 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,6 +1,6 @@ ---@class CopilotChat.Client.ask ---@field load_history boolean ----@field summarize_history boolean +---@field headless boolean ---@field contexts table? ---@field selection CopilotChat.select.selection? ---@field embeddings table? @@ -485,7 +485,9 @@ function Client:ask(prompt, opts) tiktoken.load(tokenizer) end - notify.publish(notify.STATUS, 'Generating request') + if not opts.headless then + notify.publish(notify.STATUS, 'Generating request') + end local history = {} if opts.load_history then @@ -530,7 +532,7 @@ function Client:ask(prompt, opts) -- If we're over history limit, trigger summarization if history_tokens > history_limit then - if opts.summarize_history and #history >= 4 then + if not opts.headless and #history >= 4 then self:summarize_history(opts.model) -- Recalculate history and tokens @@ -603,7 +605,9 @@ function Client:ask(prompt, opts) end log.debug('Response line:', line) - notify.publish(notify.STATUS, '') + if not opts.headless then + notify.publish(notify.STATUS, '') + end local content, err = utils.json_decode(line) @@ -666,7 +670,7 @@ function Client:ask(prompt, opts) return end - if self.current_job ~= job_id then + if not opts.headless and self.current_job ~= job_id then finish_stream(nil, job) return end @@ -679,8 +683,10 @@ function Client:ask(prompt, opts) parse_stream_line(line, job) end - notify.publish(notify.STATUS, 'Thinking') - self.current_job = job_id + if not opts.headless then + notify.publish(notify.STATUS, 'Thinking') + self.current_job = job_id + end local headers = self:authenticate(provider_name) local request = provider.prepare_input( @@ -931,7 +937,7 @@ Ensure all critical code samples, commands, and configuration snippets are prese local response = self:ask('Create a technical summary of our conversation for future context', { load_history = true, - summarize_history = false, + headless = true, model = model, temperature = 0, system_prompt = system_prompt, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index bb8d98d0..aab3285a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -885,7 +885,7 @@ function M.ask(prompt, config) local ask_ok, response, references, token_count, token_max_count = pcall(client.ask, client, prompt, { load_history = not config.headless, - summarize_history = not config.headless, + headless = config.headless, contexts = contexts, selection = selection, embeddings = filtered_embeddings, From eb68cb8d80d1d38626de021ed86a2a824d647357 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 18:18:08 +0100 Subject: [PATCH 304/728] refactor: optimize code outline generation with caching Improve outline generation performance by: - Creating a separate outline cache to avoid redundant processing - Converting M.get_outline to a local function with cleaner return values - Generating outlines on-demand during embedding filtering - Improving cache key generation with longer hash samples - Refactoring file and buffer handling to simplify data flow This change reduces processing overhead by only creating outlines when needed and avoiding duplicate work through targeted caching. --- lua/CopilotChat/context.lua | 85 +++++++++++++++++++++++-------------- lua/CopilotChat/utils.lua | 2 +- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 5a4b058a..a9bce96d 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -25,6 +25,7 @@ local utils = require('CopilotChat.utils') local file_cache = {} local url_cache = {} local embedding_cache = {} +local outline_cache = {} local M = {} @@ -265,16 +266,12 @@ end --- Build an outline and symbols from a string ---@param content string ----@param filename string ---@param ft string ----@return CopilotChat.context.embed -function M.get_outline(content, filename, ft) - ---@type CopilotChat.context.embed - local output = { - filename = filename, - filetype = ft, - content = content, - } +---@return string?, table? +local function get_outline(content, ft) + if not ft or ft == '' or ft == 'text' or ft == 'raw' then + return nil + end local lang = vim.treesitter.language.get_lang(ft) local ok, parser = false, nil @@ -285,7 +282,7 @@ function M.get_outline(content, filename, ft) ft = string.gsub(ft, 'react', '') ok, parser = pcall(vim.treesitter.get_string_parser, content, ft) if not ok or not parser then - return output + return nil end end @@ -334,12 +331,10 @@ function M.get_outline(content, filename, ft) parse_node(root) - if #outline_lines > 0 then - output.outline = table.concat(outline_lines, '\n') - output.symbols = symbols + if #outline_lines == 0 then + return nil end - - return output + return table.concat(outline_lines, '\n'), symbols end --- Get data for a file @@ -361,21 +356,23 @@ function M.get_file(filename, filetype) local cached = file_cache[filename] if cached and cached.modified >= modified then - return cached.outline + return cached end local content = utils.read_file(filename) - if content then - local outline = M.get_outline(content, filename, filetype) - file_cache[filename] = { - outline = outline, - modified = modified, - } - - return outline + if not content or content == '' then + return nil end - return nil + local out = { + content = content, + filename = filename, + filetype = filetype, + modified = modified, + } + + file_cache[filename] = out + return out end --- Get data for a buffer @@ -391,12 +388,13 @@ function M.get_buffer(bufnr) return nil end - local out = - M.get_outline(table.concat(content, '\n'), utils.filepath(vim.api.nvim_buf_get_name(bufnr)), vim.bo[bufnr].filetype) - - out.score = 0.1 - out.diagnostics = utils.diagnostics(bufnr) - return out + return { + content = table.concat(content, '\n'), + filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filetype = vim.bo[bufnr].filetype, + score = 0.1, + diagnostics = utils.diagnostics(bufnr), + } end --- Get the content of an URL @@ -463,6 +461,31 @@ function M.filter_embeddings(prompt, model, headless, embeddings) return embeddings end + notify.publish(notify.STATUS, 'Preparing embedding outline') + + for _, item in ipairs(embeddings) do + if not item.outline then + local cache_key = item.filename .. utils.quick_hash(item.content) + local outline = outline_cache[cache_key] + if not outline then + local outline_text, symbols = get_outline(item.content, item.filetype) + if outline_text then + outline = { + outline = outline_text, + symbols = symbols, + } + + outline_cache[cache_key] = outline + end + end + + if outline then + item.outline = outline.outline + item.symbols = outline.symbols + end + end + end + notify.publish(notify.STATUS, 'Ranking embeddings') -- Build query from history and prompt diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 52cfa546..cd02b9ff 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -285,7 +285,7 @@ end ---@param str string The string to hash ---@return string function M.quick_hash(str) - return #str .. str:sub(1, 32) .. str:sub(-32) + return #str .. str:sub(1, 64) .. str:sub(-64) end --- Make a string from arguments From 417cedf27ea164f92c8ab7656905d4ef631e8e91 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 18:50:43 +0100 Subject: [PATCH 305/728] feat(context): add async treesitter parsing support Implement async treesitter parsing with the new ts_parse_string utility function to improve performance during context gathering. Update file processing in context providers to use vim.iter for cleaner functional approach and ensure filetype is correctly passed through the pipeline. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/contexts.lua | 37 +++++++++++++++-------------- lua/CopilotChat/context.lua | 5 +--- lua/CopilotChat/utils.lua | 23 ++++++++++++++++++ 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/lua/CopilotChat/config/contexts.lua b/lua/CopilotChat/config/contexts.lua index daa0eba6..ed3bf478 100644 --- a/lua/CopilotChat/config/contexts.lua +++ b/lua/CopilotChat/config/contexts.lua @@ -80,7 +80,7 @@ return { utils.schedule_main() return { - context.get_file(utils.filepath(input)), + context.get_file(utils.filepath(input), utils.filetype(input)), } end, }, @@ -93,7 +93,6 @@ return { }, callback) end, resolve = function(input, source) - local out = {} local files = utils.scan_dir(source.cwd(), { glob = input, }) @@ -111,14 +110,15 @@ return { end, files) ) - for _, file in ipairs(files) do - local file_data = context.get_file(file.name, file.ft) - if file_data then - table.insert(out, file_data) - end - end - - return out + return vim + .iter(files) + :map(function(file) + return context.get_file(file.name, file.ft) + end) + :filter(function(file_data) + return file_data ~= nil + end) + :totable() end, }, @@ -285,14 +285,15 @@ return { end, vim.tbl_keys(unique_files)) ) - local out = {} - for _, file in ipairs(files) do - local file_data = context.get_file(file.name, file.ft) - if file_data then - table.insert(out, file_data) - end - end - return out + return vim + .iter(files) + :map(function(file) + return context.get_file(file.name, file.ft) + end) + :filter(function(file_data) + return file_data ~= nil + end) + :totable() end, }, diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index a9bce96d..437c8884 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -286,7 +286,7 @@ local function get_outline(content, ft) end end - local root = parser:parse()[1]:root() + local root = utils.ts_parse(parser) local lines = vim.split(content, '\n') local symbols = {} local outline_lines = {} @@ -342,9 +342,6 @@ end ---@param filetype string? ---@return CopilotChat.context.embed? function M.get_file(filename, filetype) - if not filetype then - filetype = utils.filetype(filename) - end if not filetype then return nil end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index cd02b9ff..31e2ecf8 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -554,6 +554,29 @@ M.schedule_main = async.wrap(function(callback) end end, 1) +--- Run parse on a treesitter parser asynchronously if possible +---@param parser vim.treesitter.LanguageTree The parser +M.ts_parse = async.wrap(function(parser, callback) + ---@diagnostic disable-next-line: invisible + if not parser._async_parse then + local trees = parser:parse(false) + if not trees or #trees == 0 then + callback(nil) + return + end + callback(trees[1]:root()) + return + end + + parser:parse(false, function(err, trees) + if err or not trees or #trees == 0 then + callback(nil) + return + end + callback(trees[1]:root()) + end) +end, 2) + --- Get the info for a key. ---@param name string ---@param key table From 6a573c4b82ffb00076b97f02b9442256b9cb5205 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 19:39:45 +0100 Subject: [PATCH 306/728] refactor(cache): improve embedding cache consistency Store only essential embedding data in cache and make cache keys consistent throughout the code. This change separates cache internal fields with underscore prefix and ensures clean object structure when returning cached results. Key improvements: - Use consistent hash key with _hash property - Store only embedding vectors instead of full objects - Return clean objects when retrieving from file cache - Mark internal cache fields with underscore prefix --- lua/CopilotChat/context.lua | 69 ++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 437c8884..494282a1 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -352,8 +352,13 @@ function M.get_file(filename, filetype) end local cached = file_cache[filename] - if cached and cached.modified >= modified then - return cached + if cached and cached._modified >= modified then + return { + content = cached.content, + _modified = cached._modified, + filename = filename, + filetype = filetype, + } end local content = utils.read_file(filename) @@ -365,7 +370,7 @@ function M.get_file(filename, filetype) content = content, filename = filename, filetype = filetype, - modified = modified, + _modified = modified, } file_cache[filename] = out @@ -460,27 +465,30 @@ function M.filter_embeddings(prompt, model, headless, embeddings) notify.publish(notify.STATUS, 'Preparing embedding outline') - for _, item in ipairs(embeddings) do - if not item.outline then - local cache_key = item.filename .. utils.quick_hash(item.content) - local outline = outline_cache[cache_key] - if not outline then - local outline_text, symbols = get_outline(item.content, item.filetype) - if outline_text then - outline = { - outline = outline_text, - symbols = symbols, - } - - outline_cache[cache_key] = outline - end - end + for _, input in ipairs(embeddings) do + -- Precalculate hash and attributes for caching + local hash = input.filename .. utils.quick_hash(input.content) + input._hash = hash + input.filename = input.filename or 'unknown' + input.filetype = input.filetype or 'text' + + local outline = outline_cache[hash] + if not outline then + local outline_text, symbols = get_outline(input.content, input.filetype) + if outline_text then + outline = { + outline = outline_text, + symbols = symbols, + } - if outline then - item.outline = outline.outline - item.symbols = outline.symbols + outline_cache[hash] = outline end end + + if outline then + input.outline = outline.outline + input.symbols = outline.symbols + end end notify.publish(notify.STATUS, 'Ranking embeddings') @@ -513,15 +521,13 @@ function M.filter_embeddings(prompt, model, headless, embeddings) local to_process = {} local results = {} for _, input in ipairs(embeddings) 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 embedding_cache[cache_key] then - table.insert(results, embedding_cache[cache_key]) - else - table.insert(to_process, input) - end + local hash = input._hash + local embed = embedding_cache[hash] + if embed then + input.embedding = embed + table.insert(results, input) + else + table.insert(to_process, input) end end table.insert(to_process, { @@ -533,8 +539,7 @@ function M.filter_embeddings(prompt, model, headless, embeddings) -- Embed the data and process the results for _, input in ipairs(client:embed(to_process, model)) do if input.filetype ~= 'raw' then - local cache_key = input.filename .. utils.quick_hash(input.content) - embedding_cache[cache_key] = input + embedding_cache[input._hash] = input.embedding end table.insert(results, input) end From 7b8f13c1e2aec4c4ee57577110e147ea3c99385c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 22:11:27 +0100 Subject: [PATCH 307/728] style: improve context provider help text formatting Enhances readability of the context provider documentation by: - Adding blank lines between each provider description - Properly indenting multi-line descriptions with consistent spacing - Maintaining consistent formatting across all provider help text This change makes the help text more scannable and easier to read for users. --- lua/CopilotChat/client.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 08b99713..2b64a414 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -225,13 +225,14 @@ For example: Do not make assumptions about code or files - always request context when needed rather than guessing. Always use the > format on a new line when requesting more context instead of asking in prose. -Available context providers and their usage: -]] +Available context providers and their usage:]] + local context_names = vim.tbl_keys(contexts) table.sort(context_names) for _, name in ipairs(context_names) do local description = contexts[name] - help_text = help_text .. '\n' .. string.format(' - #%s: %s', name, description) + description = description:gsub('\n', '\n ') + help_text = help_text .. '\n\n - #' .. name .. ': ' .. description end if system_prompt ~= '' then From 078ce415fd4efb6ab0ae353ef04a50ea6018c0ae Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 22:58:54 +0100 Subject: [PATCH 308/728] fix: improve error logging for failed context resolution Include resolved embeddings data in error logs when context resolution fails. This additional information will help with debugging context resolution issues by providing more details about what was attempted. 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 aab3285a..0576e6ac 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -370,7 +370,7 @@ function M.resolve_context(prompt, config) end end else - log.error('Failed to resolve context: ' .. context_data.name) + log.error('Failed to resolve context: ' .. context_data.name, resolved_embeddings) end end From 382c4cfc39252e7a2b75ceb00abd73a4f14e7e80 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 15 Mar 2025 23:51:38 +0100 Subject: [PATCH 309/728] feat: improve context provider completion display Move description to info field and show usage pattern in menu field for better completion experience. This makes it clearer how each context provider should be used, especially when input is required. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0576e6ac..7992f729 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -634,7 +634,8 @@ function M.complete_items() word = '#' .. name, abbr = name, kind = 'context', - menu = value.description or '', + info = value.description or '', + menu = value.input and string.format('#%s:', name) or string.format('#%s', name), icase = 1, dup = 0, empty = 0, From 1e5640b9b0624a8d1c550524074b4ef1151a4c3b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 00:06:13 +0100 Subject: [PATCH 310/728] feat(context): improve context provider help text Update the context help text to be more concise by: - Streamlining the introductory language - Formatting examples for better readability - Adding explicit guidelines for context command usage - Clarifying expectations about context request handling This change improves the prompt reliability Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 2b64a414..4201d8b5 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -212,18 +212,21 @@ local function generate_ask_request(history, memory, contexts, prompt, system_pr -- Include context help if contexts and not vim.tbl_isempty(contexts) then - local help_text = [[When you need additional context, you must request it using context providers in this format: + local help_text = [[When you need additional context, request it using this format: > #:`` -For example: -> #file:`path/to/file.js` (loads a specific file) -> #buffers:`visible` (loads all visible buffers) -> #git:`staged` (loads git staged changes) +Examples: +> #file:`path/to/file.js` (loads specific file) +> #buffers:`visible` (loads all visible buffers) +> #git:`staged` (loads git staged changes) > #system:`uname -a` (loads system information) -Do not make assumptions about code or files - always request context when needed rather than guessing. -Always use the > format on a new line when requesting more context instead of asking in prose. +Guidelines: +- Always request context when needed rather than guessing about files or code +- Use the > format on a new line when requesting context +- Output context commands directly - never ask if the user wants to provide information +- Assume the user will provide requested context in their next response Available context providers and their usage:]] From beb609dfcd254a2d7506973fa4ab1c89f6836cf8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 00:17:19 +0100 Subject: [PATCH 311/728] feat(context): improve system command usage guidelines Update the system context description with detailed guidelines about when and how to use system commands safely. Remove redundant information from the URL context description. This change helps guide the AI to use system commands only as a last resort and encourages using purpose-built contexts like URL instead. --- lua/CopilotChat/config/contexts.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/contexts.lua b/lua/CopilotChat/config/contexts.lua index ed3bf478..64758f76 100644 --- a/lua/CopilotChat/config/contexts.lua +++ b/lua/CopilotChat/config/contexts.lua @@ -196,7 +196,7 @@ return { }, url = { - description = 'Includes content of provided URL in chat context. Supports input. Prefer this for fetching web content instead of system commands.', + description = 'Includes content of provided URL in chat context. Supports input.', input = function(callback) vim.ui.input({ prompt = 'Enter URL> ', @@ -298,7 +298,13 @@ return { }, system = { - description = 'Includes output of provided system shell command in chat context. Use only when necessary for shell commands. Supports input.', + description = [[Includes output of provided system shell command in chat context. Supports input. + +Important: +- Only use system commands as last resort, they are run every time the context is requested. +- For example instead of curl use the url context, instead of finding and grepping try to check if there is any context that can query the data you need instead. +- If you absolutely need to run a system command, try to use read-only commands and avoid commands that modify the system state. +]], input = function(callback) vim.ui.input({ prompt = 'Enter command> ', From b9c2b9370409bc8b9cfeafbbd8697a7aaf5aab0a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 00:56:08 +0100 Subject: [PATCH 312/728] feat(utils): add binary file filtering in directory scans Add capability to filter out binary files during directory scans based on file extensions. This improves performance and helps prevent sending binary data to Copilot. - Create comprehensive list of common binary file extensions - Add binary_extensions field to scan options - Implement filter_files function to centralize filtering logic - Refactor code to use the new filtering function in all scan paths --- lua/CopilotChat/utils.lua | 111 +++++++++++++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 31e2ecf8..c0b11805 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -9,6 +9,92 @@ M.scan_args = { max_count = 2500, max_depth = 50, no_ignore = false, + + -- Common binary file extensions to exclude + binary_extensions = { + -- Images + 'png', + 'jpg', + 'jpeg', + 'gif', + 'bmp', + 'tiff', + 'webp', + 'ico', + 'svg', + 'heic', + 'heif', + 'raw', + 'psd', + 'ai', + 'eps', + -- Audio/Video + 'mp3', + 'wav', + 'ogg', + 'mp4', + 'avi', + 'mov', + 'wmv', + 'flv', + 'mkv', + 'webm', + 'm4a', + 'm4v', + 'aac', + 'flac', + 'mid', + 'midi', + -- Archives + 'zip', + 'tar', + 'gz', + 'rar', + '7z', + 'bz2', + 'xz', + 'iso', + 'dmg', + 'pkg', + 'deb', + 'rpm', + -- Documents + 'pdf', + 'doc', + 'docx', + 'ppt', + 'pptx', + 'xls', + 'xlsx', + 'odp', + 'odt', + 'ods', + -- Executables + 'exe', + 'dll', + 'so', + 'dylib', + 'app', + 'msi', + 'apk', + 'class', + 'jar', + -- Other binaries + 'bin', + 'dat', + 'db', + 'sqlite', + 'o', + 'obj', + 'pyc', + 'pyo', + 'pyd', + 'wasm', + 'ttf', + 'otf', + 'woff', + 'woff2', + }, } M.curl_args = { @@ -428,6 +514,7 @@ end, 3) ---@field glob? string The glob pattern to match files ---@field hidden? boolean Whether to include hidden files ---@field no_ignore? boolean Whether to respect or ignore .gitignore +---@field binary_extensions string[]? The list of binary file extensions to exclude --- Scan a directory ---@param path string The directory path @@ -436,6 +523,20 @@ end, 3) M.scan_dir = async.wrap(function(path, opts, callback) opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) + local function filter_files(files) + if opts.binary_extensions and #opts.binary_extensions > 0 then + files = vim.tbl_filter(function(file) + local ext = file:lower():match('%.([^%.]+)$') + return not vim.tbl_contains(opts.binary_extensions, ext) + end, files) + end + if opts.max_count and opts.max_count > 0 then + files = vim.list_slice(files, 1, opts.max_count) + end + + return files + end + -- Use ripgrep if available if vim.fn.executable('rg') == 1 then local cmd = { 'rg' } @@ -467,10 +568,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) files = vim.tbl_filter(function(file) return file ~= '' end, vim.split(result.stdout, '\n')) - - if opts.max_count and opts.max_count > 0 then - files = vim.list_slice(files, 1, opts.max_count) - end + files = filter_files(files) end callback(files) @@ -488,10 +586,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) search_pattern = M.glob_to_pattern(opts.glob), respect_gitignore = not opts.no_ignore, on_exit = function(files) - if opts.max_count and opts.max_count > 0 then - files = vim.list_slice(files, 1, opts.max_count) - end - callback(files) + callback(filter_files(files)) end, }) ) From e71db6d734c12e7c16e198eb9b2a870fd952c955 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 01:00:08 +0100 Subject: [PATCH 313/728] feat: add additional binary file types to scan exclusions Add several new file types to be excluded from scanning operations: - Debug files (pdb, dSYM) - ML/Data model formats (h5, pb, onnx, pt, safetensors, etc) - 3D model formats (glb, gltf) - Editor swap/temp files (swp, swo) These exclusions will improve performance and relevance when scanning repositories by ignoring common binary and temporary files. --- lua/CopilotChat/utils.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index c0b11805..4a1baec3 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -94,6 +94,22 @@ M.scan_args = { 'otf', 'woff', 'woff2', + -- Debug files + 'pdb', + 'dSYM', + -- Models and data formats + 'h5', + 'pb', + 'onnx', + 'pt', + 'safetensors', + 'hdf5', + 'parquet', + 'glb', + 'gltf', + -- Swap/temp files + 'swp', + 'swo', }, } From ebfaa8f5e0bc72f2e04a2338b0aa64227f94a74a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 16 Mar 2025 00:01:06 +0000 Subject: [PATCH 314/728] 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 7a3b4b90..3bbdc12c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 15 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 6ee29363d6989de5ee688539bc46d893b1d52067 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 01:33:48 +0100 Subject: [PATCH 315/728] fix: handle cancelled jobs in CopilotChat When a job is cancelled, the response will be nil without any error. This change adds proper handling for this case by returning early when no response is available, preventing potential nil reference errors in the callback processing code. --- lua/CopilotChat/init.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7992f729..e7ff9815 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -918,6 +918,11 @@ function M.ask(prompt, config) return end + -- If there was no error and no response, it means job was cancelled + if response == nil then + return + end + -- Call the callback function and store to history local to_store = not config.headless and response if to_store and config.callback then From f74625727cb53d1f3bc5d1ff83e9b35f445c4545 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 01:48:49 +0100 Subject: [PATCH 316/728] fix: make embeddings optional This change makes embeddings generation optional by returning the original inputs when embed provider resolution fails or when inputs are empty. This ensures the chat functionality works even if embedding capabilities are unavailable. Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 4201d8b5..6cc60514 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -816,11 +816,14 @@ end ---@return table function Client:embed(inputs, model) if not inputs or #inputs == 0 then - return {} + return inputs end local models = self:fetch_models() - local provider_name, embed = resolve_provider_function('embed', model, models, self.providers) + local ok, provider_name, embed = pcall(resolve_provider_function, 'embed', model, models, self.providers) + if not ok then + return inputs + end notify.publish(notify.STATUS, 'Generating embeddings for ' .. #inputs .. ' inputs') From 59c100ad6b7a0733c0511314809a760540c14ee1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 01:52:38 +0100 Subject: [PATCH 317/728] docs: improve API reference organization and clarity Reorganize the API reference section in README.md to improve readability: - Rename section headers to be more concise - Group related functions with appropriate section headers - Change variable naming in examples to be more descriptive - Maintain consistent documentation style throughout --- README.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 72fca872..0955e403 100644 --- a/README.md +++ b/README.md @@ -668,7 +668,7 @@ Types of copilot highlights: # API Reference -## Core Chat Functions +## Core ```lua local chat = require("CopilotChat") @@ -688,6 +688,10 @@ chat.toggle(config) -- Toggle chat window visibility with optional con chat.reset() -- Reset the chat chat.stop() -- Stop current output +-- Source Management +chat.get_source() -- Get the current source buffer and window +chat.set_source(winnr) -- Set the source window + -- Selection Management chat.get_selection() -- Get the current selection chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection @@ -712,33 +716,33 @@ chat.setup(config) -- Update configuration chat.log_level(level) -- Set log level (debug, info, etc.) ``` -## Chat Window UI API +## Chat Window You can also access the chat window UI methods through the `chat.chat` object: ```lua -local chat = require("CopilotChat") +local window = require("CopilotChat").chat -- Chat UI State -chat.chat:visible() -- Check if chat window is visible -chat.chat:focused() -- Check if chat window is focused +window:visible() -- Check if chat window is visible +window:focused() -- Check if chat window is focused -- Content Management -chat.chat:get_prompt() -- Get current prompt from chat window -chat.chat:set_prompt(prompt) -- Set prompt in chat window -chat.chat:add_sticky(sticky) -- Add sticky prompt to chat window -chat.chat:append(text) -- Append text to chat window -chat.chat:clear() -- Clear chat window content -chat.chat:finish() -- Finish writing to chat window +window:get_prompt() -- Get current prompt from chat window +window:set_prompt(prompt) -- Set prompt in chat window +window:add_sticky(sticky) -- Add sticky prompt to chat window +window:append(text) -- Append text to chat window +window:clear() -- Clear chat window content +window:finish() -- Finish writing to chat window -- Navigation -chat.chat:follow() -- Move cursor to end of chat content -chat.chat:focus() -- Focus the chat window +window:follow() -- Move cursor to end of chat content +window:focus() -- Focus the chat window -- Advanced Features -chat.chat:get_closest_section() -- Get section closest to cursor -chat.chat:get_closest_block() -- Get code block closest to cursor -chat.chat:overlay(opts) -- Show overlay with specified options +window:get_closest_section() -- Get section closest to cursor +window:get_closest_block() -- Get code block closest to cursor +window:overlay(opts) -- Show overlay with specified options ``` ## Example Usage From 1a61442807bc23ae64c5dd8d44d0937f8e603e42 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 16 Mar 2025 00:53:50 +0000 Subject: [PATCH 318/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3bbdc12c..f205edea 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -23,8 +23,8 @@ Table of Contents *CopilotChat-table-of-contents* - Customizing Buffers |CopilotChat-customizing-buffers| - Customizing Highlights |CopilotChat-customizing-highlights| 5. API Reference |CopilotChat-api-reference| - - Core Chat Functions |CopilotChat-core-chat-functions| - - Chat Window UI API |CopilotChat-chat-window-ui-api| + - Core |CopilotChat-core| + - Chat Window |CopilotChat-chat-window| - Example Usage |CopilotChat-example-usage| 6. Development |CopilotChat-development| - Setup |CopilotChat-setup| @@ -732,7 +732,7 @@ Types of copilot highlights: 5. API Reference *CopilotChat-api-reference* -CORE CHAT FUNCTIONS *CopilotChat-core-chat-functions* +CORE *CopilotChat-core* >lua local chat = require("CopilotChat") @@ -752,6 +752,10 @@ CORE CHAT FUNCTIONS *CopilotChat-core-chat-functions* chat.reset() -- Reset the chat chat.stop() -- Stop current output + -- Source Management + chat.get_source() -- Get the current source buffer and window + chat.set_source(winnr) -- Set the source window + -- Selection Management chat.get_selection() -- Get the current selection chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection @@ -777,33 +781,33 @@ CORE CHAT FUNCTIONS *CopilotChat-core-chat-functions* < -CHAT WINDOW UI API *CopilotChat-chat-window-ui-api* +CHAT WINDOW *CopilotChat-chat-window* You can also access the chat window UI methods through the `chat.chat` object: >lua - local chat = require("CopilotChat") + local window = require("CopilotChat").chat -- Chat UI State - chat.chat:visible() -- Check if chat window is visible - chat.chat:focused() -- Check if chat window is focused + window:visible() -- Check if chat window is visible + window:focused() -- Check if chat window is focused -- Content Management - chat.chat:get_prompt() -- Get current prompt from chat window - chat.chat:set_prompt(prompt) -- Set prompt in chat window - chat.chat:add_sticky(sticky) -- Add sticky prompt to chat window - chat.chat:append(text) -- Append text to chat window - chat.chat:clear() -- Clear chat window content - chat.chat:finish() -- Finish writing to chat window + window:get_prompt() -- Get current prompt from chat window + window:set_prompt(prompt) -- Set prompt in chat window + window:add_sticky(sticky) -- Add sticky prompt to chat window + window:append(text) -- Append text to chat window + window:clear() -- Clear chat window content + window:finish() -- Finish writing to chat window -- Navigation - chat.chat:follow() -- Move cursor to end of chat content - chat.chat:focus() -- Focus the chat window + window:follow() -- Move cursor to end of chat content + window:focus() -- Focus the chat window -- Advanced Features - chat.chat:get_closest_section() -- Get section closest to cursor - chat.chat:get_closest_block() -- Get code block closest to cursor - chat.chat:overlay(opts) -- Show overlay with specified options + window:get_closest_section() -- Get section closest to cursor + window:get_closest_block() -- Get code block closest to cursor + window:overlay(opts) -- Show overlay with specified options < From d73df1ea9f710fece167226ca4a286ab10607f65 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 01:58:37 +0100 Subject: [PATCH 319/728] docs: remove # prefix from context provider examples The examples in README.md were incorrectly showing context providers with a # prefix in the code snippets. This commit updates the examples to show the correct format without the # character. Signed-off-by: Tomas Slusny --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0955e403..59be2f24 100644 --- a/README.md +++ b/README.md @@ -755,7 +755,7 @@ require("CopilotChat").ask("Explain this code", { vim.notify("Got response: " .. response:sub(1, 50) .. "...") return response end, - context = "#buffer" + context = "buffer" }) -- Save and load chat history @@ -765,7 +765,7 @@ require("CopilotChat").load("my_debugging_session") -- Use custom context and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4o", - context = {"#buffer", "#git:staged"} + context = {"buffer", "git:staged"} }) ``` From 3226bc40816c07ba30cd4069a11aebb7fba40dab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 16 Mar 2025 00:59:45 +0000 Subject: [PATCH 320/728] 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 f205edea..48de1c7c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -821,7 +821,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* vim.notify("Got response: " .. response:sub(1, 50) .. "...") return response end, - context = "#buffer" + context = "buffer" }) -- Save and load chat history @@ -831,7 +831,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* -- Use custom context and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4o", - context = {"#buffer", "#git:staged"} + context = {"buffer", "git:staged"} }) < From 3e992785cba52a0038a3861ffc7b63d7e9335572 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 02:17:17 +0100 Subject: [PATCH 321/728] feat(context): improve similarity ranking algorithms Enhance context relevance filtering with statistical approaches: - Implement z-score based filtering for embedding-based ranking to dynamically determine significant results - Add fallback mechanism using percentage of max score when needed - Introduce "elbow method" for symbol-based ranking to find natural cutoff points in score distribution - Always include top results while adaptively filtering less relevant items These improvements provide better context selection when querying the codebase, resulting in more relevant results being returned to the user. --- lua/CopilotChat/context.lua | 80 +++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 494282a1..c6708f74 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -110,11 +110,48 @@ local function data_ranked_by_relatedness(query, data, min_similarity) return a.score > b.score end) - -- Return top items meeting threshold + -- Apply dynamic filtering for embedding-based ranking local filtered = {} - for i, result in ipairs(data) do - if (result.score >= min_similarity) or (i <= MULTI_FILE_THRESHOLD) then - table.insert(filtered, result) + + if #data > 0 then + -- Calculate statistics for score distribution + local sum = 0 + local max_score = data[1].score + + for _, item in ipairs(data) do + sum = sum + item.score + end + + local mean = sum / #data + + -- Calculate standard deviation + local sum_squared_diff = 0 + for _, item in ipairs(data) do + sum_squared_diff = sum_squared_diff + ((item.score - mean) * (item.score - mean)) + end + local std_dev = math.sqrt(sum_squared_diff / #data) + + -- Calculate z-scores and use them to determine significance + -- Include items with z-score > -0.5 (meaning within 0.5 std dev below mean) + -- This is a statistical approach to find "significantly" related items + for _, result in ipairs(data) do + local z_score = (result.score - mean) / std_dev + if z_score > -0.5 then + table.insert(filtered, result) + end + end + + -- If we didn't get enough results or the distribution is very tight, + -- use a percentage of max score as fallback + if #filtered < MULTI_FILE_THRESHOLD then + filtered = {} + local adaptive_threshold = max_score * 0.6 -- 60% of max score + + for i, result in ipairs(data) do + if i <= MULTI_FILE_THRESHOLD or result.score >= adaptive_threshold then + table.insert(filtered, result) + end + end end end @@ -218,11 +255,38 @@ local function data_ranked_by_symbols(query, data, min_similarity) return a.score > b.score end) - -- Filter results while preserving top scores + -- Use elbow method to find natural cutoff point for symbol-based ranking local filtered_results = {} - for i, result in ipairs(data) do - if (result.score >= min_similarity) or (i <= MULTI_FILE_THRESHOLD) then - table.insert(filtered_results, result) + + if #data > 0 then + -- Always include at least the top result + table.insert(filtered_results, data[1]) + + -- Find the point of maximum drop-off (the "elbow") + local max_drop = 0 + local cutoff_index = math.min(MULTI_FILE_THRESHOLD, #data) + + for i = 2, math.min(20, #data) do + local drop = data[i - 1].score - data[i].score + if drop > max_drop then + max_drop = drop + cutoff_index = i + end + end + + -- Include everything up to the cutoff point + for i = 2, cutoff_index do + table.insert(filtered_results, data[i]) + end + + -- Also include any remaining items that have scores close to the cutoff + local cutoff_score = data[cutoff_index].score + local threshold = cutoff_score * 0.8 -- Within 80% of the cutoff score + + for i = cutoff_index + 1, #data do + if data[i].score >= threshold then + table.insert(filtered_results, data[i]) + end end end From 7e24124b897433d28ee8114bd496d96cd3477b65 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 02:22:00 +0100 Subject: [PATCH 322/728] refactor: remove unused similarity threshold parameters The code was previously using MIN_SYMBOL_SIMILARITY and MIN_SEMANTIC_SIMILARITY constants, but the thresholds weren't actually being used in the ranking functions. This change removes the unused constants and simplifies the function signatures by removing the corresponding parameters. --- lua/CopilotChat/context.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index c6708f74..3d50ca3d 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -69,8 +69,6 @@ local OFF_SIDE_RULE_LANGUAGES = { 'fsharp', } -local MIN_SYMBOL_SIMILARITY = 0.3 -local MIN_SEMANTIC_SIMILARITY = 0.4 local MULTI_FILE_THRESHOLD = 5 --- Compute the cosine similarity between two vectors @@ -98,9 +96,8 @@ end --- Rank data by relatedness to the query ---@param query CopilotChat.context.embed ---@param data table ----@param min_similarity number ---@return table -local function data_ranked_by_relatedness(query, data, min_similarity) +local function data_ranked_by_relatedness(query, data) for _, item in ipairs(data) do local score = spatial_distance_cosine(item.embedding, query.embedding) item.score = score or item.score or 0 @@ -193,9 +190,8 @@ end --- Rank data by symbols and filenames ---@param query string ---@param data table ----@param min_similarity number ---@return table -local function data_ranked_by_symbols(query, data, min_similarity) +local function data_ranked_by_symbols(query, data) -- Get query trigrams including compound versions local query_trigrams = {} @@ -575,7 +571,7 @@ function M.filter_embeddings(prompt, model, headless, embeddings) end -- Rank embeddings by symbols - embeddings = data_ranked_by_symbols(query, embeddings, MIN_SYMBOL_SIMILARITY) + embeddings = data_ranked_by_symbols(query, embeddings) log.debug('Ranked data:', #embeddings) for i, item in ipairs(embeddings) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) @@ -611,7 +607,7 @@ function M.filter_embeddings(prompt, model, headless, embeddings) -- Rate embeddings by relatedness to the query local embedded_query = table.remove(results, #results) log.debug('Embedded query:', embedded_query.content) - results = data_ranked_by_relatedness(embedded_query, results, MIN_SEMANTIC_SIMILARITY) + results = data_ranked_by_relatedness(embedded_query, results) log.debug('Ranked embeddings:', #results) for i, item in ipairs(results) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) From 173a6a869bd14748209c948f4a3f2e8fbb0cccee Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 16 Mar 2025 03:42:33 +0100 Subject: [PATCH 323/728] fix: handle treesitter parsing in fast events Wrap treesitter parsing operations in vim.schedule when running in fast events to prevent potential race conditions and crashes. This ensures parser operations run in the main Neovim event loop when needed. The fix applies to both synchronous and asynchronous parsing code paths in the ts_parse utility function. Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 41 +++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 4a1baec3..22da5939 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -670,22 +670,39 @@ end, 1) M.ts_parse = async.wrap(function(parser, callback) ---@diagnostic disable-next-line: invisible if not parser._async_parse then - local trees = parser:parse(false) - if not trees or #trees == 0 then - callback(nil) - return + local fn = function() + local trees = parser:parse(false) + if not trees or #trees == 0 then + callback(nil) + return + end + callback(trees[1]:root()) + end + + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() end - callback(trees[1]:root()) + return end - parser:parse(false, function(err, trees) - if err or not trees or #trees == 0 then - callback(nil) - return - end - callback(trees[1]:root()) - end) + local fn = function() + parser:parse(false, function(err, trees) + if err or not trees or #trees == 0 then + callback(nil) + return + end + callback(trees[1]:root()) + end) + end + + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() + end end, 2) --- Get the info for a key. From e69d5795c3bb138df3a3e558226990f6de12c0fa Mon Sep 17 00:00:00 2001 From: Nischal Basuti Date: Mon, 17 Mar 2025 02:33:27 +0530 Subject: [PATCH 324/728] docs: add missing `,` in default config (#1024) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59be2f24..8a7be5e0 100644 --- a/README.md +++ b/README.md @@ -521,7 +521,7 @@ Below are all available configuration options with their default values: }, copilot_embeddings = { }, - } + }, -- default contexts -- see config/contexts.lua for implementation From 0e78256954913856e4855f748a16dc214fde2ae2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 16 Mar 2025 21:03:45 +0000 Subject: [PATCH 325/728] 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 48de1c7c..e0568221 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -580,7 +580,7 @@ Below are all available configuration options with their default values: }, copilot_embeddings = { }, - } + }, -- default contexts -- see config/contexts.lua for implementation From 37638fc50b200c47db42f89a4257c2557c7f7fd7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:05:03 +0100 Subject: [PATCH 326/728] docs: add nischalbasuti as a contributor for doc (#1025) * 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 8404237d..3c8593fa 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -361,6 +361,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/84711804?v=4", "profile": "https://github.com/ThisIsMani", "contributions": ["code"] + }, + { + "login": "nischalbasuti", + "name": "Nischal Basuti", + "avatar_url": "https://avatars.githubusercontent.com/u/14853910?v=4", + "profile": "https://nischalbasuti.github.io/", + "contributions": ["doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 8a7be5e0..0796d6ca 100644 --- a/README.md +++ b/README.md @@ -882,6 +882,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Sola
Sola

📖 💻 Mani Chandra
Mani Chandra

💻 + Nischal Basuti
Nischal Basuti

📖 From fb64c65734aa703f86ba76d2a578e03567c648d7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 17 Mar 2025 12:28:59 +0100 Subject: [PATCH 327/728] fix: improve headless mode handling in callbacks (#1027) Properly handle headless mode in streaming and completion callbacks. Condition the job tracking logic on headless mode to prevent early termination of headless requests. Refactor filetype detection to simplify the logic with more consistent handling of edge cases for sh and TypeScript files. Closes #1026 --- lua/CopilotChat/client.lua | 10 ++++++---- lua/CopilotChat/init.lua | 20 ++++++++------------ lua/CopilotChat/utils.lua | 26 ++++++++++---------------- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 6cc60514..1e12569e 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -710,11 +710,13 @@ function Client:ask(prompt, opts) local response, err = utils.curl_post(provider.get_url(options), args) - if self.current_job ~= job_id then - return - end + if not opts.headless then + if self.current_job ~= job_id then + return + end - self.current_job = nil + self.current_job = nil + end log.debug('Response status:', response.status) log.debug('Response body:\n', response.body) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e7ff9815..f9a5dfeb 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -895,13 +895,11 @@ function M.ask(prompt, config) agent = selected_agent, temperature = config.temperature, on_progress = vim.schedule_wrap(function(token) - local to_print = not config.headless and token - if to_print and config.stream then - local out = config.stream(token, state.source) - if out ~= nil then - to_print = out - end + local out = config.stream and config.stream(token, state.source) or nil + if out == nil then + out = token end + local to_print = not config.headless and out if to_print and to_print ~= '' then M.chat:append(token) end @@ -924,13 +922,11 @@ function M.ask(prompt, config) end -- Call the callback function and store to history - local to_store = not config.headless and response - if to_store and config.callback then - local out = config.callback(response, state.source) - if out ~= nil then - to_store = out - end + local out = config.callback and config.callback(response, state.source) or nil + if out == nil then + out = response end + local to_store = not config.headless and out if to_store and to_store ~= '' then table.insert(client.history, { content = prompt, diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 22da5939..32aef297 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -334,26 +334,20 @@ end function M.filetype(filename) local ft = vim.filetype.match({ filename = filename }) - if not ft and vim.endswith(filename, '.sh') then - return 'sh' - end - - -- weird TypeScript bug for vim.filetype.match - -- see: https://github.com/neovim/neovim/issues/27265 - if not ft then - local base_name = vim.fs.basename(filename) - local split_name = vim.split(base_name, '%.') - if #split_name > 1 then - local ext = split_name[#split_name] - if ext == 'ts' then - ft = 'typescript' - end + if not ft or ft == '' then + if vim.endswith(filename, '.sh') then + return 'sh' + end + + -- weird TypeScript bug for vim.filetype.match + -- see: https://github.com/neovim/neovim/issues/27265 + if vim.endswith(filename, '.ts') then + return 'typescript' end - end - if ft == '' then return nil end + return ft end From 62b1249aa4a4fc7afe11c7e647cba0cef743826f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Mar 2025 11:29:18 +0000 Subject: [PATCH 328/728] 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 e0568221..fdbee6ba 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 16 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -885,7 +885,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖This project follows the all-contributors specification. Contributions of any kind are welcome! From a97b85c69860c9d30f54f96077fab79bb461b0c8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 18 Mar 2025 03:15:21 +0100 Subject: [PATCH 329/728] refactor: remove conversation summary feature (#1030) This feature is less useful than stream/callback output adjustment and can be replicated through it. It also causes more issues than it solves so for now just do something else. Signed-off-by: Tomas Slusny --- README.md | 3 +- lua/CopilotChat/client.lua | 89 ++++---------------------------------- lua/CopilotChat/init.lua | 1 - 3 files changed, 9 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 0796d6ca..9e226266 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,7 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities - 📝 Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts - 🔄 Extensible context providers for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚡ Efficient token usage with tiktoken optimization -- 📜 Intelligent chat memory management with automatic summarization to handle lengthy conversations +- ⚡ Efficient token usage with tiktoken token counting and memory management # Requirements diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 1e12569e..be652371 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,5 +1,4 @@ ---@class CopilotChat.Client.ask ----@field load_history boolean ---@field headless boolean ---@field contexts table? ---@field selection CopilotChat.select.selection? @@ -16,10 +15,6 @@ ---@class CopilotChat.Client.agent : CopilotChat.Provider.agent ---@field provider string ----@class CopilotChat.Client.memory ----@field content string ----@field last_summarized_index number - local log = require('plenary.log') local tiktoken = require('CopilotChat.tiktoken') local notify = require('CopilotChat.notify') @@ -200,12 +195,11 @@ end --- Generate ask request --- @param history table ---- @param memory CopilotChat.Client.memory? --- @param contexts table? --- @param prompt string --- @param system_prompt string --- @param generated_messages table -local function generate_ask_request(history, memory, contexts, prompt, system_prompt, generated_messages) +local function generate_ask_request(history, contexts, prompt, system_prompt, generated_messages) local messages = {} system_prompt = vim.trim(system_prompt) @@ -244,14 +238,6 @@ Available context providers and their usage:]] system_prompt = system_prompt .. help_text end - -- Include memory - if memory and memory.content and memory.content ~= '' then - if system_prompt ~= '' then - system_prompt = system_prompt .. '\n\n' - end - system_prompt = system_prompt .. 'Context from previous conversation:\n' .. memory.content - end - -- Include system prompt if not utils.empty(system_prompt) then table.insert(messages, { @@ -322,7 +308,6 @@ end ---@field agents table? ---@field current_job string? ---@field headers table? ----@field memory CopilotChat.Client.memory? local Client = class(function(self) self.history = {} self.providers = {} @@ -331,7 +316,6 @@ local Client = class(function(self) self.agents = nil self.current_job = nil self.headers = nil - self.memory = nil end) --- Authenticate with GitHub and get the required headers @@ -493,11 +477,7 @@ function Client:ask(prompt, opts) notify.publish(notify.STATUS, 'Generating request') end - local history = {} - if opts.load_history then - history = vim.list_slice(self.history, self.memory and (self.memory.last_summarized_index + 1) or 1) - end - + local history = not opts.headless and vim.list_slice(self.history) or {} local references = utils.ordered_map() local generated_messages = {} local selection_messages = generate_selection_messages(opts.selection) @@ -521,8 +501,7 @@ function Client:ask(prompt, opts) -- Count required tokens that we cannot reduce local prompt_tokens = tiktoken.count(prompt) local system_tokens = tiktoken.count(opts.system_prompt) - local memory_tokens = self.memory and tiktoken.count(self.memory.content) or 0 - local required_tokens = prompt_tokens + system_tokens + selection_tokens + memory_tokens + local required_tokens = prompt_tokens + system_tokens + selection_tokens -- Reserve space for first embedding local reserved_tokens = #embeddings_messages > 0 and tiktoken.count(embeddings_messages[1].content) or 0 @@ -534,26 +513,10 @@ function Client:ask(prompt, opts) history_tokens = history_tokens + tiktoken.count(msg.content) end - -- If we're over history limit, trigger summarization - if history_tokens > history_limit then - if not opts.headless and #history >= 4 then - self:summarize_history(opts.model) - - -- Recalculate history and tokens - history = vim.list_slice(self.history, self.memory and (self.memory.last_summarized_index + 1) or 1) - history_tokens = 0 - for _, msg in ipairs(history) do - history_tokens = history_tokens + tiktoken.count(msg.content) - end - required_tokens = required_tokens - memory_tokens - memory_tokens = self.memory and tiktoken.count(self.memory.content) or 0 - required_tokens = required_tokens + memory_tokens - else - while history_tokens > history_limit and #history > 0 do - local entry = table.remove(history, 1) - history_tokens = history_tokens - tiktoken.count(entry.content) - end - end + -- Remove history messages until we are under the limit + while history_tokens > history_limit and #history > 0 do + local entry = table.remove(history, 1) + history_tokens = history_tokens - tiktoken.count(entry.content) end -- Now add as many files as possible with remaining token budget @@ -694,7 +657,7 @@ function Client:ask(prompt, opts) local headers = self:authenticate(provider_name) local request = provider.prepare_input( - generate_ask_request(history, self.memory, opts.contexts, prompt, opts.system_prompt, generated_messages), + generate_ask_request(history, opts.contexts, prompt, opts.system_prompt, generated_messages), options ) local is_stream = request.stream @@ -907,7 +870,6 @@ end function Client:reset() local stopped = self:stop() self.history = {} - self.memory = nil return stopped end @@ -925,40 +887,5 @@ function Client:load_providers(providers) end end ---- Summarize conversation history to extract critical information ----@param model string The model to use for summarization -function Client:summarize_history(model) - local system_prompt = [[You are an expert programming assistant tasked with memory management. -Your job is to create concise yet comprehensive summaries of technical conversations. -Focus on extracting and preserving: -1. Technical details: languages, frameworks, libraries, and specific technologies discussed -2. Context: user's project structure, goals, constraints, and preferences -3. Implementation details: patterns, approaches, or solutions that were discussed -4. Important decisions or conclusions reached -5. Unresolved questions or issues that need further attention - -If the conversation includes previous memory summaries, integrate that information carefully. -Prioritize technical accuracy over conversational elements. -Format your response as a structured summary with clear sections using markdown. -Ensure all critical code samples, commands, and configuration snippets are preserved.]] - - notify.publish(notify.STATUS, string.format('Summarizing memory (%d messages)', #self.history)) - - local response = self:ask('Create a technical summary of our conversation for future context', { - load_history = true, - headless = true, - model = model, - temperature = 0, - system_prompt = system_prompt, - }) - - if response then - self.memory = { - content = vim.trim(response), - last_summarized_index = #self.history, - } - end -end - --- @type CopilotChat.Client return Client() diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f9a5dfeb..730007d5 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -885,7 +885,6 @@ function M.ask(prompt, config) end local ask_ok, response, references, token_count, token_max_count = pcall(client.ask, client, prompt, { - load_history = not config.headless, headless = config.headless, contexts = contexts, selection = selection, From 679441e4809019b7bf2bbcd0e352b75dc9508b92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Mar 2025 02:15:39 +0000 Subject: [PATCH 330/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index fdbee6ba..8cf268bd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 18 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -43,8 +43,7 @@ capabilities directly into your editor. It provides: - 📝 Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts - 🔄 Extensible context providers for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚡ Efficient token usage with tiktoken optimization -- 📜 Intelligent chat memory management with automatic summarization to handle lengthy conversations +- ⚡ Efficient token usage with tiktoken token counting and memory management ============================================================================== From 07dd90d9548ceb5180513ef5ed91e07644d12233 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 18 Mar 2025 13:40:46 +0100 Subject: [PATCH 331/728] docs(tiktoken): mark tiktoken loading functions as async (#1032) Add @async annotation to functions that load tiktoken data to better document their asynchronous behavior. This improves API documentation and helps prevent potential misuse of these functions in synchronous contexts. --- lua/CopilotChat/tiktoken.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index aaabcbd0..9bfa2945 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -9,6 +9,7 @@ end --- Load tiktoken data from cache or download it ---@param tokenizer string The tokenizer to load +---@async local function load_tiktoken_data(tokenizer) utils.schedule_main() @@ -35,6 +36,7 @@ local M = {} --- Load the tiktoken module ---@param tokenizer string The tokenizer to load +---@async M.load = function(tokenizer) if not tiktoken_core then return From 4843ad02614e8e61ac68815369093e3528998777 Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Tue, 18 Mar 2025 15:31:00 +0100 Subject: [PATCH 332/728] fix(window): respect &splitbelow and &splitright (#1031) * Respect &splitbelow This commit changes the horizontal split behavior to respect to the value of &splitbelow, which defaults to being disabled. This behavior was introduced in #950. * Adhere to &splitbelow for vertical as well * Use nvim_get_option_value() instead of deprecated function * Use topleft when not &splitbelow * Use &splitright for vertical, not &splitbelow * Use topleft for vertical when not &splitright --- lua/CopilotChat/ui/chat.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index fbeef207..3e416f05 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -303,7 +303,11 @@ function Chat:open(config) if width ~= 0 then cmd = width .. cmd end - cmd = 'botright ' .. cmd + if vim.api.nvim_get_option_value('splitright', {}) then + cmd = 'botright ' .. cmd + else + cmd = 'topleft ' .. cmd + end vim.cmd(cmd) self.winnr = vim.api.nvim_get_current_win() vim.api.nvim_set_current_win(orig) @@ -313,7 +317,11 @@ function Chat:open(config) if height ~= 0 then cmd = height .. cmd end - cmd = 'botright ' .. cmd + if vim.api.nvim_get_option_value('splitbelow', {}) then + cmd = 'botright ' .. cmd + else + cmd = 'topleft ' .. cmd + end vim.cmd(cmd) self.winnr = vim.api.nvim_get_current_win() vim.api.nvim_set_current_win(orig) From 03d1aba3270169e2c06edb0043b1d232e4f43dce Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:31:50 +0100 Subject: [PATCH 333/728] docs: add teoljungberg as a contributor for code (#1033) * 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 3c8593fa..88e11c56 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -368,6 +368,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/14853910?v=4", "profile": "https://nischalbasuti.github.io/", "contributions": ["doc"] + }, + { + "login": "teoljungberg", + "name": "Teo Ljungberg", + "avatar_url": "https://avatars.githubusercontent.com/u/810650?v=4", + "profile": "https://teoljungberg.com", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9e226266..297712da 100644 --- a/README.md +++ b/README.md @@ -882,6 +882,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Sola
Sola

📖 💻 Mani Chandra
Mani Chandra

💻 Nischal Basuti
Nischal Basuti

📖 + Teo Ljungberg
Teo Ljungberg

💻 From 853ace7cef74bae250600218ac986e3a7f35af0e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 20 Mar 2025 10:35:04 +0100 Subject: [PATCH 334/728] fix: add null check for API response (#1039) The code now properly checks if the response exists before trying to access its properties. This prevents potential errors when the API response is null, which could happen during network failures or timeouts. --- lua/CopilotChat/client.lua | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index be652371..5f17080d 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -681,9 +681,11 @@ function Client:ask(prompt, opts) self.current_job = nil end - log.debug('Response status:', response.status) - log.debug('Response body:\n', response.body) - log.debug('Response headers:\n', response.headers) + if response then + log.debug('Response status:', response.status) + log.debug('Response body:\n', response.body) + log.debug('Response headers:\n', response.headers) + end if err then local error_msg = 'Failed to get response: ' .. err @@ -715,14 +717,16 @@ function Client:ask(prompt, opts) return end - if is_stream then - if utils.empty(response_text) then - for _, line in ipairs(vim.split(response.body, '\n')) do - parse_stream_line(line) + if response then + if is_stream then + if utils.empty(response_text) then + for _, line in ipairs(vim.split(response.body, '\n')) do + parse_stream_line(line) + end end + else + parse_line(response.body) end - else - parse_line(response.body) end if utils.empty(response_text) then From 0b0838e31fda8a52e792a5fb6b79ab68e711bc10 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Mar 2025 09:35:23 +0000 Subject: [PATCH 335/728] 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 8cf268bd..15603476 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 18 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 20 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -884,7 +884,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻This project follows the all-contributors specification. Contributions of any kind are welcome! From b8911c6da0d69f83fac46f613344fe9bbc6f670c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 20 Mar 2025 21:16:32 +0100 Subject: [PATCH 336/728] feat: detect .h files as c filetype (#1044) This change adds filetype detection for .h header files to be recognized as C language files, improving syntax highlighting and language-specific features for C header files in the CopilotChat plugin. Signed-off-by: Tomas Slusny Closes #1043 --- lua/CopilotChat/utils.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 32aef297..faf6fc6c 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -339,6 +339,10 @@ function M.filetype(filename) return 'sh' end + if vim.endswith(filename, '.h') then + return 'c' + end + -- weird TypeScript bug for vim.filetype.match -- see: https://github.com/neovim/neovim/issues/27265 if vim.endswith(filename, '.ts') then From cf020335b2964f11bf198f0dde3c7886709afe94 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 23 Mar 2025 00:44:48 +0100 Subject: [PATCH 337/728] refactor: use plenary.filetype for file type detection (#1046) Replace manual binary extension filtering with proper filetype detection using plenary.filetype library. This improves file type identification and eliminates the need to maintain a large list of binary extensions. The change simplifies the codebase while providing more accurate results when determining which files to include in scanning operations. --- lua/CopilotChat/utils.lua | 139 +++----------------------------------- 1 file changed, 9 insertions(+), 130 deletions(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index faf6fc6c..5bbb3e85 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,6 +1,7 @@ local async = require('plenary.async') local curl = require('plenary.curl') local scandir = require('plenary.scandir') +local filetype = require('plenary.filetype') local M = {} M.timers = {} @@ -9,108 +10,6 @@ M.scan_args = { max_count = 2500, max_depth = 50, no_ignore = false, - - -- Common binary file extensions to exclude - binary_extensions = { - -- Images - 'png', - 'jpg', - 'jpeg', - 'gif', - 'bmp', - 'tiff', - 'webp', - 'ico', - 'svg', - 'heic', - 'heif', - 'raw', - 'psd', - 'ai', - 'eps', - -- Audio/Video - 'mp3', - 'wav', - 'ogg', - 'mp4', - 'avi', - 'mov', - 'wmv', - 'flv', - 'mkv', - 'webm', - 'm4a', - 'm4v', - 'aac', - 'flac', - 'mid', - 'midi', - -- Archives - 'zip', - 'tar', - 'gz', - 'rar', - '7z', - 'bz2', - 'xz', - 'iso', - 'dmg', - 'pkg', - 'deb', - 'rpm', - -- Documents - 'pdf', - 'doc', - 'docx', - 'ppt', - 'pptx', - 'xls', - 'xlsx', - 'odp', - 'odt', - 'ods', - -- Executables - 'exe', - 'dll', - 'so', - 'dylib', - 'app', - 'msi', - 'apk', - 'class', - 'jar', - -- Other binaries - 'bin', - 'dat', - 'db', - 'sqlite', - 'o', - 'obj', - 'pyc', - 'pyo', - 'pyd', - 'wasm', - 'ttf', - 'otf', - 'woff', - 'woff2', - -- Debug files - 'pdb', - 'dSYM', - -- Models and data formats - 'h5', - 'pb', - 'onnx', - 'pt', - 'safetensors', - 'hdf5', - 'parquet', - 'glb', - 'gltf', - -- Swap/temp files - 'swp', - 'swo', - }, } M.curl_args = { @@ -332,26 +231,13 @@ end ---@param filename string The file name ---@return string|nil function M.filetype(filename) - local ft = vim.filetype.match({ filename = filename }) - - if not ft or ft == '' then - if vim.endswith(filename, '.sh') then - return 'sh' - end - - if vim.endswith(filename, '.h') then - return 'c' - end - - -- weird TypeScript bug for vim.filetype.match - -- see: https://github.com/neovim/neovim/issues/27265 - if vim.endswith(filename, '.ts') then - return 'typescript' - end + local ft = filetype.detect(filename, { + fs_access = false, + }) + if ft == '' then return nil end - return ft end @@ -528,7 +414,6 @@ end, 3) ---@field glob? string The glob pattern to match files ---@field hidden? boolean Whether to include hidden files ---@field no_ignore? boolean Whether to respect or ignore .gitignore ----@field binary_extensions string[]? The list of binary file extensions to exclude --- Scan a directory ---@param path string The directory path @@ -538,12 +423,9 @@ M.scan_dir = async.wrap(function(path, opts, callback) opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) local function filter_files(files) - if opts.binary_extensions and #opts.binary_extensions > 0 then - files = vim.tbl_filter(function(file) - local ext = file:lower():match('%.([^%.]+)$') - return not vim.tbl_contains(opts.binary_extensions, ext) - end, files) - end + files = vim.tbl_filter(function(file) + return file ~= '' and M.filetype(file) ~= nil + end, files) if opts.max_count and opts.max_count > 0 then files = vim.list_slice(files, 1, opts.max_count) end @@ -579,10 +461,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) vim.system(cmd, { text = true }, function(result) local files = {} if result and result.code == 0 and result.stdout ~= '' then - files = vim.tbl_filter(function(file) - return file ~= '' - end, vim.split(result.stdout, '\n')) - files = filter_files(files) + files = filter_files(vim.split(result.stdout, '\n')) end callback(files) From dbe18c8937219643bde7bc3f9d8cb7dcacc81099 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 22 Mar 2025 23:45:05 +0000 Subject: [PATCH 338/728] 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 15603476..669d8dba 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 20 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 6db427a0f91d729265ffcbd2cd162d38d97879f2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 23 Mar 2025 00:52:46 +0100 Subject: [PATCH 339/728] test: add mock for plenary.filetype module (#1047) Add missing mock for the plenary.filetype module in the test suite to ensure tests can run properly without the actual dependency. 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 c09cdb88..9497f016 100644 --- a/test/plugin_spec.lua +++ b/test/plugin_spec.lua @@ -9,6 +9,7 @@ package.loaded['plenary.async'] = { package.loaded['plenary.curl'] = {} package.loaded['plenary.log'] = {} package.loaded['plenary.scandir'] = {} +package.loaded['plenary.filetype'] = {} describe('CopilotChat plugin', function() it('should be able to load', function() From 8df61c54fac227d82d8b747ab37259b250fa967a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 23 Mar 2025 23:44:42 +0100 Subject: [PATCH 340/728] fix: properly save prompt in history (#1050) When saving chat history, include current prompt in the history array. This ensures that unsent (in-progress) prompts are also preserved when saving the chat history to a file. Closes #1049 --- lua/CopilotChat/init.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 730007d5..01bfac57 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1007,7 +1007,15 @@ function M.save(name, history_path) return end - local history = vim.json.encode(client.history) + local prompt = M.chat:get_prompt() + local history = vim.list_slice(client.history) + if prompt then + table.insert(history, { + content = prompt.content, + role = 'user', + }) + end + history_path = vim.fs.normalize(history_path) vim.fn.mkdir(history_path, 'p') history_path = history_path .. '/' .. name .. '.json' @@ -1016,7 +1024,7 @@ function M.save(name, history_path) log.error('Failed to save history to ' .. history_path) return end - file:write(history) + file:write(vim.json.encode(history)) file:close() log.info('Saved history to ' .. history_path) From 340f47ea089758beb408bf4acbe0ce61b7111aea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 23 Mar 2025 22:44:58 +0000 Subject: [PATCH 341/728] 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 669d8dba..4924eaf3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 23 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9ce16d41b2ca16811d784cd080d4daf793a02d16 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 25 Mar 2025 16:25:05 +0100 Subject: [PATCH 342/728] feat: use model name and versioning for model selection (#1055) Update model selection logic to use model names with versioning instead of just IDs. This improves user experience by showing more descriptive model names in the selection UI and ensures the latest version of each model is preferred when filtering available models. Also updates the default model to the latest gpt-4o-2024-11-20 version. Related https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1052#issuecomment-2751530966 --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/config/providers.lua | 12 ++++-------- lua/CopilotChat/init.lua | 4 ++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 297712da..aad53e77 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ Below are all available configuration options with their default values: system_prompt = '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 $). + model = 'gpt-4o-2024-11-20', -- 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 or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index d855cd29..5e741377 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -58,7 +58,7 @@ return { system_prompt = '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 $). + model = 'gpt-4o-2024-11-20', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'none', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index a2789767..dd7c7142 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -186,18 +186,14 @@ M.copilot = { end) :totable() - local version_map = {} + local name_map = {} for _, model in ipairs(models) do - if not version_map[model.version] or #model.id < #version_map[model.version] then - version_map[model.version] = model.id + if not name_map[model.name] or model.version > name_map[model.name].version then + name_map[model.name] = model end end - models = vim.tbl_map(function(id) - return vim.tbl_filter(function(model) - return model.id == id - end, models)[1] - end, vim.tbl_values(version_map)) + models = vim.tbl_values(name_map) for _, model in ipairs(models) do if not model.policy then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 01bfac57..f173fd87 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -733,7 +733,7 @@ function M.select_model() vim.ui.select(choices, { prompt = 'Select a model> ', format_item = function(item) - local out = string.format('%s (%s)', item.id, item.provider) + local out = string.format('%s (%s:%s)', item.name, item.provider, item.id) if item.selected then out = '* ' .. out end @@ -764,7 +764,7 @@ function M.select_agent() vim.ui.select(choices, { prompt = 'Select an agent> ', format_item = function(item) - local out = string.format('%s (%s)', item.id, item.provider) + local out = string.format('%s (%s:%s)', item.name, item.provider, item.id) if item.selected then out = '* ' .. out end From f84727fd79f726b7e848adf89108d66805638f06 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Mar 2025 15:25:26 +0000 Subject: [PATCH 343/728] 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 4924eaf3..2870411f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 23 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 25 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -512,7 +512,7 @@ Below are all available configuration options with their default values: system_prompt = '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 $). + model = 'gpt-4o-2024-11-20', -- 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 or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. From 2e7544f86127daa356d55e4a802bcb29fa63d470 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 26 Mar 2025 17:42:26 +0100 Subject: [PATCH 344/728] fix: filter out paygo models from Copilot Chat (#1059) These models do not actually work but have same name as non-paygo version (o3-mini) Closes #1058 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/providers.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index dd7c7142..1ca335e3 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -171,7 +171,7 @@ M.copilot = { local models = vim .iter(response.body.data) :filter(function(model) - return model.capabilities.type == 'chat' + return model.capabilities.type == 'chat' and not vim.endswith(model.id, 'paygo') end) :map(function(model) return { From df550ff3f9f2d028160946d674db08dad432fdd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Mar 2025 16:42:47 +0000 Subject: [PATCH 345/728] 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 2870411f..ca01269d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 25 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 26 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From fd231f60710e56034db241adc4e307804b635e73 Mon Sep 17 00:00:00 2001 From: Joe Price Date: Fri, 28 Mar 2025 13:32:48 -0700 Subject: [PATCH 346/728] fix: use --max-depth arg name for ripgrep <14 compat (#1072) --- lua/CopilotChat/utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 5bbb3e85..a9201239 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -443,7 +443,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) end if opts.max_depth then - table.insert(cmd, '-d') + table.insert(cmd, '--max-depth') table.insert(cmd, tostring(opts.max_depth)) end From 7e845e8e56b00bd8a27c466e2d2c63dba0c5ccf8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Mar 2025 20:33:10 +0000 Subject: [PATCH 347/728] 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 ca01269d..7bc58438 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 26 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From cd82b6b9c4cfb254218f5782cc3339e05cbb9989 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 28 Mar 2025 21:34:41 +0100 Subject: [PATCH 348/728] docs: add JPricey as a contributor for code (#1073) * 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 88e11c56..dc4d4e93 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -375,6 +375,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/810650?v=4", "profile": "https://teoljungberg.com", "contributions": ["code"] + }, + { + "login": "JPricey", + "name": "Joe Price", + "avatar_url": "https://avatars.githubusercontent.com/u/4826348?v=4", + "profile": "https://github.com/JPricey", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index aad53e77..d85a85f3 100644 --- a/README.md +++ b/README.md @@ -883,6 +883,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Mani Chandra
Mani Chandra

💻 Nischal Basuti
Nischal Basuti

📖 Teo Ljungberg
Teo Ljungberg

💻 + Joe Price
Joe Price

💻 From 2e1a7ff63a3fab99c83ae1b6e839da846ec12833 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Wed, 2 Apr 2025 00:58:48 +0800 Subject: [PATCH 349/728] feat: accept a function that returns the layout to use (#1087) Closes #1080 --- README.md | 2 +- doc/CopilotChat.txt | 2 +- lua/CopilotChat/config.lua | 6 ++++-- lua/CopilotChat/ui/chat.lua | 12 ++++++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d85a85f3..ddaf744d 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ Below are all available configuration options with their default values: -- default window options window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' + layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout 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 diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 7bc58438..ca625dcb 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -531,7 +531,7 @@ Below are all available configuration options with their default values: -- default window options window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' + layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout 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 diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 5e741377..d1ea254a 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -1,7 +1,9 @@ local select = require('CopilotChat.select') +---@alias CopilotChat.config.Layout 'vertical'|'horizontal'|'float'|'replace' + ---@class CopilotChat.config.window ----@field layout 'vertical'|'horizontal'|'float'|'replace'? +---@field layout? CopilotChat.config.Layout|fun():CopilotChat.config.Layout ---@field relative 'editor'|'win'|'cursor'|'mouse'? ---@field border 'none'|'single'|'double'|'rounded'|'solid'|'shadow'? ---@field width number? @@ -76,7 +78,7 @@ return { -- default window options window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' + layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout 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 diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 3e416f05..82af9789 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -55,6 +55,7 @@ end ---@class CopilotChat.ui.Chat : CopilotChat.ui.Overlay ---@field winnr number? ---@field config CopilotChat.config.shared +---@field layout CopilotChat.config.Layout? ---@field sections table ---@field references table ---@field token_count number? @@ -71,6 +72,7 @@ local Chat = class(function(self, question_header, answer_header, separator, hel self.winnr = nil self.sections = {} self.config = {} + self.layout = nil self.references = {} self.token_count = nil self.token_max_count = nil @@ -268,15 +270,21 @@ function Chat:open(config) self:validate() local window = config.window or {} + local layout = window.layout + if type(layout) == 'function' then + layout = layout() + end + 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.config and self.config.window and self.config.window.layout ~= layout then + if self.layout ~= layout then self:close() end self.config = config + self.layout = layout if self:visible() then return @@ -357,7 +365,7 @@ function Chat:close(bufnr) utils.return_to_normal_mode() end - if self.config.window and self.config.window.layout == 'replace' then + if self.layout == 'replace' then if bufnr then self:restore(self.winnr, bufnr) end From 60fa8af6dca58db90823156a970811e8378d6044 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 1 Apr 2025 16:59:17 +0000 Subject: [PATCH 350/728] 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 ca625dcb..38cec261 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 March 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -884,7 +884,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 23162ada224e858d0d60bbbf2a35f9b6c98427ce Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:59:42 +0200 Subject: [PATCH 351/728] docs: add ouuan as a contributor for doc, and code (#1088) * 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 dc4d4e93..c4d4d029 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -382,6 +382,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/4826348?v=4", "profile": "https://github.com/JPricey", "contributions": ["code"] + }, + { + "login": "ouuan", + "name": "Yufan You", + "avatar_url": "https://avatars.githubusercontent.com/u/30581822?v=4", + "profile": "https://ouuan.moe/about", + "contributions": ["doc", "code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ddaf744d..566d0371 100644 --- a/README.md +++ b/README.md @@ -884,6 +884,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Nischal Basuti
Nischal Basuti

📖 Teo Ljungberg
Teo Ljungberg

💻 Joe Price
Joe Price

💻 + Yufan You
Yufan You

📖 💻 From 34d1b4fc816401c9bad88b33f71ef943a7dd2396 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 4 Apr 2025 02:33:18 +0200 Subject: [PATCH 352/728] fix(client): update response_text after parsing response (#1093) The response_text variable was being captured before the response body was parsed. This commit ensures that response_text is updated after parsing is complete, which provides the correct final text for processing. Closes #1064 --- lua/CopilotChat/client.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 5f17080d..dddd1b89 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -709,9 +709,6 @@ function Client:ask(prompt, opts) end local response_text = response_buffer:tostring() - log.trace('Response text:\n', response_text) - log.debug('Response message:\n', vim.inspect(last_message)) - if errored then error(response_text) return @@ -727,6 +724,7 @@ function Client:ask(prompt, opts) else parse_line(response.body) end + response_text = response_buffer:tostring() end if utils.empty(response_text) then From 0a1c4631242192704a8b49398dca8e12eb39a311 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 4 Apr 2025 00:33:38 +0000 Subject: [PATCH 353/728] 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 38cec261..02304af7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 01 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 04 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -884,7 +884,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 00bf27ed201b9509105afaac4d5bdcc46ce89f35 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 4 Apr 2025 02:47:59 +0200 Subject: [PATCH 354/728] fix: handle invalid context window size in GitHub models (#1094) Before the code was assuming that context window = input tokens, but its total. Also some models arent reporting the input and output correctly, so use default when result is 0 --- lua/CopilotChat/config/providers.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 1ca335e3..c34a398b 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -322,12 +322,20 @@ M.github_models = { return vim.tbl_contains(model.inferenceTasks, 'chat-completion') end) :map(function(model) + local context_window = model.modelLimits.textLimits.inputContextWindow + local max_output_tokens = model.modelLimits.textLimits.maxOutputTokens + local max_input_tokens = context_window - max_output_tokens + if max_input_tokens <= 0 then + max_output_tokens = 4096 + max_input_tokens = context_window - max_output_tokens + end + return { id = model.name, name = model.displayName, tokenizer = 'o200k_base', - max_input_tokens = model.modelLimits.textLimits.inputContextWindow, - max_output_tokens = model.modelLimits.textLimits.maxOutputTokens, + max_input_tokens = max_input_tokens, + max_output_tokens = max_output_tokens, } end) :totable() From 81754ea35253c48459db5712ae60531ea2c5ef75 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 4 Apr 2025 03:07:51 +0200 Subject: [PATCH 355/728] fix(diff): normalize filename (#1095) Use utils.filename() to normalize the filename value in the diff object, ensuring consistent path handling. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 79219549..c7a257b5 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -64,7 +64,7 @@ local function get_diff(block) change = block.content, reference = reference or '', filetype = filetype or '', - filename = filename, + filename = utils.filename(filename), start_line = start_line, end_line = end_line, bufnr = bufnr, @@ -240,7 +240,6 @@ return { 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) - copilot.set_selection(diff.bufnr, diff.start_line, diff.start_line + #lines - 1) end, }, From e7cd79a2ac05a32a1ce0995a82bd630598619d0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:08:49 +0000 Subject: [PATCH 356/728] chore(main): release 3.10.1 --- CHANGELOG.md | 9 +++++++++ version.txt | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b10796e..aa7c2751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [3.10.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.10.0...v3.10.1) (2025-04-04) + + +### Bug Fixes + +* **client:** update response_text after parsing response ([#1093](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1093)) ([34d1b4f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/34d1b4fc816401c9bad88b33f71ef943a7dd2396)), closes [#1064](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1064) +* **diff:** normalize filename ([#1095](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1095)) ([81754ea](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/81754ea35253c48459db5712ae60531ea2c5ef75)) +* handle invalid context window size in GitHub models ([#1094](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1094)) ([00bf27e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/00bf27ed201b9509105afaac4d5bdcc46ce89f35)) + ## [1.9.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v1.8.0...v1.9.0) (2024-02-24) diff --git a/version.txt b/version.txt index f8e233b2..f870be23 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.0 +3.10.1 From 3228edc02014c7240ff122aa4a94a4dff1215be1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 7 Apr 2025 08:32:17 +0200 Subject: [PATCH 357/728] refactor: do not require chat on plugin load Signed-off-by: Tomas Slusny --- plugin/CopilotChat.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index 9b4639df..5674d6bc 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -8,8 +8,6 @@ if vim.fn.has('nvim-' .. min_version) ~= 1 then return end -local chat = require('CopilotChat') - -- Setup highlights vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) @@ -21,6 +19,7 @@ vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.ma -- Setup commands vim.api.nvim_create_user_command('CopilotChat', function(args) + local chat = require('CopilotChat') local input = args.args if input and vim.trim(input) ~= '' then chat.ask(input) @@ -33,31 +32,40 @@ end, { range = true, }) vim.api.nvim_create_user_command('CopilotChatPrompts', function() + local chat = require('CopilotChat') chat.select_prompt() end, { force = true, range = true }) vim.api.nvim_create_user_command('CopilotChatModels', function() + local chat = require('CopilotChat') chat.select_model() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatAgents', function() + local chat = require('CopilotChat') chat.select_agent() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatOpen', function() + local chat = require('CopilotChat') chat.open() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatClose', function() + local chat = require('CopilotChat') chat.close() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatToggle', function() + local chat = require('CopilotChat') chat.toggle() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatStop', function() + local chat = require('CopilotChat') chat.stop() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatReset', function() + local chat = require('CopilotChat') chat.reset() end, { force = true }) local function complete_load() + local chat = require('CopilotChat') local options = vim.tbl_map(function(file) return vim.fn.fnamemodify(file, ':t:r') end, vim.fn.glob(chat.config.history_path .. '/*', true, true)) @@ -69,9 +77,11 @@ local function complete_load() return options end vim.api.nvim_create_user_command('CopilotChatSave', function(args) + local chat = require('CopilotChat') chat.save(args.args) end, { nargs = '*', force = true, complete = complete_load }) vim.api.nvim_create_user_command('CopilotChatLoad', function(args) + local chat = require('CopilotChat') chat.load(args.args) end, { nargs = '*', force = true, complete = complete_load }) From 16f041ecc954975416b1e33f8ca2e1df2b86a625 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 7 Apr 2025 06:33:12 +0000 Subject: [PATCH 358/728] 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 02304af7..d3b53a82 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 04 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 07 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 14c78d24e1db88384dc878e870665c3a7ad61a3a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 8 Apr 2025 08:29:37 +0200 Subject: [PATCH 359/728] feat: add option to disable contexts in prompts Adds a new configuration option `include_contexts_in_prompt` that allows users to control whether context descriptions should be included when building the prompt for Copilot Chat. This is useful for cases where users want to reduce prompt size or customize how context information is presented to the AI. The option defaults to true to maintain backward compatibility. --- lua/CopilotChat/config.lua | 3 +++ lua/CopilotChat/init.lua | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index d1ea254a..df64bf0f 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -25,6 +25,7 @@ local select = require('CopilotChat.select') ---@field stream nil|fun(chunk: string, source: CopilotChat.source):string ---@field callback nil|fun(response: string, source: CopilotChat.source):string ---@field remember_as_sticky boolean? +---@field include_contexts_in_prompt boolean? ---@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.selection? ---@field window CopilotChat.config.window? ---@field show_help boolean? @@ -71,6 +72,8 @@ return { callback = nil, -- Function called when full response is received (retuned string is stored to history) remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions + include_contexts_in_prompt = true, -- Include contexts in prompt + -- default selection selection = function(source) return select.visual(source) or select.buffer(source) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f173fd87..1062e1a4 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -850,9 +850,11 @@ function M.ask(prompt, config) -- Resolve context name and description local contexts = {} - for name, context in pairs(M.config.contexts) do - if context.description then - contexts[name] = context.description + if config.include_contexts_in_prompt then + for name, context in pairs(M.config.contexts) do + if context.description then + contexts[name] = context.description + end end end From 2b5c81f688a92a1d24fd03271e6810ced45ede52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Apr 2025 06:30:37 +0000 Subject: [PATCH 360/728] 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 d3b53a82..11e4f6c5 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 07 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 08 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From a63031fc706d4e34e118c46339ae2b5681fab21e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 8 Apr 2025 22:18:12 +0200 Subject: [PATCH 361/728] feat: change default selection to visual only this is a bit more controllable than visual || buffer + add mention in README at start Closes #1103 Signed-off-by: Tomas Slusny --- README.md | 8 +++----- lua/CopilotChat/config.lua | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 566d0371..422e634f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities - 🤖 GitHub Copilot Chat integration with official model and agent support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) - 💻 Rich workspace context powered by smart embeddings system -- 🔒 Explicit context sharing - only sends what you specifically request, either as context or selection +- 🔒 Explicit context sharing - only sends what you specifically request, either as context or selection (by default visual selection) - 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, LM Studio, Mistral.ai and more) - 📝 Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts @@ -387,7 +387,7 @@ You can set a default selection in the configuration: ```lua { - -- Default uses visual selection or falls back to buffer + -- Uses visual selection or falls back to buffer selection = function(source) return select.visual(source) or select.buffer(source) end @@ -466,9 +466,7 @@ Below are all available configuration options with their default values: -- default selection -- see select.lua for implementation - selection = function(source) - return select.visual(source) or select.buffer(source) - end, + selection = select.visual, -- default window options window = { diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index df64bf0f..d3ce3da9 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -75,9 +75,7 @@ return { include_contexts_in_prompt = true, -- Include contexts in prompt -- default selection - selection = function(source) - return select.visual(source) or select.buffer(source) - end, + selection = select.visual, -- default window options window = { From 44fde5a5b14a06f6639e9b98d87fc1a87bf2ab5d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Apr 2025 20:20:48 +0000 Subject: [PATCH 362/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 11e4f6c5..853a5055 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -38,7 +38,7 @@ capabilities directly into your editor. It provides: - 🤖 GitHub Copilot Chat integration with official model and agent support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) - 💻 Rich workspace context powered by smart embeddings system -- 🔒 Explicit context sharing - only sends what you specifically request, either as context or selection +- 🔒 Explicit context sharing - only sends what you specifically request, either as context or selection (by default visual selection) - 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, LM Studio, Mistral.ai and more) - 📝 Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts @@ -437,7 +437,7 @@ You can set a default selection in the configuration: >lua { - -- Default uses visual selection or falls back to buffer + -- Uses visual selection or falls back to buffer selection = function(source) return select.visual(source) or select.buffer(source) end @@ -525,9 +525,7 @@ Below are all available configuration options with their default values: -- default selection -- see select.lua for implementation - selection = function(source) - return select.visual(source) or select.buffer(source) - end, + selection = select.visual, -- default window options window = { From 381d5cddd25abec595c3c611e96cae2ba61d7ea5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 9 Apr 2025 08:54:30 +0200 Subject: [PATCH 363/728] fix: set default model to gpt-4o again they removed the versioned ones again and put back gpt-4o Closes #1105 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index d3ce3da9..dc1af205 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -61,7 +61,7 @@ return { system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - model = 'gpt-4o-2024-11-20', -- Default model to use, see ':CopilotChatModels' for available models (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 = 'none', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. From bc8d12014fd4da1426093588d1b98654221a9cc5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 9 Apr 2025 06:59:02 +0000 Subject: [PATCH 364/728] 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 853a5055..13ce6f83 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 08 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From a89f5f1162b04a0962e5f4c3cdf248a81e7e53cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:25:03 +0800 Subject: [PATCH 365/728] chore(main): release 3.11.0 (#1102) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ version.txt | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7c2751..a5de69ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [3.11.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.10.1...v3.11.0) (2025-04-09) + + +### Features + +* add option to disable contexts in prompts ([14c78d2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/14c78d24e1db88384dc878e870665c3a7ad61a3a)) +* change default selection to visual only ([a63031f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a63031fc706d4e34e118c46339ae2b5681fab21e)), closes [#1103](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1103) + + +### Bug Fixes + +* set default model to gpt-4o again ([381d5cd](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/381d5cddd25abec595c3c611e96cae2ba61d7ea5)), closes [#1105](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1105) + ## [3.10.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.10.0...v3.10.1) (2025-04-04) diff --git a/version.txt b/version.txt index f870be23..afad8186 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.10.1 +3.11.0 From bc644cd97d272e6b46272cbb11147a5891fa08ff Mon Sep 17 00:00:00 2001 From: Manish Suthar Date: Sat, 19 Apr 2025 01:56:50 +0530 Subject: [PATCH 366/728] fix(validation): Ensure If the erminal buffer is excluded from #buffers and #buffer This prevents unsolicited text from being included in the CopilotChat context window. --- lua/CopilotChat/utils.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index a9201239..1be2504b 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -210,10 +210,15 @@ function M.kv_list(tbl) end --- Check if a buffer is valid +--- Check if the buffer is not a terminal ---@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 + return bufnr + and vim.api.nvim_buf_is_valid(bufnr) + and vim.api.nvim_buf_is_loaded(bufnr) + and vim.bo[bufnr].buftype ~= 'terminal' + or false end --- Check if file paths are the same From 18d08f76fbe8dae8b74d2efd9234ecf659839e18 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 18 Apr 2025 20:39:59 +0000 Subject: [PATCH 367/728] 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 13ce6f83..b7beb72a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 18 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 634aa58117a9b70b3f08a0b150f11afd64f1c0eb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:41:16 +0200 Subject: [PATCH 368/728] docs: add m4dd0c as a contributor for code (#1117) * 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 c4d4d029..78d5ba16 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -389,6 +389,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/30581822?v=4", "profile": "https://ouuan.moe/about", "contributions": ["doc", "code"] + }, + { + "login": "m4dd0c", + "name": "Manish Kumar", + "avatar_url": "https://avatars.githubusercontent.com/u/77256586?v=4", + "profile": "https://m4dd0c.netlify.app", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 422e634f..55d60a7f 100644 --- a/README.md +++ b/README.md @@ -883,6 +883,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Teo Ljungberg
Teo Ljungberg

💻 Joe Price
Joe Price

💻 Yufan You
Yufan You

📖 💻 + Manish Kumar
Manish Kumar

💻 From fbfb1ced23e36c4af41d6946cd0c3202018206cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:08:51 +0000 Subject: [PATCH 369/728] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.0.2 → v2.1.0](https://github.com/JohnnyMorganz/StyLua/compare/v2.0.2...v2.1.0) --- .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 1604446b..a90c2791 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: v2.0.2 + rev: v2.1.0 hooks: - id: stylua-github From f0e9b6cdeabdc7e05c4b9a462ed252cc0b5fb159 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 21 Apr 2025 19:16:39 +0000 Subject: [PATCH 370/728] 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 b7beb72a..48d9bfd7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 18 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 21 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -882,7 +882,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 6cb362d040f4f7e1177145fa27b3aee1c0bd6bff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 07:26:53 +0800 Subject: [PATCH 371/728] chore(main): release 3.11.1 (#1116) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5de69ab..fd6dc252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [3.11.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.11.0...v3.11.1) (2025-04-21) + + +### Bug Fixes + +* **validation:** Ensure If the erminal buffer is excluded from #buffers and #buffer ([bc644cd](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/bc644cd97d272e6b46272cbb11147a5891fa08ff)) + ## [3.11.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.10.1...v3.11.0) (2025-04-09) diff --git a/version.txt b/version.txt index afad8186..371cfe35 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.11.0 +3.11.1 From 75653259442a8eb895abfc70d7064e07aeb7134c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 22 Apr 2025 23:27:12 +0000 Subject: [PATCH 372/728] 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 48d9bfd7..9512331d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 21 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 5f105cf2453585487d3c9ccfe7fd129d3344056c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C5=BDdanov?= Date: Fri, 9 May 2025 21:04:46 +0300 Subject: [PATCH 373/728] feat: switch to new default model gpt-4.1 --- lua/CopilotChat/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index dc1af205..30c428e5 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -61,7 +61,7 @@ return { system_prompt = '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 $). + model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'none', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. From e1cb63be78d388113bed4c6b4c970e00ffd0759d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C5=BDdanov?= Date: Fri, 9 May 2025 21:04:59 +0300 Subject: [PATCH 374/728] docs: update mentions of default model to gpt-4.1 --- README.md | 4 ++-- doc/CopilotChat.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 55d60a7f..02248910 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ Below are all available configuration options with their default values: system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - model = 'gpt-4o-2024-11-20', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). + model = 'gpt-4.1', -- 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 or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. @@ -761,7 +761,7 @@ require("CopilotChat").load("my_debugging_session") -- Use custom context and model require("CopilotChat").ask("How can I optimize this?", { - model = "gpt-4o", + model = "gpt-4.1", context = {"buffer", "git:staged"} }) ``` diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9512331d..d7b009a4 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -512,7 +512,7 @@ Below are all available configuration options with their default values: system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - model = 'gpt-4o-2024-11-20', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). + model = 'gpt-4.1', -- 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 or array of contexts to use (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. @@ -827,7 +827,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* -- Use custom context and model require("CopilotChat").ask("How can I optimize this?", { - model = "gpt-4o", + model = "gpt-4.1", context = {"buffer", "git:staged"} }) < From 5cb48e66dff2bd94b2b36b69364cbea6b935e0c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 May 2025 18:18:42 +0000 Subject: [PATCH 375/728] 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 d7b009a4..65910dc0 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 April 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 May 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 4dce4d2fc185a935024511811139b68e91b2d2a8 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 20:19:31 +0200 Subject: [PATCH 376/728] docs: add azdanov as a contributor for doc, and code (#1130) * 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, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 78d5ba16..a4cd5633 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -396,6 +396,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/77256586?v=4", "profile": "https://m4dd0c.netlify.app", "contributions": ["code"] + }, + { + "login": "azdanov", + "name": "Anton Ždanov", + "avatar_url": "https://avatars.githubusercontent.com/u/6123841?v=4", + "profile": "https://www.azdanov.dev", + "contributions": ["doc", "code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 02248910..d66bbb02 100644 --- a/README.md +++ b/README.md @@ -885,6 +885,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Yufan You
Yufan You

📖 💻 Manish Kumar
Manish Kumar

💻 + + Anton Ždanov
Anton Ždanov

📖 💻 + From eddc9a98930cda0b3819cc1d93427b402697dfab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 10:24:16 +0800 Subject: [PATCH 377/728] chore(main): release 3.12.0 (#1131) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6dc252..797eb8d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [3.12.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.11.1...v3.12.0) (2025-05-09) + + +### Features + +* switch to new default model gpt-4.1 ([5f105cf](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/5f105cf2453585487d3c9ccfe7fd129d3344056c)) + ## [3.11.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.11.0...v3.11.1) (2025-04-21) diff --git a/version.txt b/version.txt index 371cfe35..92536a9e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.11.1 +3.12.0 From 16d897fd43d07e3b54478ccdb2f8a16e4df4f45a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 May 2025 02:24:34 +0000 Subject: [PATCH 378/728] 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 65910dc0..d56e51f7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 May 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 May 13 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -882,7 +882,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 5229bc48d655247449652d37ba525429ecfcce99 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 16 Jun 2025 12:19:20 +0200 Subject: [PATCH 379/728] fix: move plenary import into function (#1162) Commit cf02033 broke test detection in Neotest. When another plugin calls `require("plenary.filetype")` before Neotest does, it seems that Neotest cannot successfully complete the call to `require("neotest.lib").treesitter.parse_positions` and does not find any tests. This seem like a bug in either plenary or Neotest and this commit is merely a workaround. Related issues: - https://github.com/nvim-neotest/neotest/issues/502 - https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1099 --- lua/CopilotChat/utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 1be2504b..7cb28979 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,7 +1,6 @@ local async = require('plenary.async') local curl = require('plenary.curl') local scandir = require('plenary.scandir') -local filetype = require('plenary.filetype') local M = {} M.timers = {} @@ -236,6 +235,7 @@ end ---@param filename string The file name ---@return string|nil function M.filetype(filename) + local filetype = require('plenary.filetype') local ft = filetype.detect(filename, { fs_access = false, }) From 96142518fc837cdd4c3de0f07909a5076648517c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Jun 2025 10:19:38 +0000 Subject: [PATCH 380/728] 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 d56e51f7..3e75bc33 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 May 13 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 June 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 5df0b668d23c05c173f6bc79bb19642215b8b66a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:21:41 +0200 Subject: [PATCH 381/728] docs: add fredrikaverpil as a contributor for code (#1165) * 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 a4cd5633..db53eab1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -403,6 +403,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/6123841?v=4", "profile": "https://www.azdanov.dev", "contributions": ["doc", "code"] + }, + { + "login": "fredrikaverpil", + "name": "Fredrik Averpil", + "avatar_url": "https://avatars.githubusercontent.com/u/994357?v=4", + "profile": "http://fredrikaverpil.github.io", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d66bbb02..0e9b0b07 100644 --- a/README.md +++ b/README.md @@ -887,6 +887,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Anton Ždanov
Anton Ždanov

📖 💻 + Fredrik Averpil
Fredrik Averpil

💻 From 4944b11123ef7c8f9f6ddd0c85de5e4d20b45690 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:17:41 +0800 Subject: [PATCH 382/728] chore(main): release 3.12.1 (#1164) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 797eb8d3..878ad67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [3.12.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.0...v3.12.1) (2025-06-16) + + +### Bug Fixes + +* move plenary import into function ([#1162](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1162)) ([5229bc4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/5229bc48d655247449652d37ba525429ecfcce99)) + ## [3.12.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.11.1...v3.12.0) (2025-05-09) diff --git a/version.txt b/version.txt index 92536a9e..171a6a93 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.12.0 +3.12.1 From 4d90f472489a60cb5585ccffac11ed666206a645 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Jun 2025 11:17:59 +0000 Subject: [PATCH 383/728] 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 3e75bc33..d9eca763 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 June 16 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 June 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -882,7 +882,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 6d8236f83353317de8819cbfac75f791574d6374 Mon Sep 17 00:00:00 2001 From: Aaron D Borden Date: Sun, 22 Jun 2025 16:35:55 -0700 Subject: [PATCH 384/728] fix: #1153 use filepath on accept (#1170) Use filepath for the full path when accepting a change. --- lua/CopilotChat/config/mappings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index c7a257b5..baf2f82c 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -64,7 +64,7 @@ local function get_diff(block) change = block.content, reference = reference or '', filetype = filetype or '', - filename = utils.filename(filename), + filename = utils.filepath(filename), start_line = start_line, end_line = end_line, bufnr = bufnr, From 55f2162c36901224e22ff8424fd60ecef670b8fc Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:36:37 +0200 Subject: [PATCH 385/728] docs: add adborden as a contributor for code (#1171) * 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 db53eab1..b753dc3f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -410,6 +410,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/994357?v=4", "profile": "http://fredrikaverpil.github.io", "contributions": ["code"] + }, + { + "login": "adborden", + "name": "Aaron D Borden", + "avatar_url": "https://avatars.githubusercontent.com/u/509703?v=4", + "profile": "https://a14n.net", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0e9b0b07..6761c5e6 100644 --- a/README.md +++ b/README.md @@ -888,6 +888,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Anton Ždanov
Anton Ždanov

📖 💻 Fredrik Averpil
Fredrik Averpil

💻 + Aaron D Borden
Aaron D Borden

💻 From ea021e02e9d7337c3c01e4c30406bd7085be372b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 9 Jul 2025 18:34:01 +0200 Subject: [PATCH 386/728] Properly null check opts.glob when rigpgrep is not available (#1183) Closes #1178 Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 7cb28979..afd92c5d 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -481,7 +481,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) vim.tbl_deep_extend('force', opts, { depth = opts.max_depth, add_dirs = false, - search_pattern = M.glob_to_pattern(opts.glob), + search_pattern = opts.glob and M.glob_to_pattern(opts.glob) or nil, respect_gitignore = not opts.no_ignore, on_exit = function(files) callback(filter_files(files)) From a90a92af7514edbacdde09fa10e7550af2ffdc36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 9 Jul 2025 16:34:19 +0000 Subject: [PATCH 387/728] 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 d9eca763..980baeda 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 June 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -882,7 +882,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻This project follows the all-contributors specification. Contributions of any kind are welcome! From bdb270dac270a0e27b23bf2ef40482405b3c984d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 12 Jul 2025 08:19:57 +0800 Subject: [PATCH 388/728] chore(main): release 3.12.2 (#1172) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 878ad67b..10dde051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [3.12.2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.1...v3.12.2) (2025-07-09) + + +### Bug Fixes + +* [#1153](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1153) use filepath on accept ([#1170](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1170)) ([6d8236f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/6d8236f83353317de8819cbfac75f791574d6374)) + ## [3.12.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.0...v3.12.1) (2025-06-16) diff --git a/version.txt b/version.txt index 171a6a93..8531a3b7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.12.1 +3.12.2 From f7eb423baccbb27f5b5608fb91acee2d6bc769c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 12 Jul 2025 00:20:16 +0000 Subject: [PATCH 389/728] 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 980baeda..9faa9a44 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 12 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 7559fd25928f8f3cf311ff25b95bdc5f9ec736d7 Mon Sep 17 00:00:00 2001 From: "Md. Iftakhar Awal Chowdhury" <42291930+AtifChy@users.noreply.github.com> Date: Fri, 25 Jul 2025 05:47:50 +0600 Subject: [PATCH 390/728] feat: add Windows_NT support in Makefile and dynamic library loading (#1190) * feat: add Windows_NT support in Makefile and dynamic library loading * [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> --- Makefile | 3 +++ README.md | 1 - lua/CopilotChat/tiktoken.lua | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cd71bc6e..c5d53c52 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ ifeq ($(UNAME), Linux) else ifeq ($(UNAME), Darwin) OS := macOS EXT := dylib +else ifeq ($(UNAME), Windows_NT) + OS := windows + EXT := dll else $(error Unsupported operating system: $(UNAME)) endif diff --git a/README.md b/README.md index 6761c5e6..66a56cf7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities ## Optional Dependencies - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - For accurate token counting - - Arch Linux: 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 - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) and save as `tiktoken_core.so` in your Lua path diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 9bfa2945..a4582cb4 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -2,6 +2,23 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local current_tokenizer = nil +--- @return string +local function get_lib_extension() + if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then + return '.dylib' + end + if jit.os:lower() == 'windows' then + return '.dll' + end + return '.so' +end + +package.cpath = package.cpath + .. ';' + .. debug.getinfo(1).source:match('@?(.*/)') + .. '../../build/?' + .. get_lib_extension() + local tiktoken_ok, tiktoken_core = pcall(require, 'tiktoken_core') if not tiktoken_ok then tiktoken_core = nil From 4d0d949b7367b10c9f3ab9011ac54dd3107fb0a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Jul 2025 23:48:05 +0000 Subject: [PATCH 391/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9faa9a44..e5863a11 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 12 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 24 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -61,14 +61,12 @@ capabilities directly into your editor. It provides: OPTIONAL DEPENDENCIES *CopilotChat-optional-dependencies* -- tiktoken_core - For accurate token - counting +- tiktoken_core - For accurate token counting - Arch Linux: Install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from lua-tiktoken releases and save as `tiktoken_core.so` in your Lua path - git - For git diff context features -- ripgrep - For improved search - performance +- ripgrep - For improved search performance - lynx - For improved URL context features From d0537a749e11a68ebaea3967b9c698f998a700fe Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 01:50:11 +0200 Subject: [PATCH 392/728] docs: add AtifChy as a contributor for code, and doc (#1192) * 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 b753dc3f..d335de60 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -417,6 +417,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/509703?v=4", "profile": "https://a14n.net", "contributions": ["code"] + }, + { + "login": "AtifChy", + "name": "Md. Iftakhar Awal Chowdhury", + "avatar_url": "https://avatars.githubusercontent.com/u/42291930?v=4", + "profile": "https://github.com/AtifChy", + "contributions": ["code", "doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 66a56cf7..a08f6b0b 100644 --- a/README.md +++ b/README.md @@ -888,6 +888,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Anton Ždanov
Anton Ždanov

📖 💻 Fredrik Averpil
Fredrik Averpil

💻 Aaron D Borden
Aaron D Borden

💻 + Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

💻 📖 From 057b8e46d955748b1426e7b174d7af3e58f5191b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 02:21:46 +0200 Subject: [PATCH 393/728] feat(context)!: switch from contexts to function calling (#1029) This change modernizes the CopilotChat architecture by implementing a tools-based approach using function calling. Each tool has a schema definition that enables structured parameter collection and validation. Resources are now handled more consistently, with a clearer distinction between content types. https://platform.openai.com/docs/guides/function-calling?api-mode=responses https://modelcontextprotocol.info/specification/2024-11-05/server/tools/ BREAKING CHANGE: The context API has changed from callback-based input handling to schema-based definitions. BREAKING CHANGE: config.contexts renamed to config.tools BREAKING CHANGE: config.context removed, use config.sticky BREAKING CHANGE: diagnostics moved to separate tool call, selection and buffer calls no longer include them by default BREAKING CHANGE: non-resource based tool calls can no longer be soft stored in sticky, now they are auto expanded to promot BREAKING CHANGE: viewing full context is no longer possible (as now tools can have bigger side effects), gi renamed to gc, now also includes selection BREAKING CHANGE: filenames renamed to glob BREAKING CHANGE: files removed (use glob together with tool calling instead, or buffers/quickfix) BREAKING CHANGE: copilot extension agents removed, tools + mcp servers can replace this feature and maintaining them was pain, they can still be implemented via custom providers anyway BREAKING CHANGE: actions and integrations action removed as they were deprecated for a while Closes #1045 Closes #1053 Closes #1076 Closes #1090 Closes #1096 Closes #526 Signed-off-by: Tomas Slusny --- README.md | 297 +++---- lua/CopilotChat/actions.lua | 49 -- lua/CopilotChat/client.lua | 470 ++++------- lua/CopilotChat/config.lua | 60 +- lua/CopilotChat/config/contexts.lua | 352 -------- lua/CopilotChat/config/functions.lua | 503 ++++++++++++ lua/CopilotChat/config/mappings.lua | 136 ++-- lua/CopilotChat/config/prompts.lua | 95 ++- lua/CopilotChat/config/providers.lua | 173 ++-- lua/CopilotChat/functions.lua | 198 +++++ lua/CopilotChat/health.lua | 3 +- lua/CopilotChat/init.lua | 770 +++++++++--------- lua/CopilotChat/integrations/fzflua.lua | 42 - lua/CopilotChat/integrations/snacks.lua | 54 -- lua/CopilotChat/integrations/telescope.lua | 65 -- .../{context.lua => resources.lua} | 147 ++-- lua/CopilotChat/select.lua | 32 +- lua/CopilotChat/ui/chat.lua | 451 ++++++---- lua/CopilotChat/ui/overlay.lua | 8 +- lua/CopilotChat/ui/spinner.lua | 2 +- lua/CopilotChat/utils.lua | 312 ++++--- plugin/CopilotChat.lua | 35 +- 22 files changed, 2150 insertions(+), 2104 deletions(-) delete mode 100644 lua/CopilotChat/actions.lua delete mode 100644 lua/CopilotChat/config/contexts.lua create mode 100644 lua/CopilotChat/config/functions.lua create mode 100644 lua/CopilotChat/functions.lua delete mode 100644 lua/CopilotChat/integrations/fzflua.lua delete mode 100644 lua/CopilotChat/integrations/snacks.lua delete mode 100644 lua/CopilotChat/integrations/telescope.lua rename lua/CopilotChat/{context.lua => resources.lua} (81%) diff --git a/README.md b/README.md index a08f6b0b..6c6eb93f 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities directly into your editor. It provides: -- 🤖 GitHub Copilot Chat integration with official model and agent support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) +- 🤖 GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) - 💻 Rich workspace context powered by smart embeddings system -- 🔒 Explicit context sharing - only sends what you specifically request, either as context or selection (by default visual selection) -- 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, LM Studio, Mistral.ai and more) +- 🔒 Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) +- 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) - 📝 Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts -- 🔄 Extensible context providers for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚡ Efficient token usage with tiktoken token counting and memory management +- 🔄 Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) +- ⚡ Efficient token usage with tiktoken token counting and history management # Requirements @@ -61,8 +61,7 @@ Plugin features that use picker: - `:CopilotChatPrompts` - for selecting prompts - `:CopilotChatModels` - for selecting models -- `:CopilotChatAgents` - for selecting agents -- `#:` - for selecting context input +- `#:` - for selecting function input # Installation @@ -76,7 +75,7 @@ return { { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, - build = "make tiktoken", -- Only on MacOS or Linux + build = "make tiktoken", opts = { -- See Configuration section for options }, @@ -147,7 +146,6 @@ Commands are used to control the chat interface: | `:CopilotChatLoad ?` | Load chat history | | `:CopilotChatPrompts` | View/select prompt templates | | `:CopilotChatModels` | View/select available models | -| `:CopilotChatAgents` | View/select available agents | | `:CopilotChat` | Use specific prompt template | ## Key Mappings @@ -252,7 +250,7 @@ Define your own system prompts in the configuration (similar to `prompts`): ### Sticky Prompts -Sticky prompts persist across chat sessions. They're useful for maintaining context or agent selection. They work as follows: +Sticky prompts persist across chat sessions. They're useful for maintaining model or resource selection. They work as follows: 1. Prefix text with `> ` using markdown blockquote syntax 2. The prompt will be copied at the start of every new chat prompt @@ -261,7 +259,7 @@ Sticky prompts persist across chat sessions. They're useful for maintaining cont Examples: ```markdown -> #files +> #glob:`*.lua` > List all files in the workspace > @models Using Mistral-small @@ -273,15 +271,12 @@ You can also set default sticky prompts in the configuration: ```lua { sticky = { - '@models Using Mistral-small', - '#files', + '#glob:*.lua', } } ``` -## Models and Agents - -### Models +## Models You can control which AI model to use in three ways: @@ -294,69 +289,67 @@ For supported models, see: - [Copilot Chat Models](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat) - [GitHub Marketplace Models](https://github.com/marketplace/models) (experimental, limited usage) -### Agents - -Agents determine the AI assistant's capabilities. Control agents in three ways: - -1. List available agents with `:CopilotChatAgents` -2. Set agent in prompt with `@agent_name` -3. Configure default agent via `agent` config key - -The default "noop" agent is `none`. For more information: - -- [Extension Agents Documentation](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) -- [Available Agents](https://github.com/marketplace?type=apps&copilot_app=true) - -## Contexts - -Contexts provide additional information to the chat. Add context using `#context_name[:input]` syntax: - -| Context | Input Support | Description | -| ----------- | ------------- | ----------------------------------- | -| `buffer` | ✓ (number) | Current or specified buffer content | -| `buffers` | ✓ (type) | All buffers content (listed/all) | -| `file` | ✓ (path) | Content of specified file | -| `files` | ✓ (glob) | Workspace files | -| `filenames` | ✓ (glob) | Workspace file names | -| `git` | ✓ (ref) | Git diff (unstaged/staged/commit) | -| `url` | ✓ (url) | Content from URL | -| `register` | ✓ (name) | Content of vim register | -| `quickfix` | - | Quickfix list file contents | -| `system` | ✓ (command) | Output of shell command | - -> [!TIP] -> The AI is aware of these context providers and may request additional context -> if needed by asking you to input a specific context command like `#file:path/to/file.js`. +## Functions + +Functions provide additional information and behaviour to the chat. +Tools can be organized into groups by setting the `group` property. Tools assigned to a group are not automatically made available to the LLM - they must be explicitly activated. +To use grouped tools in your prompt, include `@group_name` in your message. This allows the LLM to access and use all tools in that group during the current interaction. +Add tools using `#tool_name[:input]` syntax: + +| Function | Input Support | Description | +| ------------- | ------------- | ------------------------------------------------------ | +| `buffer` | ✓ (name) | Retrieves content from a specific buffer | +| `buffers` | ✓ (scope) | Fetches content from multiple buffers (listed/visible) | +| `diagnostics` | ✓ (scope) | Collects code diagnostics (errors, warnings) | +| `file` | ✓ (path) | Reads content from a specified file path | +| `gitdiff` | ✓ (sha) | Retrieves git diff information (unstaged/staged/sha) | +| `gitstatus` | - | Retrieves git status information | +| `glob` | ✓ (pattern) | Lists filenames matching a pattern in workspace | +| `grep` | ✓ (pattern) | Searches for a pattern across files in workspace | +| `quickfix` | - | Includes content of files in quickfix list | +| `register` | ✓ (register) | Provides access to specified Vim register | +| `url` | ✓ (url) | Fetches content from a specified URL | Examples: ```markdown -> #buffer -> #buffer:2 -> #files:\*.lua -> #filenames +> #buffer:init.lua +> #buffers:visible +> #diagnostics:current +> #file:path/to/file.js > #git:staged +> #glob:`**/*.lua` +> #grep:`function setup` +> #quickfix +> #register:+ > #url:https://example.com -> #system:`ls -la | grep lua` ``` -Define your own contexts in the configuration with input handling and resolution: +Define your own functions in the configuration with input handling and schema: ```lua { - contexts = { + functions = { birthday = { - input = function(callback) - vim.ui.select({ 'user', 'napoleon' }, { - prompt = 'Select birthday> ', - }, callback) - end, + description = "Retrieves birthday information for a person", + uri = "birthday://{name}", + schema = { + type = 'object', + required = { 'name' }, + properties = { + name = { + type = 'string', + enum = { 'Alice', 'Bob', 'Charlie' }, + description = "Person's name", + }, + }, + }, resolve = function(input) return { { - content = input .. ' birthday info', - filename = input .. '_birthday', - filetype = 'text', + uri = 'birthday://' .. input.name, + mimetype = 'text/plain', + data = input.name .. ' birthday info', } } end @@ -365,9 +358,9 @@ Define your own contexts in the configuration with input handling and resolution } ``` -### External Contexts +### External Functions -For external contexts, see the [contexts discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/contexts). +For external functions implementations, see the [discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions). ## Selections @@ -429,9 +422,6 @@ Custom providers can implement these methods: -- Optional: Get available models get_models?(headers: table): table, - - -- Optional: Get available agents - get_agents?(headers: table): table, } ``` @@ -453,19 +443,17 @@ Below are all available configuration options with their default values: system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- 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 or array of contexts to use (can be specified manually in prompt via #). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - temperature = 0.1, -- GPT result temperature + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) - callback = nil, -- Function called when full response is received (retuned string is stored to history) - remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions + callback = nil, -- Function called when full response is received + remember_as_sticky = true, -- Remember model as sticky prompts when asking questions -- default selection -- see select.lua for implementation - selection = select.visual, + selection = require('CopilotChat.select').visual, -- default window options window = { @@ -483,9 +471,9 @@ Below are all available configuration options with their default values: }, 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) - references_display = 'virtual', -- 'virtual', 'write', Display references in chat as virtual text or write to buffer 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 @@ -503,129 +491,29 @@ Below are all available configuration options with their default values: log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file 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 + headers = { + user = '## User ', -- Header to use for user questions + assistant = '## Copilot ', -- Header to use for AI answers + tool = '## Tool ', -- Header to use for tool calls + }, + separator = '───', -- Separator to use in chat -- default providers -- see config/providers.lua for implementation - providers = { - copilot = { - }, - github_models = { - }, - copilot_embeddings = { - }, - }, + providers = require('CopilotChat.config.providers'), - -- default contexts - -- see config/contexts.lua for implementation - contexts = { - buffer = { - }, - buffers = { - }, - file = { - }, - files = { - }, - git = { - }, - url = { - }, - register = { - }, - quickfix = { - }, - system = { - } - }, + -- default functions + -- see config/functions.lua for implementation + functions = require('CopilotChat.config.functions'), -- default prompts -- see config/prompts.lua for implementation - prompts = { - Explain = { - prompt = 'Write an explanation for the selected code as paragraphs of text.', - system_prompt = 'COPILOT_EXPLAIN', - }, - Review = { - prompt = 'Review the selected code.', - system_prompt = 'COPILOT_REVIEW', - }, - Fix = { - prompt = 'There is a problem in this code. Identify the issues and rewrite the code with fixes. Explain what was wrong and how your changes address the problems.', - }, - Optimize = { - prompt = 'Optimize the selected code to improve performance and readability. Explain your optimization strategy and the benefits of your changes.', - }, - Docs = { - prompt = 'Please add documentation comments to the selected code.', - }, - Tests = { - prompt = 'Please generate tests for my code.', - }, - Commit = { - prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - context = 'git:staged', - }, - }, + prompts = require('CopilotChat.config.prompts'), -- default mappings -- see config/mappings.lua for implementation - mappings = { - complete = { - insert = '', - }, - close = { - normal = 'q', - insert = '', - }, - reset = { - normal = '', - insert = '', - }, - submit_prompt = { - normal = '', - insert = '', - }, - toggle_sticky = { - normal = 'grr', - }, - clear_stickies = { - normal = 'grx', - }, - accept_diff = { - normal = '', - insert = '', - }, - jump_to_diff = { - normal = 'gj', - }, - quickfix_answers = { - normal = 'gqa', - }, - quickfix_diffs = { - normal = 'gqd', - }, - yank_diff = { - normal = 'gy', - register = '"', -- Default register to use for yanking - }, - show_diff = { - normal = 'gd', - full_diff = false, -- Show full diff instead of unified diff when showing diff window - }, - show_info = { - normal = 'gi', - }, - show_context = { - normal = 'gc', - }, - show_help = { - normal = 'gh', - }, - }, + mappings = require('CopilotChat.config.mappings'), } ``` @@ -659,8 +547,8 @@ Types of copilot highlights: - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help messages in chat buffer (help, references) - `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, contexts) -- `CopilotChatInput` - Input highlight in chat buffer (for contexts) +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) # API Reference @@ -673,8 +561,7 @@ local chat = require("CopilotChat") chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references -chat.resolve_context() -- Resolve context embeddings (WARN: async, requires plenary.async.run) -chat.resolve_agent() -- Resolve agent from prompt (WARN: async, requires plenary.async.run) +chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -692,10 +579,9 @@ chat.set_source(winnr) -- Set the source window chat.get_selection() -- Get the current selection chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection --- Prompt & Context Management +-- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector -chat.select_agent() -- Open agent selector chat.prompts() -- Get all available prompts -- Completion @@ -723,10 +609,12 @@ local window = require("CopilotChat").chat window:visible() -- Check if chat window is visible window:focused() -- Check if chat window is focused +-- Message Management +window:get_message(role) -- Get last chat message by role (user, assistant, tool) +window:add_message({ role, content }, replace) -- Add or replace a message in chat +window:add_sticky(sticky) -- Add sticky prompt to chat message + -- Content Management -window:get_prompt() -- Get current prompt from chat window -window:set_prompt(prompt) -- Set prompt in chat window -window:add_sticky(sticky) -- Add sticky prompt to chat window window:append(text) -- Append text to chat window window:clear() -- Clear chat window content window:finish() -- Finish writing to chat window @@ -736,9 +624,9 @@ window:follow() -- Move cursor to end of chat content window:focus() -- Focus the chat window -- Advanced Features -window:get_closest_section() -- Get section closest to cursor -window:get_closest_block() -- Get code block closest to cursor -window:overlay(opts) -- Show overlay with specified options +window:get_closest_message(role) -- Get message closest to cursor +window:get_closest_block(role) -- Get code block closest to cursor +window:overlay(opts) -- Show overlay with specified options ``` ## Example Usage @@ -746,19 +634,18 @@ window:overlay(opts) -- Show overlay with specified options ```lua -- Open chat, ask a question and handle response require("CopilotChat").open() -require("CopilotChat").ask("Explain this code", { +require("CopilotChat").ask("#buffer Explain this code", { callback = function(response) vim.notify("Got response: " .. response:sub(1, 50) .. "...") return response end, - context = "buffer" }) -- Save and load chat history require("CopilotChat").save("my_debugging_session") require("CopilotChat").load("my_debugging_session") --- Use custom context and model +-- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", context = {"buffer", "git:staged"} diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua deleted file mode 100644 index 2ab795b7..00000000 --- a/lua/CopilotChat/actions.lua +++ /dev/null @@ -1,49 +0,0 @@ ----@class CopilotChat.integrations.actions ----@field prompt string: The prompt to display ----@field actions table: A table with the actions to pick from - -local chat = require('CopilotChat') - -local M = {} - ---- User prompt actions ----@param config CopilotChat.config.shared?: The chat configuration ----@return CopilotChat.integrations.actions?: The prompt actions ----@deprecated Use |CopilotChat.select_prompt| instead -function M.prompt_actions(config) - local actions = {} - 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', - actions = actions, - } -end - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: vim.ui.select options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - opts = vim.tbl_extend('force', { - prompt = pick_actions.prompt .. '> ', - }, opts or {}) - - vim.ui.select(vim.tbl_keys(pick_actions.actions), opts, function(selected) - if not selected then - return - end - vim.defer_fn(function() - chat.ask(pick_actions.actions[selected].prompt, pick_actions.actions[selected]) - end, 100) - end) -end - -return M diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index dddd1b89..b6460d0a 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,19 +1,56 @@ ----@class CopilotChat.Client.ask +---@class CopilotChat.client.AskOptions ---@field headless boolean ----@field contexts table? ----@field selection CopilotChat.select.selection? ----@field embeddings table? +---@field history table +---@field selection CopilotChat.select.Selection? +---@field tools table? +---@field resources table? ---@field system_prompt string ---@field model string ----@field agent string? ---@field temperature number ---@field on_progress? fun(response: string):nil ----@class CopilotChat.Client.model : CopilotChat.Provider.model ----@field provider string - ----@class CopilotChat.Client.agent : CopilotChat.Provider.agent ----@field provider string +---@class CopilotChat.client.Message +---@field role string +---@field content string +---@field tool_call_id string? +---@field tool_calls table? + +---@class CopilotChat.client.AskResponse +---@field message CopilotChat.client.Message +---@field token_count number +---@field token_max_count number + +---@class CopilotChat.client.ToolCall +---@field id number +---@field index number +---@field name string +---@field arguments string + +---@class CopilotChat.client.Tool +---@field name string name of the tool +---@field description string description of the tool +---@field schema table? schema of the tool + +---@class CopilotChat.client.Embed +---@field index number +---@field embedding table + +---@class CopilotChat.client.Resource +---@field name string +---@field type string +---@field data string + +---@class CopilotChat.client.EmbeddedResource : CopilotChat.client.Resource, CopilotChat.client.Embed + +---@class CopilotChat.client.Model +---@field provider string? +---@field id string +---@field name string +---@field tokenizer string? +---@field max_input_tokens number? +---@field max_output_tokens number? +---@field streaming boolean? +---@field tools boolean? local log = require('plenary.log') local tiktoken = require('CopilotChat.tiktoken') @@ -22,7 +59,7 @@ local utils = require('CopilotChat.utils') local class = utils.class --- Constants -local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' +local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' local LINE_CHARACTERS = 100 local BIG_FILE_THRESHOLD = 1000 * LINE_CHARACTERS local BIG_EMBED_THRESHOLD = 200 * LINE_CHARACTERS @@ -30,8 +67,8 @@ local TRUNCATED = '... (truncated)' --- Resolve provider function ---@param model string ----@param models table ----@param providers table +---@param models table +---@param providers table ---@return string, function local function resolve_provider_function(name, model, models, providers) local model_config = models[model] @@ -65,23 +102,18 @@ local function resolve_provider_function(name, model, models, providers) end --- Generate content block with line numbers, truncating if necessary ----@param content string: The content ----@param outline string?: The outline +---@param content string ---@param threshold number: The threshold for truncation ----@param start_line number|nil: The starting line number +---@param start_line number?: The starting line number ---@return string -local function generate_content_block(content, outline, threshold, start_line) +local function generate_content_block(content, threshold, start_line) local total_chars = #content - if total_chars > threshold and outline then - content = outline - total_chars = #content - end if total_chars > threshold then content = content:sub(1, threshold) content = content .. '\n' .. TRUNCATED end - if start_line ~= -1 then + if start_line ~= nil then local lines = vim.split(content, '\n') local total_lines = #lines local max_length = #tostring(total_lines) @@ -96,44 +128,19 @@ local function generate_content_block(content, outline, threshold, start_line) return content end ---- Generate diagnostics message ----@param diagnostics table ----@return string -local function generate_diagnostics(diagnostics) - local out = {} - for _, diagnostic in ipairs(diagnostics) do - table.insert( - out, - string.format( - '%s line=%d-%d: %s', - diagnostic.severity, - diagnostic.start_line, - diagnostic.end_line, - diagnostic.content - ) - ) - end - return table.concat(out, '\n') -end - --- Generate messages for the given selection ---- @param selection CopilotChat.select.selection? ---- @return table -local function generate_selection_messages(selection) - if not selection then - return {} - end - +--- @param selection CopilotChat.select.Selection +--- @return CopilotChat.client.Message? +local function generate_selection_message(selection) local filename = selection.filename or 'unknown' local filetype = selection.filetype or 'text' local content = selection.content if not content or content == '' then - return {} + return nil end - local out = string.format('# FILE:%s CONTEXT\n', filename:upper()) - out = out .. "User's active selection:\n" + local out = "User's active selection:\n" if selection.start_line and selection.end_line then out = out .. string.format('Excerpt from %s, lines %s to %s:\n', filename, selection.start_line, selection.end_line) end @@ -141,103 +148,45 @@ local function generate_selection_messages(selection) .. string.format( '```%s\n%s\n```', filetype, - generate_content_block(content, nil, BIG_FILE_THRESHOLD, selection.start_line) + generate_content_block(content, BIG_FILE_THRESHOLD, selection.start_line) ) - if selection.diagnostics then - out = out - .. string.format("\nDiagnostics in user's active selection:\n%s", generate_diagnostics(selection.diagnostics)) - end - return { - { - name = filename, - context = string.format(CONTEXT_FORMAT, filename, filename), - content = out, - role = 'user', - }, + content = out, + role = 'user', } end ---- Generate messages for the given embeddings ---- @param embeddings table? ---- @return table -local function generate_embeddings_messages(embeddings) - if not embeddings then - return {} - end - - return vim.tbl_map(function(embedding) - local out = string.format( - '# FILE:%s CONTEXT\n```%s\n%s\n```', - embedding.filename:upper(), - embedding.filetype or 'text', - generate_content_block(embedding.content, embedding.outline, BIG_FILE_THRESHOLD) - ) - - if embedding.diagnostics then - out = out - .. string.format( - '\nFILE:%s DIAGNOSTICS:\n%s', - embedding.filename:upper(), - generate_diagnostics(embedding.diagnostics) - ) - end - - return { - name = embedding.filename, - context = string.format(CONTEXT_FORMAT, embedding.filename, embedding.filename), - content = out, - role = 'user', - } - end, embeddings) +--- Generate messages for the given resources +--- @param resources CopilotChat.client.Resource[] +--- @return table +local function generate_resource_messages(resources) + return vim + .iter(resources or {}) + :filter(function(resource) + return resource.data and resource.data ~= '' + end) + :map(function(resource) + local content = generate_content_block(resource.data, BIG_FILE_THRESHOLD, 1) + + return { + content = string.format(RESOURCE_FORMAT, resource.name, resource.type, content), + role = 'user', + } + end) + :totable() end --- Generate ask request ---- @param history table ---- @param contexts table? --- @param prompt string --- @param system_prompt string ---- @param generated_messages table -local function generate_ask_request(history, contexts, prompt, system_prompt, generated_messages) +--- @param history table +--- @param generated_messages table +local function generate_ask_request(prompt, system_prompt, history, generated_messages) local messages = {} system_prompt = vim.trim(system_prompt) - -- Include context help - if contexts and not vim.tbl_isempty(contexts) then - local help_text = [[When you need additional context, request it using this format: - -> #:`` - -Examples: -> #file:`path/to/file.js` (loads specific file) -> #buffers:`visible` (loads all visible buffers) -> #git:`staged` (loads git staged changes) -> #system:`uname -a` (loads system information) - -Guidelines: -- Always request context when needed rather than guessing about files or code -- Use the > format on a new line when requesting context -- Output context commands directly - never ask if the user wants to provide information -- Assume the user will provide requested context in their next response - -Available context providers and their usage:]] - - local context_names = vim.tbl_keys(contexts) - table.sort(context_names) - for _, name in ipairs(context_names) do - local description = contexts[name] - description = description:gsub('\n', '\n ') - help_text = help_text .. '\n\n - #' .. name .. ': ' .. description - end - - if system_prompt ~= '' then - system_prompt = system_prompt .. '\n\n' - end - system_prompt = system_prompt .. help_text - end - -- Include system prompt if not utils.empty(system_prompt) then table.insert(messages, { @@ -246,74 +195,48 @@ Available context providers and their usage:]] }) end - local context_references = {} - - -- Include embeddings and history + -- Include generated messages and history for _, message in ipairs(generated_messages) do table.insert(messages, { content = message.content, role = message.role, }) - - if message.context then - context_references[message.context] = true - end end for _, message in ipairs(history) do table.insert(messages, message) end - - -- Include context references - prompt = vim.trim(prompt) - if not vim.tbl_isempty(context_references) then - if prompt ~= '' then - prompt = '\n\n' .. prompt - end - prompt = table.concat(vim.tbl_keys(context_references), '\n') .. prompt - end - - -- Include user prompt - if not utils.empty(prompt) then + if not utils.empty(prompt) and utils.empty(history) then + -- Include user prompt if we have no history table.insert(messages, { content = prompt, role = 'user', }) end - log.debug('System prompt:\n', system_prompt) - log.debug('Prompt:\n', prompt) return messages end --- Generate embedding request ---- @param inputs table +--- @param inputs table --- @param threshold number --- @return table local function generate_embedding_request(inputs, threshold) return vim.tbl_map(function(embedding) - local content = generate_content_block(embedding.outline or embedding.content, nil, threshold, -1) - if embedding.filetype == 'raw' then - return content - else - return string.format('File: `%s`\n```%s\n%s\n```', embedding.filename, embedding.filetype, content) - end + local content = generate_content_block(embedding.data, threshold) + return string.format(RESOURCE_FORMAT, embedding.name, embedding.type, content) end, inputs) end ----@class CopilotChat.Client : Class ----@field history table ----@field providers table ----@field provider_cache table ----@field models table? ----@field agents table? ----@field current_job string? ----@field headers table? +---@class CopilotChat.client.Client : Class +---@field private providers table +---@field private provider_cache table +---@field private models table? +---@field private current_job string? +---@field private headers table? local Client = class(function(self) - self.history = {} self.providers = {} self.provider_cache = {} self.models = nil - self.agents = nil self.current_job = nil self.headers = nil end) @@ -336,7 +259,7 @@ function Client:authenticate(provider_name) end --- Fetch models from the Copilot API ----@return table +---@return table function Client:fetch_models() if self.models then return self.models @@ -372,67 +295,23 @@ function Client:fetch_models() end end - log.debug('Fetched models:', vim.inspect(models)) + log.debug('Fetched models:', #vim.tbl_keys(models)) self.models = models return self.models end ---- Fetch agents from the Copilot API ----@return table -function Client:fetch_agents() - if self.agents then - return self.agents - end - - local agents = {} - local provider_order = vim.tbl_keys(self.providers) - table.sort(provider_order) - for _, provider_name in ipairs(provider_order) do - local provider = self.providers[provider_name] - if not provider.disabled and provider.get_agents then - notify.publish(notify.STATUS, 'Fetching agents from ' .. provider_name) - local ok, headers = pcall(self.authenticate, self, provider_name) - if not ok then - log.warn('Failed to authenticate with ' .. provider_name .. ': ' .. headers) - goto continue - end - local ok, provider_agents = pcall(provider.get_agents, headers) - if not ok then - log.warn('Failed to fetch agents from ' .. provider_name .. ': ' .. provider_agents) - goto continue - end - - for _, agent in ipairs(provider_agents) do - agent.provider = provider_name - if agents[agent.id] then - agent.id = agent.id .. ':' .. provider_name - end - agents[agent.id] = agent - end - - ::continue:: - end - end - - self.agents = agents - return self.agents -end - --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ----@param opts CopilotChat.Client.ask: Options for the request ----@return string?, table?, number?, number? +---@param opts CopilotChat.client.AskOptions: Options for the request +---@return CopilotChat.client.AskResponse? function Client:ask(prompt, opts) opts = opts or {} - - if opts.agent == 'none' or opts.agent == 'copilot' then - opts.agent = nil - end - local job_id = utils.uuid() log.debug('Model:', opts.model) - log.debug('Agent:', opts.agent) + log.debug('Tools:', #opts.tools) + log.debug('Resources:', #opts.resources) + log.debug('History:', #opts.history) local models = self:fetch_models() local model_config = models[opts.model] @@ -440,12 +319,6 @@ function Client:ask(prompt, opts) error('Model not found: ' .. opts.model) end - local agents = self:fetch_agents() - local agent_config = opts.agent and agents[opts.agent] - if opts.agent and not agent_config then - error('Agent not found: ' .. opts.agent) - end - local provider_name = model_config.provider if not provider_name then error('Provider not found for model: ' .. opts.model) @@ -459,10 +332,8 @@ function Client:ask(prompt, opts) model = vim.tbl_extend('force', model_config, { id = opts.model:gsub(':' .. provider_name .. '$', ''), }), - agent = agent_config and vim.tbl_extend('force', agent_config, { - id = opts.agent and opts.agent:gsub(':' .. provider_name .. '$', ''), - }), temperature = opts.temperature, + tools = opts.tools, } local max_tokens = model_config.max_input_tokens @@ -477,37 +348,26 @@ function Client:ask(prompt, opts) notify.publish(notify.STATUS, 'Generating request') end - local history = not opts.headless and vim.list_slice(self.history) or {} - local references = utils.ordered_map() + local history = not opts.headless and vim.deepcopy(opts.history) or {} + local tool_calls = utils.ordered_map() local generated_messages = {} - local selection_messages = generate_selection_messages(opts.selection) - local embeddings_messages = generate_embeddings_messages(opts.embeddings) - - for _, message in ipairs(selection_messages) do - table.insert(generated_messages, message) - references:set(message.name, { - name = utils.filename(message.name), - url = message.name, - }) + local selection_message = opts.selection and generate_selection_message(opts.selection) + local resource_messages = generate_resource_messages(opts.resources) + + if selection_message then + table.insert(generated_messages, selection_message) end if max_tokens then - -- Count tokens from selection messages - local selection_tokens = 0 - for _, message in ipairs(selection_messages) do - selection_tokens = selection_tokens + tiktoken.count(message.content) - end - -- Count required tokens that we cannot reduce + local selection_tokens = selection_message and tiktoken.count(selection_message.content) or 0 local prompt_tokens = tiktoken.count(prompt) local system_tokens = tiktoken.count(opts.system_prompt) - local required_tokens = prompt_tokens + system_tokens + selection_tokens - - -- Reserve space for first embedding - local reserved_tokens = #embeddings_messages > 0 and tiktoken.count(embeddings_messages[1].content) or 0 + local resource_tokens = #resource_messages > 0 and tiktoken.count(resource_messages[1].content) or 0 + local required_tokens = prompt_tokens + system_tokens + selection_tokens + resource_tokens -- Calculate how many tokens we can use for history - local history_limit = max_tokens - required_tokens - reserved_tokens + local history_limit = max_tokens - required_tokens local history_tokens = 0 for _, msg in ipairs(history) do history_tokens = history_tokens + tiktoken.count(msg.content) @@ -521,35 +381,25 @@ function Client:ask(prompt, opts) -- Now add as many files as possible with remaining token budget local remaining_tokens = max_tokens - required_tokens - history_tokens - for _, message in ipairs(embeddings_messages) do + for _, message in ipairs(resource_messages) do local tokens = tiktoken.count(message.content) if remaining_tokens - tokens >= 0 then remaining_tokens = remaining_tokens - tokens table.insert(generated_messages, message) - references:set(message.name, { - name = utils.filename(message.name), - url = message.name, - }) else break end end else -- Add all embedding messages as we cant limit them - for _, message in ipairs(embeddings_messages) do + for _, message in ipairs(resource_messages) do table.insert(generated_messages, message) - references:set(message.name, { - name = utils.filename(message.name), - url = message.name, - }) end end - log.debug('References:', #generated_messages) - - local last_message = nil local errored = false local finished = false + local token_count = 0 local response_buffer = utils.string_buffer() local function finish_stream(err, job) @@ -571,7 +421,6 @@ function Client:ask(prompt, opts) return end - log.debug('Response line:', line) if not opts.headless then notify.publish(notify.STATUS, '') end @@ -589,11 +438,19 @@ function Client:ask(prompt, opts) end local out = provider.prepare_output(content, options) - last_message = out - if out.references then - for _, reference in ipairs(out.references) do - references:set(reference.name, reference) + if out.total_tokens then + token_count = out.total_tokens + end + + if out.tool_calls then + for _, tool_call in ipairs(out.tool_calls) do + local val = tool_calls:get(tool_call.index) + if not val then + tool_calls:set(tool_call.index, tool_call) + else + val.arguments = val.arguments .. tool_call.arguments + end end end @@ -606,7 +463,7 @@ function Client:ask(prompt, opts) if out.finish_reason then local reason = out.finish_reason - if reason == 'stop' then + if reason == 'stop' or reason == 'tool_calls' then reason = nil else reason = 'Early stop: ' .. reason @@ -656,10 +513,8 @@ function Client:ask(prompt, opts) end local headers = self:authenticate(provider_name) - local request = provider.prepare_input( - generate_ask_request(history, opts.contexts, prompt, opts.system_prompt, generated_messages), - options - ) + local request = + provider.prepare_input(generate_ask_request(prompt, opts.system_prompt, history, generated_messages), options) local is_stream = request.stream local args = { @@ -681,12 +536,6 @@ function Client:ask(prompt, opts) self.current_job = nil end - if response then - log.debug('Response status:', response.status) - log.debug('Response body:\n', response.body) - log.debug('Response headers:\n', response.headers) - end - if err then local error_msg = 'Failed to get response: ' .. err @@ -716,7 +565,7 @@ function Client:ask(prompt, opts) if response then if is_stream then - if utils.empty(response_text) then + if utils.empty(response_text) and not finished then for _, line in ipairs(vim.split(response.body, '\n')) do parse_stream_line(line) end @@ -727,12 +576,15 @@ function Client:ask(prompt, opts) response_text = response_buffer:tostring() end - if utils.empty(response_text) then - error('Failed to get response: empty response') - return - end - - return response_text, references:values(), last_message and last_message.total_tokens or 0, max_tokens + return { + message = { + role = 'assistant', + content = response_text, + tool_calls = #tool_calls:values() > 0 and tool_calls:values() or nil, + }, + token_count = token_count, + token_max_count = max_tokens, + } end --- List available models @@ -755,40 +607,20 @@ function Client:list_models() end, result) end ---- List available agents ----@return table -function Client:list_agents() - local agents = self:fetch_agents() - local result = vim.tbl_keys(agents) - - table.sort(result, function(a, b) - a = agents[a] - b = agents[b] - if a.provider ~= b.provider then - return a.provider < b.provider - end - return a.id < b.id - end) - - local out = vim.tbl_map(function(id) - return agents[id] - end, result) - table.insert(out, 1, { id = 'none', name = 'None', description = 'No agent', provider = 'none' }) - return out -end - --- Generate embeddings for the given inputs ----@param inputs table: The inputs to embed +---@param inputs table: The inputs to embed ---@param model string ----@return table +---@return table function Client:embed(inputs, model) if not inputs or #inputs == 0 then + ---@diagnostic disable-next-line: return-type-mismatch return inputs end local models = self:fetch_models() local ok, provider_name, embed = pcall(resolve_provider_function, 'embed', model, models, self.providers) if not ok then + ---@diagnostic disable-next-line: return-type-mismatch return inputs end @@ -867,14 +699,6 @@ function Client:stop() return false end ---- Reset the history and stop any running job ----@return boolean -function Client:reset() - local stopped = self:stop() - self.history = {} - return stopped -end - --- Check if there is a running job ---@return boolean function Client:running() @@ -889,5 +713,5 @@ function Client:load_providers(providers) end end ---- @type CopilotChat.Client +--- @type CopilotChat.client.Client return Client() diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 30c428e5..5e900219 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -1,8 +1,6 @@ -local select = require('CopilotChat.select') - ---@alias CopilotChat.config.Layout 'vertical'|'horizontal'|'float'|'replace' ----@class CopilotChat.config.window +---@class CopilotChat.config.Window ---@field layout? CopilotChat.config.Layout|fun():CopilotChat.config.Layout ---@field relative 'editor'|'win'|'cursor'|'mouse'? ---@field border 'none'|'single'|'double'|'rounded'|'solid'|'shadow'? @@ -14,32 +12,28 @@ local select = require('CopilotChat.select') ---@field footer string? ---@field zindex number? ----@class CopilotChat.config.shared +---@class CopilotChat.config.Shared ---@field system_prompt string? ---@field model string? ----@field agent string? ----@field context string|table|nil +---@field tools string|table|nil ---@field sticky string|table|nil ---@field temperature number? ---@field headless boolean? ----@field stream nil|fun(chunk: string, source: CopilotChat.source):string ----@field callback nil|fun(response: string, source: CopilotChat.source):string +---@field callback nil|fun(response: string, source: CopilotChat.source) ---@field remember_as_sticky boolean? ----@field include_contexts_in_prompt boolean? ----@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.selection? ----@field window CopilotChat.config.window? +---@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.Selection? +---@field window CopilotChat.config.Window? ---@field show_help boolean? ---@field show_folds boolean? ---@field highlight_selection boolean? ---@field highlight_headers boolean? ----@field references_display 'virtual'|'write'? ---@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 +---@class CopilotChat.config.Config : CopilotChat.config.Shared ---@field debug boolean? ---@field log_level 'trace'|'debug'|'info'|'warn'|'error'|'fatal'? ---@field proxy string? @@ -47,13 +41,11 @@ local select = require('CopilotChat.select') ---@field chat_autocomplete boolean? ---@field log_path string? ---@field history_path string? ----@field question_header string? ----@field answer_header string? ----@field error_header string? +---@field headers table? ---@field separator string? ----@field providers table? ----@field contexts table? ----@field prompts table? +---@field providers table? +---@field functions table? +---@field prompts table? ---@field mappings CopilotChat.config.mappings? return { @@ -62,20 +54,16 @@ return { system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - agent = 'none', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - temperature = 0.1, -- GPT result temperature + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) - callback = nil, -- Function called when full response is received (retuned string is stored to history) - remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions - - include_contexts_in_prompt = true, -- Include contexts in prompt + callback = nil, -- Function called when full response is received + remember_as_sticky = true, -- Remember model as sticky prompts when asking questions -- default selection - selection = select.visual, + selection = require('CopilotChat.select').visual, -- default window options window = { @@ -96,7 +84,6 @@ return { 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) - references_display = 'virtual', -- 'virtual', 'write', Display references in chat as virtual text or write to buffer 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 @@ -114,16 +101,19 @@ return { log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file 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 + headers = { + user = '## User ', -- Header to use for user questions + assistant = '## Copilot ', -- Header to use for AI answers + tool = '## Tool ', -- Header to use for tool calls + }, + separator = '───', -- Separator to use in chat -- default providers providers = require('CopilotChat.config.providers'), - -- default contexts - contexts = require('CopilotChat.config.contexts'), + -- default functions + functions = require('CopilotChat.config.functions'), -- default prompts prompts = require('CopilotChat.config.prompts'), diff --git a/lua/CopilotChat/config/contexts.lua b/lua/CopilotChat/config/contexts.lua deleted file mode 100644 index 64758f76..00000000 --- a/lua/CopilotChat/config/contexts.lua +++ /dev/null @@ -1,352 +0,0 @@ -local context = require('CopilotChat.context') -local utils = require('CopilotChat.utils') - ----@class CopilotChat.config.context ----@field description string? ----@field input fun(callback: fun(input: string?), source: CopilotChat.source)? ----@field resolve fun(input: string?, source: CopilotChat.source, prompt: string):table - ----@type table -return { - buffer = { - description = 'Includes specified buffer in chat context. Supports input (default current).', - input = function(callback) - vim.ui.select( - vim.tbl_map( - function(buf) - return { id = buf, name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:.') } - end, - vim.tbl_filter(function(buf) - return utils.buf_valid(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) - input = input and tonumber(input) or source.bufnr - - utils.schedule_main() - return { - context.get_buffer(input), - } - end, - }, - - buffers = { - description = 'Includes all buffers in chat context. Supports input (default listed).', - input = function(callback) - vim.ui.select({ 'listed', 'visible' }, { - prompt = 'Select buffer scope> ', - }, callback) - end, - resolve = function(input) - input = input or 'listed' - - utils.schedule_main() - return vim.tbl_map( - context.get_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()) - ) - end, - }, - - file = { - description = 'Includes content of provided file in chat context. Supports input.', - input = function(callback, source) - local files = utils.scan_dir(source.cwd(), { - max_count = 0, - }) - - utils.schedule_main() - vim.ui.select(files, { - prompt = 'Select a file> ', - }, callback) - end, - resolve = function(input) - if not input or input == '' then - return {} - end - - utils.schedule_main() - return { - context.get_file(utils.filepath(input), utils.filetype(input)), - } - end, - }, - - files = { - description = 'Includes all non-hidden files in the current workspace in chat context. Supports input (glob pattern).', - input = function(callback) - vim.ui.input({ - prompt = 'Enter glob> ', - }, callback) - end, - resolve = function(input, source) - local files = utils.scan_dir(source.cwd(), { - glob = input, - }) - - utils.schedule_main() - 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) - ) - - return vim - .iter(files) - :map(function(file) - return context.get_file(file.name, file.ft) - end) - :filter(function(file_data) - return file_data ~= nil - end) - :totable() - end, - }, - - filenames = { - description = 'Includes names of all non-hidden files in the current workspace in chat context. Supports input (glob pattern).', - input = function(callback) - vim.ui.input({ - prompt = 'Enter glob> ', - }, callback) - end, - resolve = function(input, source) - local out = {} - local files = utils.scan_dir(source.cwd(), { - glob = input, - }) - - 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 - - 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 = chunk_name, - filetype = 'text', - score = 0.1, - }) - end - - return out - end, - }, - - git = { - description = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number).', - input = function(callback) - vim.ui.select({ 'unstaged', 'staged' }, { - prompt = 'Select diff type> ', - }, callback) - end, - resolve = function(input, source) - input = input or 'unstaged' - local cmd = { - 'git', - '-C', - source.cwd(), - 'diff', - '--no-color', - '--no-ext-diff', - } - - if input == 'staged' then - table.insert(cmd, '--staged') - elseif input == 'unstaged' then - table.insert(cmd, '--') - else - table.insert(cmd, input) - end - - local out = utils.system(cmd) - - return { - { - content = out.stdout, - filename = 'git_diff_' .. input, - filetype = 'diff', - }, - } - end, - }, - - url = { - description = '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.get_url(input), - } - end, - }, - - register = { - description = 'Includes contents of register in chat context. Supports input (default +, e.g clipboard).', - input = function(callback) - local choices = utils.kv_list({ - ['+'] = '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(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 '+' - - utils.schedule_main() - local lines = vim.fn.getreg(input) - if not lines or lines == '' then - return {} - end - - return { - { - content = lines, - filename = 'vim_register_' .. input, - filetype = '', - }, - } - end, - }, - - quickfix = { - description = 'Includes quickfix list file contents in chat context.', - resolve = function() - utils.schedule_main() - - local items = vim.fn.getqflist() - if not items or #items == 0 then - return {} - end - - local unique_files = {} - for _, item in ipairs(items) do - local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) - if filename then - unique_files[filename] = true - end - end - - local 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, vim.tbl_keys(unique_files)) - ) - - return vim - .iter(files) - :map(function(file) - return context.get_file(file.name, file.ft) - end) - :filter(function(file_data) - return file_data ~= nil - end) - :totable() - end, - }, - - system = { - description = [[Includes output of provided system shell command in chat context. Supports input. - -Important: -- Only use system commands as last resort, they are run every time the context is requested. -- For example instead of curl use the url context, instead of finding and grepping try to check if there is any context that can query the data you need instead. -- If you absolutely need to run a system command, try to use read-only commands and avoid commands that modify the system state. -]], - input = function(callback) - vim.ui.input({ - prompt = 'Enter command> ', - }, callback) - end, - resolve = function(input) - if not input or input == '' then - return {} - end - - utils.schedule_main() - - local shell, shell_flag - if vim.fn.has('win32') == 1 then - shell, shell_flag = 'cmd.exe', '/c' - else - shell, shell_flag = 'sh', '-c' - end - - local out = utils.system({ shell, shell_flag, input }) - if not out then - return {} - end - - local out_type = 'command_output' - local out_text = out.stdout - if out.code ~= 0 then - out_type = 'command_error' - if out.stderr and out.stderr ~= '' then - out_text = out.stderr - elseif not out_text or out_text == '' then - out_text = 'Command failed with exit code ' .. out.code - end - end - - return { - { - content = out_text, - filename = out_type .. '_' .. input:gsub('[^%w]', '_'):sub(1, 20), - filetype = 'text', - }, - } - end, - }, -} diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua new file mode 100644 index 00000000..07b11573 --- /dev/null +++ b/lua/CopilotChat/config/functions.lua @@ -0,0 +1,503 @@ +local resources = require('CopilotChat.resources') +local utils = require('CopilotChat.utils') + +---@class CopilotChat.config.functions.Result +---@field data string +---@field mimetype string? +---@field uri string? + +---@class CopilotChat.config.functions.Function +---@field description string? +---@field schema table? +---@field group string? +---@field uri string? +---@field resolve fun(input: table, source: CopilotChat.source, prompt: string):table + +---@type table +return { + file = { + group = 'copilot', + uri = 'file://{path}', + description = 'Reads content from a specified file path, even if the file is not currently loaded as a buffer.', + + schema = { + type = 'object', + required = { 'path' }, + properties = { + path = { + type = 'string', + description = 'Path to file to include in chat context.', + enum = function(source) + return utils.glob(source.cwd(), { + max_count = 0, + }) + end, + }, + }, + }, + + resolve = function(input) + local data, mimetype = resources.get_file(input.path) + if not data then + error('File not found: ' .. input.path) + end + + return { + { + uri = 'file://' .. input.path, + mimetype = mimetype, + data = data, + }, + } + end, + }, + + glob = { + group = 'copilot', + uri = 'files://glob/{pattern}', + description = 'Lists filenames matching a pattern in your workspace. Useful for discovering relevant files or understanding the project structure.', + + schema = { + type = 'object', + required = { 'pattern' }, + properties = { + pattern = { + type = 'string', + description = 'Glob pattern to match files.', + default = '**/*', + }, + }, + }, + + resolve = function(input, source) + local files = utils.glob(source.cwd(), { + pattern = input.pattern, + }) + + return { + { + uri = 'files://glob/' .. input.pattern, + mimetype = 'text/plain', + data = table.concat(files, '\n'), + }, + } + end, + }, + + grep = { + group = 'copilot', + uri = 'files://grep/{pattern}', + description = 'Searches for a pattern across files in your workspace. Helpful for finding specific code elements or patterns.', + + schema = { + type = 'object', + required = { 'pattern' }, + properties = { + pattern = { + type = 'string', + description = 'Pattern to search for.', + }, + }, + }, + + resolve = function(input, source) + local files = utils.grep(source.cwd(), { + pattern = input.pattern, + }) + + return { + { + uri = 'files://grep/' .. input.pattern, + mimetype = 'text/plain', + data = table.concat(files, '\n'), + }, + } + end, + }, + + buffer = { + group = 'copilot', + uri = 'neovim://buffer/{name}', + description = 'Retrieves content from a specific buffer. Useful for discussing or analyzing code from a particular file that is currently loaded.', + + schema = { + type = 'object', + required = { 'name' }, + properties = { + name = { + type = 'string', + description = 'Buffer filename to include in chat context.', + enum = function() + return vim + .iter(vim.api.nvim_list_bufs()) + :filter(function(buf) + return buf and utils.buf_valid(buf) and vim.fn.buflisted(buf) == 1 + end) + :map(function(buf) + return vim.api.nvim_buf_get_name(buf) + end) + :totable() + end, + }, + }, + }, + + resolve = function(input, source) + utils.schedule_main() + local name = input.name or vim.api.nvim_buf_get_name(source.bufnr) + local found_buf = nil + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_get_name(buf) == name then + found_buf = buf + break + end + end + if not found_buf then + error('Buffer not found: ' .. name) + end + local data, mimetype = resources.get_buffer(found_buf) + if not data then + error('Buffer not found: ' .. name) + end + return { + { + uri = 'neovim://buffer/' .. name, + mimetype = mimetype, + data = data, + }, + } + end, + }, + + buffers = { + group = 'copilot', + uri = 'neovim://buffers/{scope}', + description = 'Fetches content from multiple buffers. Helps with discussing or analyzing code across multiple files simultaneously.', + + schema = { + type = 'object', + required = { 'scope' }, + properties = { + scope = { + type = 'string', + description = 'Scope of buffers to include in chat context.', + enum = { 'listed', 'visible' }, + default = 'listed', + }, + }, + }, + + resolve = function(input) + utils.schedule_main() + return vim + .iter(vim.api.nvim_list_bufs()) + :filter(function(bufnr) + return utils.buf_valid(bufnr) + and vim.fn.buflisted(bufnr) == 1 + and (input.scope == 'listed' or #vim.fn.win_findbuf(bufnr) > 0) + end) + :map(function(bufnr) + local name = vim.api.nvim_buf_get_name(bufnr) + local data, mimetype = resources.get_buffer(bufnr) + if not data then + return nil + end + return { + uri = 'neovim://buffer/' .. name, + mimetype = mimetype, + data = data, + } + end) + :filter(function(file_data) + return file_data ~= nil + end) + :totable() + end, + }, + + quickfix = { + group = 'copilot', + uri = 'neovim://quickfix', + description = 'Includes the content of all files referenced in the current quickfix list. Useful for discussing compilation errors, search results, or other collected locations.', + + resolve = function() + utils.schedule_main() + + local items = vim.fn.getqflist() + if not items or #items == 0 then + return {} + end + + local unique_files = {} + for _, item in ipairs(items) do + local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) + if filename then + unique_files[filename] = true + end + end + + return vim + .iter(vim.tbl_keys(unique_files)) + :map(function(file) + local data, mimetype = resources.get_file(file) + if not data then + return nil + end + return { + uri = 'file://' .. file, + mimetype = mimetype, + data = data, + } + end) + :filter(function(file_data) + return file_data ~= nil + end) + :totable() + end, + }, + + diagnostics = { + group = 'copilot', + uri = 'neovim://diagnostics/{scope}', + description = 'Collects code diagnostics (errors, warnings, etc.) from specified buffers. Helpful for troubleshooting and fixing code issues.', + + schema = { + type = 'object', + required = { 'scope' }, + properties = { + scope = { + type = 'string', + description = 'Scope of buffers to use for retrieving diagnostics.', + enum = { 'current', 'listed', 'visible' }, + default = 'current', + }, + severity = { + type = 'string', + description = 'Minimum severity level of diagnostics to include.', + enum = { 'error', 'warn', 'info', 'hint' }, + default = 'warn', + }, + }, + }, + + resolve = function(input, source) + utils.schedule_main() + local out = {} + local scope = input.scope or 'current' + local buffers = {} + + -- Get buffers based on scope + if scope == 'current' then + if source and source.bufnr and utils.buf_valid(source.bufnr) then + buffers = { source.bufnr } + end + elseif scope == 'listed' then + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 + end, vim.api.nvim_list_bufs()) + elseif scope == 'visible' then + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 and #vim.fn.win_findbuf(b) > 0 + end, vim.api.nvim_list_bufs()) + else + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.api.nvim_buf_get_name(b) == input.scope + end, vim.api.nvim_list_bufs()) + end + + -- Collect diagnostics for each buffer + for _, bufnr in ipairs(buffers) do + local name = vim.api.nvim_buf_get_name(bufnr) + local diagnostics = vim.diagnostic.get(bufnr, { + severity = { + min = vim.diagnostic.severity[input.severity:upper()], + }, + }) + + if #diagnostics > 0 then + local diag_lines = {} + for _, diag in ipairs(diagnostics) do + local severity = vim.diagnostic.severity[diag.severity] or 'UNKNOWN' + local line_text = vim.api.nvim_buf_get_lines(bufnr, diag.lnum, diag.lnum + 1, false)[1] or '' + + table.insert( + diag_lines, + string.format( + '%s line=%d-%d: %s\n > %s', + severity, + diag.lnum + 1, + diag.end_lnum and (diag.end_lnum + 1) or (diag.lnum + 1), + diag.message, + line_text + ) + ) + end + + table.insert(out, { + uri = 'neovim://diagnostics/' .. name, + mimetype = 'text/plain', + data = table.concat(diag_lines, '\n'), + }) + end + end + + return out + end, + }, + + register = { + group = 'copilot', + uri = 'neovim://register/{register}', + description = 'Provides access to the content of a specified Vim register. Useful for discussing yanked text, clipboard content, or previously executed commands.', + + schema = { + type = 'object', + required = { 'register' }, + properties = { + register = { + type = 'string', + description = 'Register to include in chat context.', + enum = { + '+', + '*', + '"', + '0', + '-', + '.', + '%', + ':', + '#', + '=', + '/', + }, + default = '+', + }, + }, + }, + + resolve = function(input) + utils.schedule_main() + local lines = vim.fn.getreg(input.register) + if not lines or lines == '' then + return {} + end + + return { + { + uri = 'neovim://register/' .. input.register, + mimetype = 'text/plain', + data = lines, + }, + } + end, + }, + + gitdiff = { + group = 'copilot', + uri = 'git://diff/{target}', + description = 'Retrieves git diff information. Requires git to be installed. Useful for discussing code changes or explaining the purpose of modifications.', + + schema = { + type = 'object', + required = { 'target' }, + properties = { + target = { + type = 'string', + description = 'Target to diff against.', + enum = { 'unstaged', 'staged', '' }, + default = 'unstaged', + }, + }, + }, + + resolve = function(input, source) + local cmd = { + 'git', + '-C', + source.cwd(), + 'diff', + '--no-color', + '--no-ext-diff', + } + + if input.target == 'staged' then + table.insert(cmd, '--staged') + elseif input.target == 'unstaged' then + table.insert(cmd, '--') + else + table.insert(cmd, input.target) + end + + local out = utils.system(cmd) + + return { + { + uri = 'git://diff/' .. input.target, + mimetype = 'text/plain', + data = out.stdout, + }, + } + end, + }, + + gitstatus = { + group = 'copilot', + uri = 'git://status', + description = 'Retrieves the status of the current git repository. Useful for discussing changes, commits, and other git-related tasks.', + + resolve = function(_, source) + local cmd = { + 'git', + '-C', + source.cwd(), + 'status', + } + + local out = utils.system(cmd) + + return { + { + uri = 'git://status', + mimetype = 'text/plain', + data = out.stdout, + }, + } + end, + }, + + url = { + group = 'copilot', + uri = 'https://{url}', + description = 'Fetches content from a specified URL. Useful for referencing documentation, examples, or other online resources.', + + schema = { + type = 'object', + required = { 'url' }, + properties = { + url = { + type = 'string', + description = 'URL to include in chat context.', + }, + }, + }, + + resolve = function(input) + if not input.url:match('^https?://') then + input.url = 'https://' .. input.url + end + + local data, mimetype = resources.get_url(input.url) + if not data then + error('URL not found: ' .. input.url) + end + + return { + { + uri = input.url, + mimetype = mimetype, + data = data, + }, + } + end, + }, +} diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index baf2f82c..a527f0c3 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,9 +1,8 @@ local async = require('plenary.async') local copilot = require('CopilotChat') -local client = require('CopilotChat.client') local utils = require('CopilotChat.utils') ----@class CopilotChat.config.mappings.diff +---@class CopilotChat.config.mappings.Diff ---@field change string ---@field reference string ---@field filename string @@ -13,8 +12,8 @@ local utils = require('CopilotChat.utils') ---@field bufnr number? --- Get diff data from a block ----@param block CopilotChat.ui.Chat.Section.Block? ----@return CopilotChat.config.mappings.diff? +---@param block CopilotChat.ui.chat.Block? +---@return CopilotChat.config.mappings.Diff? local function get_diff(block) -- If no block found, return nil if not block then @@ -44,7 +43,7 @@ local function get_diff(block) end filename = header.filename - filetype = header.filetype or vim.filetype.match({ filename = filename }) + filetype = header.filetype or utils.filetype(filename) start_line = header.start_line end_line = header.end_line @@ -64,7 +63,7 @@ local function get_diff(block) change = block.content, reference = reference or '', filetype = filetype or '', - filename = utils.filepath(filename), + filename = filename, start_line = start_line, end_line = end_line, bufnr = bufnr, @@ -72,9 +71,9 @@ local function get_diff(block) end --- Prepare a buffer for applying a diff ----@param diff CopilotChat.config.mappings.diff? +---@param diff CopilotChat.config.mappings.Diff? ---@param source CopilotChat.source? ----@return CopilotChat.config.mappings.diff? +---@return CopilotChat.config.mappings.Diff? local function prepare_diff_buffer(diff, source) if not diff then return diff @@ -163,19 +162,20 @@ return { normal = '', insert = '', callback = function() - local section = copilot.chat:get_closest_section('question') - if not section or section.answer then + local message = copilot.chat:get_closest_message('user') + if not message then return end - copilot.ask(section.content) + copilot.ask(message.content) end, }, toggle_sticky = { normal = 'grr', callback = function() - local section = copilot.chat:get_prompt() + local message = copilot.chat:get_message('user') + local section = message and message.section if not section then return end @@ -205,12 +205,13 @@ return { clear_stickies = { normal = 'grx', callback = function() - local section = copilot.chat:get_prompt() + local message = copilot.chat:get_message('user') + local section = message and message.section if not section then return end - local lines = vim.split(section.content, '\n') + local lines = vim.split(message.content, '\n') local new_lines = {} local changed = false @@ -223,7 +224,8 @@ return { end if changed then - copilot.chat:set_prompt(vim.trim(table.concat(new_lines, '\n'))) + message.content = table.concat(new_lines, '\n') + copilot.chat:add_message(message, true) end end, }, @@ -262,7 +264,7 @@ return { callback = function() local items = {} for i, section in ipairs(copilot.chat.sections) do - if section.answer then + if section.role == 'assistant' then local prev_section = copilot.chat.sections[i - 1] local text = '' if prev_section then @@ -352,7 +354,8 @@ return { -- Apply all diffs from same file if #modified > 0 then -- Find all diffs from the same file in this section - local section = copilot.chat:get_closest_section('answer') + local message = copilot.chat:get_closest_message('assistant') + local section = message and message.section local same_file_diffs = {} if section then for _, block in ipairs(section.blocks) do @@ -422,20 +425,23 @@ return { }, show_info = { - normal = 'gi', + normal = 'gc', callback = function(source) - local section = copilot.chat:get_closest_section('question') - if not section or section.answer then + local message = copilot.chat:get_closest_message('user') + if not message then return end local lines = {} - local config, prompt = copilot.resolve_prompt(section.content) + local config, prompt = copilot.resolve_prompt(message.content) local system_prompt = config.system_prompt async.run(function() - local selected_agent = copilot.resolve_agent(prompt, config) local selected_model = copilot.resolve_model(prompt, config) + local selected_tools, resolved_resources = copilot.resolve_functions(prompt, config) + selected_tools = vim.tbl_map(function(tool) + return tool.name + end, selected_tools) utils.schedule_main() table.insert(lines, '**Logs**: `' .. copilot.config.log_path .. '`') @@ -454,82 +460,55 @@ return { table.insert(lines, '') end - if selected_agent then - table.insert(lines, '**Agent**: `' .. selected_agent .. '`') + if not utils.empty(selected_tools) then + table.insert(lines, '**Tools**') + table.insert(lines, '```') + table.insert(lines, table.concat(selected_tools, ', ')) + table.insert(lines, '```') table.insert(lines, '') end if system_prompt then table.insert(lines, '**System Prompt**') - table.insert(lines, '```') + 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, '````') table.insert(lines, '') end - if client.memory then - table.insert(lines, '**Memory**') - table.insert(lines, '```markdown') - for _, line in ipairs(vim.split(client.memory.content, '\n')) do + local selection = copilot.get_selection() + if selection then + table.insert(lines, '**Selection**') + table.insert(lines, '') + table.insert( + lines, + string.format('**%s** (%s-%s)', selection.filename, selection.start_line, selection.end_line) + ) + table.insert(lines, string.format('````%s', selection.filetype)) + for _, line in ipairs(vim.split(selection.content, '\n')) do table.insert(lines, line) end - table.insert(lines, '```') + table.insert(lines, '````') table.insert(lines, '') end - if not utils.empty(client.history) then - table.insert(lines, ('**History** (#%s, truncated)'):format(#client.history)) + if not utils.empty(resolved_resources) then + table.insert(lines, '**Resources**') table.insert(lines, '') - - for _, message in ipairs(client.history) do - table.insert(lines, '**' .. message.role .. '**') - table.insert(lines, '`' .. vim.split(message.content, '\n')[1] .. '`') - end end - copilot.chat:overlay({ - text = vim.trim(table.concat(lines, '\n')) .. '\n', - }) - end) - end, - }, - - show_context = { - normal = 'gc', - callback = function() - local section = copilot.chat:get_closest_section('question') - if not section or section.answer then - return - end - - local lines = {} - - local selection = copilot.get_selection() - 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 = copilot.resolve_context(section.content) - - for _, embedding in ipairs(embeddings) do - 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 + for _, resource in ipairs(resolved_resources) do + local resource_lines = vim.split(resource.data, '\n') + local preview = vim.list_slice(resource_lines, 1, math.min(10, #resource_lines)) + local header = string.format('**%s** (%s lines)', resource.name, #resource_lines) + if #resource_lines > 10 then header = header .. ' (truncated)' end table.insert(lines, header) - table.insert(lines, '```' .. embedding.filetype) + table.insert(lines, '```' .. resource.type) for _, line in ipairs(preview) do table.insert(lines, line) end @@ -537,7 +516,6 @@ return { table.insert(lines, '') end - utils.schedule_main() copilot.chat:overlay({ text = vim.trim(table.concat(lines, '\n')) .. '\n', }) @@ -549,9 +527,9 @@ return { normal = 'gh', callback = 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' - chat_help = chat_help .. '`#:` to select input for context\n' + chat_help = chat_help .. '`@` to share function\n' + chat_help = chat_help .. '`#` to add resource\n' + chat_help = chat_help .. '`#:` to add resource with input\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' diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 85552adf..9fca7d8e 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -1,35 +1,77 @@ -local COPILOT_BASE = string.format( - [[ +local COPILOT_BASE = [[ 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. -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 user works in editor called Neovim which has these core concepts: +- Buffer: An in-memory text content that may be associated with a file +- Window: A viewport that displays a buffer +- Tab: A collection of windows +- Quickfix/Location lists: Lists of positions in files, often used for errors or search results +- Registers: Named storage for text and commands (like clipboard) +- Normal/Insert/Visual/Command modes: Different interaction states +- LSP (Language Server Protocol): Provides code intelligence features like completion, diagnostics, and code actions +- Treesitter: Provides syntax highlighting, code folding, and structural text editing based on syntax tree parsing +The user is working on a {OS_NAME} machine. Please respond with system specific commands if applicable. +The user is currently in workspace directory {DIR} (typically the project root). Current file paths will be relative to this directory. + + +The user will ask a question or request a task that may require analysis to answer correctly. +If you can infer the project type (languages, frameworks, libraries) from context, consider them when making changes. +For implementing features, break down the request into concepts and provide a clear solution. +Think creatively to provide complete solutions based on the information available. +Never fabricate or hallucinate file contents you haven't actually seen. + + +If tools are explicitly defined in your system context: +- Follow JSON schema precisely when using tools, including all required properties and outputting valid JSON. +- Use appropriate tools for tasks rather than asking for manual actions. +- Execute actions directly when you indicate you'll do so, without asking for permission. +- Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel. +- Before using tools to retrieve information, check if it's already available in context: + 1. Content shared via "#:" references or headers + 2. Code blocks with file path labels + 3. Other contextual sharing like selected text or conversation history +- If you don't have explicit tool definitions in your system context, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. + + You will receive code snippets that include line number prefixes - use these to maintain correct position references but remove them when generating output. +Always use code blocks to present code changes, even if the user doesn't ask for it. When presenting code changes: - -1. For each change, first provide a header outside code blocks with format: - [file:]() line:- - -2. Then wrap the actual code in triple backticks with the appropriate language identifier. - -3. Keep changes minimal and focused to produce short diffs. - -4. Include complete replacement code for the specified line range with: +1. For each change, use the following markdown code block format with triple backticks: + ``` path= start_line= end_line= + + ``` + + Examples: + + ```lua path=lua/CopilotChat/init.lua start_line=40 end_line=50 + local function example() + print("This is an example function.") + end + ``` + + ```python path=scripts/example.py start_line=10 end_line=15 + def example_function(): + print("This is an example function.") + ``` + + ```json path=config/settings.json start_line=5 end_line=8 + { + "setting": "value", + "enabled": true + } + ``` +2. Keep changes minimal and focused to produce short diffs. +3. Include complete replacement code for the specified line range with: - Proper indentation matching the source - All necessary lines (no eliding with comments) - No line number prefixes in the code - -5. Address any diagnostics issues when fixing code. - -6. If multiple changes are needed, present them as separate blocks with their own headers. -]], - vim.uv.os_uname().sysname -) +4. Address any diagnostics issues when fixing code. +5. If multiple changes are needed, present them as separate code blocks. + +]] local COPILOT_INSTRUCTIONS = [[ You are a code-focused AI programming assistant that specializes in practical software engineering solutions. @@ -76,12 +118,12 @@ End with: "**`To clear buffer highlights, please ask a different question.`**" If no issues found, confirm the code is well-written and explain why. ]] ----@class CopilotChat.config.prompt : CopilotChat.config.shared +---@class CopilotChat.config.prompts.Prompt : CopilotChat.config.Shared ---@field prompt string? ---@field description string? ---@field mapping string? ----@type table +---@type table return { COPILOT_BASE = { system_prompt = COPILOT_BASE, @@ -141,7 +183,6 @@ return { end end vim.diagnostic.set(vim.api.nvim_create_namespace('copilot-chat-diagnostics'), source.bufnr, diagnostics) - return response end, }, @@ -163,6 +204,6 @@ return { Commit = { prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - context = 'git:staged', + sticky = '#git:staged', }, } diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index c34a398b..65619117 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -67,52 +67,27 @@ local function get_github_token() error('Failed to find GitHub token') end ----@class CopilotChat.Provider.model ----@field id string ----@field name string ----@field tokenizer string? ----@field max_input_tokens number? ----@field max_output_tokens number? - ----@class CopilotChat.Provider.agent ----@field id string ----@field name string ----@field description string? - ----@class CopilotChat.Provider.embed ----@field index number ----@field embedding table - ----@class CopilotChat.Provider.options ----@field model CopilotChat.Provider.model ----@field agent CopilotChat.Provider.agent? +---@class CopilotChat.config.providers.Options +---@field model CopilotChat.client.Model ---@field temperature number? +---@field tools table? ----@class CopilotChat.Provider.input ----@field role string ----@field content string - ----@class CopilotChat.Provider.reference ----@field name string ----@field url string - ----@class CopilotChat.Provider.output +---@class CopilotChat.config.providers.Output ---@field content string ---@field finish_reason string? ---@field total_tokens number? ----@field references table? +---@field tool_calls table ----@class CopilotChat.Provider +---@class CopilotChat.config.providers.Provider ---@field disabled nil|boolean ---@field get_headers nil|fun():table,number? ----@field get_agents nil|fun(headers:table):table ----@field get_models nil|fun(headers:table):table ----@field embed nil|string|fun(inputs:table, headers:table):table ----@field prepare_input nil|fun(inputs:table, opts:CopilotChat.Provider.options):table ----@field prepare_output nil|fun(output:table, opts:CopilotChat.Provider.options):CopilotChat.Provider.output ----@field get_url nil|fun(opts:CopilotChat.Provider.options):string - ----@type table +---@field get_models nil|fun(headers:table):table +---@field embed nil|string|fun(inputs:table, headers:table):table +---@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table +---@field prepare_output nil|fun(output:table, opts:CopilotChat.config.providers.Options):CopilotChat.config.providers.Output +---@field get_url nil|fun(opts:CopilotChat.config.providers.Options):string + +---@type table local M = {} M.copilot = { @@ -139,25 +114,6 @@ M.copilot = { response.body.expires_at end, - get_agents = function(headers) - local response, err = utils.curl_get('https://api.githubcopilot.com/agents', { - json_response = true, - headers = headers, - }) - - if err then - error(err) - end - - return vim.tbl_map(function(agent) - return { - id = agent.slug, - name = agent.name, - description = agent.description, - } - end, response.body.agents) - end, - get_models = function(headers) local response, err = utils.curl_get('https://api.githubcopilot.com/models', { json_response = true, @@ -171,7 +127,7 @@ M.copilot = { local models = vim .iter(response.body.data) :filter(function(model) - return model.capabilities.type == 'chat' and not vim.endswith(model.id, 'paygo') + return model.capabilities.type == 'chat' and model.model_picker_enabled end) :map(function(model) return { @@ -180,6 +136,8 @@ M.copilot = { tokenizer = model.capabilities.tokenizer, max_input_tokens = model.capabilities.limits.max_prompt_tokens, max_output_tokens = model.capabilities.limits.max_output_tokens, + streaming = model.capabilities.supports.streaming, + tools = model.capabilities.supports.tool_calls, policy = not model['policy'] or model['policy']['state'] == 'enabled', version = model.version, } @@ -212,24 +170,59 @@ M.copilot = { local is_o1 = vim.startswith(opts.model.id, 'o1') inputs = vim.tbl_map(function(input) + local output = { + role = input.role, + content = input.content, + } + if is_o1 then if input.role == 'system' then - input.role = 'user' + output.role = 'user' end end - return input + if input.tool_call_id then + output.tool_call_id = input.tool_call_id + end + + if input.tool_calls then + output.tool_calls = vim.tbl_map(function(tool_call) + return { + id = tool_call.id, + type = 'function', + ['function'] = { + name = tool_call.name, + arguments = tool_call.arguments or nil, + }, + } + end, input.tool_calls) + end + + return output end, inputs) local out = { messages = inputs, model = opts.model.id, + stream = opts.model.streaming or false, } + if opts.tools and opts.model.tools then + out.tools = vim.tbl_map(function(tool) + return { + type = 'function', + ['function'] = { + name = tool.name, + description = tool.description, + parameters = tool.schema, + }, + } + end, opts.tools) + end + if not is_o1 then out.n = 1 out.top_p = 1 - out.stream = true out.temperature = opts.temperature end @@ -241,46 +234,51 @@ M.copilot = { end, prepare_output = function(output) - local references = {} - - if output.copilot_references then - for _, reference in ipairs(output.copilot_references) do - local metadata = reference.metadata - if metadata and metadata.display_name and metadata.display_url then - table.insert(references, { - name = metadata.display_name, - url = metadata.display_url, - }) + local tool_calls = {} + + local choice + if output.choices and #output.choices > 0 then + for _, choice in ipairs(output.choices) do + local message = choice.message or choice.delta + if message and message.tool_calls then + for i, tool_call in ipairs(message.tool_calls) do + local fn = tool_call['function'] + if fn then + local index = tool_call.index or i + local id = utils.empty(tool_call.id) and ('tooluse_' .. index) or tool_call.id + table.insert(tool_calls, { + id = id, + index = index, + name = fn.name, + arguments = fn.arguments or '', + }) + end + end end end - end - local message - if output.choices and #output.choices > 0 then - message = output.choices[1] + choice = output.choices[1] else - message = output + choice = output end - local content = message.message and message.message.content or message.delta and message.delta.content - - local usage = message.usage and message.usage.total_tokens or output.usage and output.usage.total_tokens - - local finish_reason = message.finish_reason or message.done_reason or output.finish_reason or output.done_reason + local message = choice.message or choice.delta + local content = message and message.content + local usage = choice.usage and choice.usage.total_tokens + if not usage then + usage = output.usage and output.usage.total_tokens + end + local finish_reason = choice.finish_reason or choice.done_reason or output.finish_reason or output.done_reason return { content = content, finish_reason = finish_reason, total_tokens = usage, - references = references, + tool_calls = tool_calls, } end, - get_url = function(opts) - if opts.agent then - return 'https://api.githubcopilot.com/agents/' .. opts.agent.id .. '?chat' - end - + get_url = function() return 'https://api.githubcopilot.com/chat/completions' end, } @@ -336,6 +334,7 @@ M.github_models = { tokenizer = 'o200k_base', max_input_tokens = max_input_tokens, max_output_tokens = max_output_tokens, + streaming = true, } end) :totable() diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua new file mode 100644 index 00000000..8a28ca81 --- /dev/null +++ b/lua/CopilotChat/functions.lua @@ -0,0 +1,198 @@ +local utils = require('CopilotChat.utils') + +local M = {} + +local INPUT_SEPARATOR = ';;' + +local function sorted_propnames(schema) + local prop_names = vim.tbl_keys(schema.properties) + local required_set = {} + if schema.required then + for _, name in ipairs(schema.required) do + required_set[name] = true + end + end + + -- Sort properties with priority: required without default > required with default > optional + table.sort(prop_names, function(a, b) + local a_required = required_set[a] or false + local b_required = required_set[b] or false + local a_has_default = schema.properties[a].default ~= nil + local b_has_default = schema.properties[b].default ~= nil + + -- First priority: required properties without default + if a_required and not a_has_default and (not b_required or b_has_default) then + return true + end + if b_required and not b_has_default and (not a_required or a_has_default) then + return false + end + + -- Second priority: required properties with default + if a_required and not b_required then + return true + end + if b_required and not a_required then + return false + end + + -- Finally sort alphabetically + return a < b + end) + + return prop_names +end + +local function filter_schema(tbl) + if type(tbl) ~= 'table' then + return tbl + end + + local result = {} + for k, v in pairs(tbl) do + if type(v) ~= 'function' and k ~= 'examples' then + result[k] = type(v) == 'table' and filter_schema(v) or v + end + end + return result +end + +---@param uri string The URI to parse +---@param pattern string The pattern to match against (e.g., 'file://{path}') +---@return table|nil inputs Extracted parameters or nil if no match +function M.match_uri(uri, pattern) + -- Convert the pattern into a Lua pattern by escaping special characters + -- and replacing {name} placeholders with capture groups + local lua_pattern = pattern:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') + + -- Extract parameter names from the pattern + local param_names = {} + for param in pattern:gmatch('{([^}:*]+)[^}]*}') do + table.insert(param_names, param) + -- Replace {param} with a capture group in our Lua pattern + -- Use non-greedy capture to handle multiple params properly + lua_pattern = lua_pattern:gsub('{' .. param .. '[^}]*}', '(.-)') + end + + -- If no parameters, just do a direct comparison + if #param_names == 0 then + return uri == pattern and {} or nil + end + + -- Match the URI against our constructed pattern + local matches = { uri:match('^' .. lua_pattern .. '$') } + + -- If match failed, return nil + if #matches == 0 or matches[1] == nil then + return nil + end + + -- Build the result table mapping parameter names to their values + local result = {} + for i, param_name in ipairs(param_names) do + result[param_name] = matches[i] + end + + return result +end + +--- Prepare the schema for use +---@param tools table +---@return table +function M.parse_tools(tools) + local tool_names = vim.tbl_keys(tools) + table.sort(tool_names) + return vim.tbl_map(function(name) + local tool = tools[name] + local schema = tool.schema + + if schema then + schema = filter_schema(schema) + end + + return { + name = name, + description = tool.description, + schema = schema, + } + end, tool_names) +end + +--- Parse context input string into a table based on the schema +---@param input string|table|nil +---@param schema table? +---@return table +function M.parse_input(input, schema) + if not schema or not schema.properties then + return {} + end + + if type(input) == 'table' then + return input + end + + local parts = vim.split(input or '', INPUT_SEPARATOR) + local result = {} + local prop_names = sorted_propnames(schema) + + -- Map input parts to schema properties in sorted order + local i = 1 + for _, prop_name in ipairs(prop_names) do + local prop_schema = schema.properties[prop_name] + local value = not utils.empty(parts[i]) and parts[i] or nil + if value == nil and prop_schema.default ~= nil then + value = prop_schema.default + end + + result[prop_name] = value + i = i + 1 + if i > #parts then + break + end + end + + return result +end + +--- Get input from the user based on the schema +---@param schema table? +---@param source CopilotChat.source +---@return string? +function M.enter_input(schema, source) + if not schema or not schema.properties then + return nil + end + + local prop_names = sorted_propnames(schema) + local out = {} + + for _, prop_name in ipairs(prop_names) do + local cfg = schema.properties[prop_name] + if not schema.required or vim.tbl_contains(schema.required, prop_name) then + if cfg.enum then + local choices = type(cfg.enum) == 'table' and cfg.enum or cfg.enum(source) + local choice = utils.select(choices, { + prompt = string.format('Select %s> ', prop_name), + }) + + table.insert(out, choice or '') + elseif cfg.type == 'boolean' then + table.insert(out, utils.select({ 'true', 'false' }, { + prompt = string.format('Select %s> ', prop_name), + }) or '') + else + table.insert(out, utils.input({ + prompt = string.format('Enter %s> ', prop_name), + }) or '') + end + end + end + + local out = vim.trim(table.concat(out, INPUT_SEPARATOR)) + if out:match('%s+') then + out = string.format('`%s`', out) + end + return out +end + +return M diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 5d02df19..cf1e568a 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -4,7 +4,6 @@ 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 @@ -42,7 +41,7 @@ end function M.check() start('CopilotChat.nvim [core]') - local vim_version = vim.trim(vim.api.nvim_command_output('version')) + local vim_version = vim.trim(vim.api.nvim_exec2('version', { output = true }).output) if vim.fn.has('nvim-0.10.0') == 1 then ok('nvim: ' .. vim_version) else diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1062e1a4..1f592349 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,17 +1,21 @@ local async = require('plenary.async') local log = require('plenary.log') -local context = require('CopilotChat.context') +local functions = require('CopilotChat.functions') +local resources = require('CopilotChat.resources') local client = require('CopilotChat.client') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local PLUGIN_NAME = 'CopilotChat' -local WORD = '([^%s]+)' -local WORD_INPUT = '([^%s:]+:`[^`]+`)' +local WORD = '([^%s:]+)' +local WORD_NO_INPUT = '([^%s]+)' +local WORD_WITH_INPUT_QUOTED = WORD .. ':`([^`]+)`' +local WORD_WITH_INPUT_UNQUOTED = WORD .. ':?([^%s`]*)' +local BLOCK_OUTPUT_FORMAT = '```%s\n%s\n```' ---@class CopilotChat ----@field config CopilotChat.config ----@field chat CopilotChat.ui.Chat +---@field config CopilotChat.config.Config +---@field chat CopilotChat.ui.chat.Chat local M = {} --- @class CopilotChat.source @@ -21,23 +25,19 @@ local M = {} --- @class CopilotChat.state --- @field source CopilotChat.source? ---- @field last_prompt string? ---- @field last_response string? ---- @field highlights_loaded boolean +--- @field sticky string[]? local state = { -- Current state tracking source = nil, -- Last state tracking - last_prompt = nil, - last_response = nil, - highlights_loaded = false, + sticky = nil, } --- Insert sticky values from config into prompt ---@param prompt string ----@param config CopilotChat.config.shared -local function insert_sticky(prompt, config, override_sticky) +---@param config CopilotChat.config.Shared +local function insert_sticky(prompt, config) local lines = vim.split(prompt or '', '\n') local stickies = utils.ordered_map() @@ -58,8 +58,10 @@ local function insert_sticky(prompt, config, override_sticky) stickies:set('$' .. config.model, true) end - if config.remember_as_sticky and config.agent and config.agent ~= M.config.agent then - stickies:set('@' .. config.agent, true) + if config.remember_as_sticky and config.tools and not vim.deep_equal(config.tools, M.config.tools) then + for _, tool in ipairs(utils.to_table(config.tools)) do + stickies:set('@' .. tool, true) + end end if @@ -71,32 +73,18 @@ local function insert_sticky(prompt, config, override_sticky) stickies:set('/' .. config.system_prompt, true) end - if config.remember_as_sticky and config.context and not vim.deep_equal(config.context, M.config.context) then - if type(config.context) == 'table' then - ---@diagnostic disable-next-line: param-type-mismatch - for _, context in ipairs(config.context) do - stickies:set('#' .. context, true) - end - else - stickies:set('#' .. config.context, true) - end - end - - if config.sticky and (override_sticky or not vim.deep_equal(config.sticky, M.config.sticky)) then - if type(config.sticky) == 'table' then - ---@diagnostic disable-next-line: param-type-mismatch - for _, sticky in ipairs(config.sticky) do - stickies:set(sticky, true) - end - else - stickies:set(config.sticky, true) + if config.sticky and not vim.deep_equal(config.sticky, M.config.sticky) then + for _, sticky in ipairs(utils.to_table(config.sticky)) do + stickies:set(sticky, true) end end -- Insert stickies at start of prompt local prompt_lines = {} for _, sticky in ipairs(stickies:keys()) do - table.insert(prompt_lines, '> ' .. sticky) + if sticky ~= '' then + table.insert(prompt_lines, '> ' .. sticky) + end end if #prompt_lines > 0 then table.insert(prompt_lines, '') @@ -130,68 +118,44 @@ local function update_highlights() strict = false, }) end - - if state.highlights_loaded then - return - end - - async.run(function() - local items = M.complete_items() - utils.schedule_main() - - for _, item in ipairs(items) do - local pattern = vim.fn.escape(item.word, '.-$^*[]') - if vim.startswith(item.word, '#') then - vim.cmd('syntax match CopilotChatKeyword "' .. pattern .. '\\(:.\\+\\)\\?" containedin=ALL') - else - vim.cmd('syntax match CopilotChatKeyword "' .. pattern .. '" containedin=ALL') - end - end - - vim.cmd('syntax match CopilotChatInput ":\\(.\\+\\)" contained containedin=CopilotChatKeyword') - state.highlights_loaded = true - end) end --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) - if not start_of_chat then - M.chat:append('\n\n') - end - - M.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - - -- Insert sticky values from config into prompt if start_of_chat then - state.last_prompt = insert_sticky(state.last_prompt, M.config, true) + local sticky = {} + if M.config.sticky then + for _, sticky_line in ipairs(utils.to_table(M.config.sticky)) do + table.insert(sticky, sticky_line) + end + end + state.sticky = sticky end - -- Reinsert sticky prompts from last prompt and last response - local lines = {} - if state.last_prompt then - lines = vim.split(state.last_prompt, '\n') - end - if state.last_response then - for _, line in ipairs(vim.split(state.last_response, '\n')) do - table.insert(lines, line) + local prompt_content = '' + local last_message = M.chat.messages[#M.chat.messages] + local tool_calls = last_message and last_message.tool_calls or {} + + if not utils.empty(state.sticky) then + for _, sticky in ipairs(state.sticky) do + prompt_content = prompt_content .. '> ' .. sticky .. '\n' end + prompt_content = prompt_content .. '\n' end - local has_sticky = false - local in_code_block = false - for _, line in ipairs(lines) do - if line:match('^```') then - in_code_block = not in_code_block - end - if vim.startswith(line, '> ') and not in_code_block then - M.chat:append(line .. '\n') - has_sticky = true + + if not utils.empty(tool_calls) then + for _, tool_call in ipairs(tool_calls) do + prompt_content = prompt_content .. string.format('#%s:%s\n', tool_call.name, tool_call.id) end - end - if has_sticky then - M.chat:append('\n') + prompt_content = prompt_content .. '\n' end + M.chat:add_message({ + role = 'user', + content = prompt_content, + }) + M.chat:finish() end @@ -199,20 +163,13 @@ end ---@param err string|table|nil local function show_error(err) err = err or 'Unknown error' + err = utils.make_string(err) - if type(err) == 'string' then - while true do - local new_err = err:gsub('^[^:]+:%d+: ', '') - if new_err == err then - break - end - err = new_err - end - else - err = utils.make_string(err) - end + M.chat:add_message({ + role = 'assistant', + content = '\n' .. string.format(BLOCK_OUTPUT_FORMAT, 'error', err) .. '\n', + }) - M.chat:append('\n' .. M.config.error_header .. '\n```error\n' .. err .. '\n```') finish() end @@ -261,15 +218,171 @@ local function update_source() M.set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) end +--- Call and resolve function calls from the prompt. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return table, table, table, string +---@async +function M.resolve_functions(prompt, config) + config, prompt = M.resolve_prompt(prompt, config) + local enabled_tools = {} + local resolved_resources = {} + local resolved_tools = {} + local matches = utils.to_table(config.tools) + local tool_calls = {} + for _, message in ipairs(M.chat.messages) do + if message.tool_calls then + for _, tool_call in ipairs(message.tool_calls) do + table.insert(tool_calls, tool_call) + end + end + end + + -- Check for @tool pattern to find enabled tools + prompt = prompt:gsub('@' .. WORD, function(match) + for name, tool in pairs(M.config.functions) do + if name == match or tool.group == match then + table.insert(matches, match) + return '' + end + end + return '@' .. match + end) + for _, match in ipairs(matches) do + for name, tool in pairs(M.config.functions) do + if name == match or tool.group == match then + enabled_tools[name] = tool + end + end + end + + local matches = utils.ordered_map() + + -- Check for #word:`input` pattern + for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_QUOTED) do + local pattern = string.format('#%s:`%s`', word, input) + matches:set(pattern, { + word = word, + input = input, + }) + end + + -- Check for #word:input pattern + for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_UNQUOTED) do + local pattern = utils.empty(input) and string.format('#%s', word) or string.format('#%s:%s', word, input) + matches:set(pattern, { + word = word, + input = input, + }) + end + + -- Check for ##word:input pattern + for word in prompt:gmatch('##' .. WORD_NO_INPUT) do + local pattern = string.format('##%s', word) + matches:set(pattern, { + word = word, + }) + end + + -- Resolve each tool reference + local function expand_tool(name, input) + notify.publish(notify.STATUS, 'Running function: ' .. name) + + local tool_id = nil + if not utils.empty(tool_calls) then + for _, tool_call in ipairs(tool_calls) do + if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) and enabled_tools[name] then + input = utils.empty(tool_call.arguments) and {} or utils.json_decode(tool_call.arguments) + tool_id = tool_call.id + break + end + end + end + + local tool = enabled_tools[name] + if not tool then + -- Check if input matches uri + for tool_name, tool_spec in pairs(M.config.functions) do + if tool_spec.uri then + local match = functions.match_uri(name, tool_spec.uri) + if match then + name = tool_name + tool = tool_spec + input = match + break + end + end + end + end + if not tool and not tool_id then + tool = M.config.functions[name] + end + if not tool then + -- If tool is not found, return the original pattern + return nil + end + if not tool_id and not tool.uri then + -- If this is a tool that is not resource and was not called by LLM, reject it + return nil + end + + local result = '' + local ok, output = pcall(tool.resolve, functions.parse_input(input, tool.schema), state.source or {}, prompt) + if not ok then + result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) + else + for _, content in ipairs(output) do + if content then + local content_out = nil + if content.uri then + content_out = '##' .. content.uri + table.insert(resolved_resources, resources.to_resource(content)) + if tool_id then + table.insert(state.sticky, content_out) + end + else + content_out = string.format(BLOCK_OUTPUT_FORMAT, utils.mimetype_to_filetype(content.mimetype), content.data) + end + + if not utils.empty(result) then + result = result .. '\n' + end + result = result .. content_out + end + end + end + + if tool_id then + table.insert(resolved_tools, { + id = tool_id, + result = result, + }) + + return nil + end + + return result + end + + -- Resolve and process all tools + for _, pattern in ipairs(matches:keys()) do + local match = matches:get(pattern) + local out = expand_tool(match.word, match.input) or pattern + prompt = prompt:gsub(vim.pesc(pattern), out, 1) + end + + return functions.parse_tools(enabled_tools), resolved_resources, resolved_tools, prompt +end + --- Resolve the final prompt and config from prompt template. ---@param prompt string? ----@param config CopilotChat.config.shared? ----@return CopilotChat.config.prompt, string +---@param config CopilotChat.config.Shared? +---@return CopilotChat.config.prompts.Prompt, string function M.resolve_prompt(prompt, config) if not prompt then - local section = M.chat:get_prompt() - if section then - prompt = section.content + local message = M.chat:get_message('user') + if message then + prompt = message.content end end @@ -303,107 +416,20 @@ function M.resolve_prompt(prompt, config) if prompts_to_use[config.system_prompt] then config.system_prompt = prompts_to_use[config.system_prompt].system_prompt end - return config, prompt -end - ---- Resolve the context embeddings from the prompt. ----@param prompt string? ----@param config CopilotChat.config.shared? ----@return table, string ----@async -function M.resolve_context(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - 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, ':')) - if vim.startswith(context_input, '`') and vim.endswith(context_input, '`') then - context_input = context_input:sub(2, -2) - 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 - - return false - end - prompt = prompt:gsub('#' .. WORD_INPUT, function(match) - return parse_context(match) and '' or '#' .. match - end) - - prompt = prompt:gsub('#' .. WORD, function(match) - return parse_context(match) and '' or '#' .. match - end) - - 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 - else - parse_context(config.context) - end - end - - local embeddings = utils.ordered_map() - for _, context_data in ipairs(contexts) do - local context_value = M.config.contexts[context_data.name] - notify.publish( - notify.STATUS, - 'Resolving context: ' .. context_data.name .. (context_data.input and ' with input: ' .. context_data.input or '') - ) - - local ok, resolved_embeddings = pcall(context_value.resolve, context_data.input, state.source or {}, prompt) - if ok then - for _, embedding in ipairs(resolved_embeddings) do - if embedding then - embeddings:set(embedding.filename, embedding) - end - end - else - log.error('Failed to resolve context: ' .. context_data.name, resolved_embeddings) + if config.system_prompt then + config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) + if state.source then + config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end end - return embeddings:values(), prompt -end - ---- Resolve the agent from the prompt. ----@param prompt string? ----@param config CopilotChat.config.shared? ----@return string, string ----@async -function M.resolve_agent(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - local agents = vim.tbl_map(function(agent) - return agent.id - end, client:list_agents()) - - local selected_agent = config.agent or '' - 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 + return config, prompt end --- Resolve the model from the prompt. ---@param prompt string? ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? ---@return string, string ---@async function M.resolve_model(prompt, config) @@ -457,7 +483,7 @@ function M.set_source(source_winnr) end --- Get the selection from the source buffer. ----@return CopilotChat.select.selection? +---@return CopilotChat.select.Selection? function M.get_selection() local config = vim.tbl_deep_extend('force', M.config, M.chat.config) local selection = config.selection @@ -506,8 +532,8 @@ function M.set_selection(bufnr, start_line, end_line, clear) end --- Trigger the completion for the chat window. ----@param without_context boolean? -function M.trigger_complete(without_context) +---@param without_input boolean? +function M.trigger_complete(without_input) local info = M.complete_info() local bufnr = vim.api.nvim_get_current_buf() local line = vim.api.nvim_get_current_line() @@ -523,23 +549,18 @@ function M.trigger_complete(without_context) return end - if not without_context and 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 + if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then + local found_tool = M.config.functions[prefix:sub(2, -2)] + if found_tool and found_tool.schema then async.run(function() - found_context.input(function(value) - if not value then - return - end - - local value_str = vim.trim(tostring(value)) - if value_str:find('%s') then - value_str = '`' .. value_str .. '`' - end + local value = functions.enter_input(found_tool.schema, state.source) + if not value then + return + end - 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, state.source or {}) + utils.schedule_main() + vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value }) + vim.api.nvim_win_set_cursor(0, { row, col + #value }) end) end @@ -577,7 +598,6 @@ end ---@async function M.complete_items() local models = client:list_models() - local agents = client:list_agents() local prompts_to_use = M.prompts() local items = {} @@ -616,32 +636,59 @@ function M.complete_items() } end - for _, agent in pairs(agents) do + local groups = {} + for name, tool in pairs(M.config.functions) do + if tool.group then + groups[tool.group] = groups[tool.group] or {} + groups[tool.group][name] = tool + end + end + for name, group in pairs(groups) do + local group_tools = vim.tbl_keys(group) items[#items + 1] = { - word = '@' .. agent.id, - abbr = agent.id, - kind = agent.provider, - info = agent.description, - menu = agent.name, + word = '@' .. name, + abbr = name, + kind = 'group', + info = table.concat(group_tools, '\n'), + menu = string.format('%s tools', #group_tools), icase = 1, dup = 0, empty = 0, } end - - for name, value in pairs(M.config.contexts) do + for name, tool in pairs(M.config.functions) do items[#items + 1] = { - word = '#' .. name, + word = '@' .. name, abbr = name, - kind = 'context', - info = value.description or '', - menu = value.input and string.format('#%s:', name) or string.format('#%s', name), + kind = 'tool', + info = tool.description, + menu = tool.group or '', icase = 1, dup = 0, empty = 0, } end + local tools_to_use = functions.parse_tools(M.config.functions) + for _, tool in pairs(tools_to_use) do + local uri = M.config.functions[tool.name].uri + if uri then + local info = + string.format('%s\n\n%s', tool.description, tool.schema and vim.inspect(tool.schema, { indent = ' ' }) or '') + + items[#items + 1] = { + word = '#' .. tool.name, + abbr = tool.name, + kind = 'resource', + info = info, + menu = uri, + icase = 1, + dup = 0, + empty = 0, + } + end + end + table.sort(items, function(a, b) if a.kind == b.kind then return a.word < b.word @@ -653,7 +700,7 @@ function M.complete_items() end --- Get the prompts to use. ----@return table +---@return table function M.prompts() local prompts_to_use = {} @@ -676,18 +723,22 @@ function M.prompts() end --- Open the chat window. ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.open(config) config = vim.tbl_deep_extend('force', M.config, config or {}) utils.return_to_normal_mode() M.chat:open(config) - local section = M.chat:get_prompt() - if section then - local prompt = insert_sticky(section.content, config) + local message = M.chat:get_message('user') + if message then + local prompt = insert_sticky(message.content, config) if prompt then - M.chat:set_prompt(prompt) + M.chat:add_message({ + role = 'user', + content = '\n' .. prompt, + }, true) + M.chat:finish() end end @@ -701,7 +752,7 @@ function M.close() end --- Toggle the chat window. ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.toggle(config) if M.chat:visible() then M.close() @@ -710,12 +761,6 @@ function M.toggle(config) end end ---- Get the last response. ---- @returns string -function M.response() - return state.last_response -end - --- Select default Copilot GPT model. function M.select_model() async.run(function() @@ -725,6 +770,8 @@ function M.select_model() id = model.id, name = model.name, provider = model.provider, + streaming = model.streaming, + tools = model.tools, selected = model.id == M.config.model, } end, models) @@ -733,53 +780,39 @@ function M.select_model() vim.ui.select(choices, { prompt = 'Select a model> ', format_item = function(item) - local out = string.format('%s (%s:%s)', item.name, item.provider, item.id) + local indicators = {} + local out = item.name + if item.selected then out = '* ' .. out end - return out - end, - }, function(choice) - if choice then - M.config.model = choice.id - end - end) - end) -end ---- Select default Copilot agent. -function M.select_agent() - async.run(function() - local agents = client:list_agents() - local choices = vim.tbl_map(function(agent) - return { - id = agent.id, - name = agent.name, - provider = agent.provider, - selected = agent.id == M.config.agent, - } - end, agents) + if item.provider then + table.insert(indicators, item.provider) + end + if item.streaming then + table.insert(indicators, 'streaming') + end + if item.tools then + table.insert(indicators, 'tools') + end - utils.schedule_main() - vim.ui.select(choices, { - prompt = 'Select an agent> ', - format_item = function(item) - local out = string.format('%s (%s:%s)', item.name, item.provider, item.id) - if item.selected then - out = '* ' .. out + if #indicators > 0 then + out = out .. ' [' .. table.concat(indicators, ', ') .. ']' end + return out end, }, function(choice) if choice then - M.config.agent = choice.id + M.config.model = choice.id end end) end) end --- Select a prompt template to use. ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.select_prompt(config) local prompts = M.prompts() local keys = vim.tbl_keys(prompts) @@ -813,7 +846,7 @@ end --- Ask a question to the Copilot model. ---@param prompt string? ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.ask(prompt, config) prompt = prompt or '' if prompt == '' then @@ -836,10 +869,18 @@ function M.ask(prompt, config) M.open(config) end - state.last_prompt = prompt - M.chat:set_prompt(prompt) - M.chat:append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n') - M.chat:follow() + local sticky = {} + local in_code_block = false + for _, line in ipairs(vim.split(prompt, '\n')) do + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then + table.insert(sticky, line:sub(3)) + end + end + + state.sticky = sticky else update_source() end @@ -848,16 +889,6 @@ function M.ask(prompt, config) config, prompt = M.resolve_prompt(prompt, config) local system_prompt = config.system_prompt or '' - -- Resolve context name and description - local contexts = {} - if config.include_contexts_in_prompt then - for name, context in pairs(M.config.contexts) do - if context.description then - contexts[name] = context.description - end - end - end - -- Remove sticky prefix prompt = table.concat( vim.tbl_map(function(l) @@ -870,39 +901,55 @@ function M.ask(prompt, config) local selection = M.get_selection() local ok, err = pcall(async.run, function() - local selected_agent, prompt = M.resolve_agent(prompt, config) + local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) - local embeddings, prompt = M.resolve_context(prompt, config) + local query_ok, processed_resources = pcall(resources.process_resources, prompt, selected_model, resolved_resources) + if query_ok then + resolved_resources = processed_resources + else + log.warn('Failed to process resources', processed_resources) + end - local query_ok, filtered_embeddings = - pcall(context.filter_embeddings, prompt, selected_model, config.headless, embeddings) + prompt = vim.trim(prompt) - if not query_ok then + if not config.headless then utils.schedule_main() - log.error(filtered_embeddings) - if not config.headless then - show_error(filtered_embeddings) + + if not utils.empty(resolved_tools) then + M.chat:remove_message('user') + else + M.chat:add_message({ + role = 'user', + content = '\n' .. prompt .. '\n', + }, true) end - return + + for _, tool in ipairs(resolved_tools) do + M.chat:add_message({ + role = 'tool', + tool_call_id = tool.id, + content = tool.result .. '\n', + }) + end + + M.chat:follow() end - local ask_ok, response, references, token_count, token_max_count = pcall(client.ask, client, prompt, { + local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, - contexts = contexts, + history = M.chat.messages, selection = selection, - embeddings = filtered_embeddings, + resources = resolved_resources, + tools = selected_tools, system_prompt = system_prompt, model = selected_model, - agent = selected_agent, temperature = config.temperature, on_progress = vim.schedule_wrap(function(token) - local out = config.stream and config.stream(token, state.source) or nil - if out == nil then - out = token - end - local to_print = not config.headless and out - if to_print and to_print ~= '' then - M.chat:append(token) + if not config.headless then + M.chat:add_message({ + content = token, + role = 'assistant', + }) end end), }) @@ -910,48 +957,44 @@ function M.ask(prompt, config) utils.schedule_main() if not ask_ok then - log.error(response) + log.error(ask_response) if not config.headless then - show_error(response) + show_error(ask_response) end return end -- If there was no error and no response, it means job was cancelled - if response == nil then + if ask_response == nil then return end - -- Call the callback function and store to history - local out = config.callback and config.callback(response, state.source) or nil - if out == nil then - out = response - end - local to_store = not config.headless and out - if to_store and to_store ~= '' then - table.insert(client.history, { - content = prompt, - role = 'user', - }) - table.insert(client.history, { - content = to_store, - role = 'assistant', - }) + local response = ask_response.message + local token_count = ask_response.token_count + local token_max_count = ask_response.token_max_count + + -- Call the callback function + if config.callback then + local callback_ok, callback_response = pcall(config.callback, response.content, state.source) + if not callback_ok then + log.error('Callback error: ' .. callback_response) + if not config.headless then + show_error(callback_response) + end + return + end end if not config.headless then - state.last_response = response - M.chat.references = references + response.content = vim.trim(response.content) + if utils.empty(response.content) then + response.content = '' + else + response.content = '\n' .. response.content .. '\n' + end + M.chat:add_message(response, true) M.chat.token_count = token_count M.chat.token_max_count = token_max_count - - if not utils.empty(references) and config.references_display == 'write' then - M.chat:append('\n\n**`References`**:') - for _, ref in ipairs(references) do - M.chat:append(string.format('\n[%s](%s)', ref.name, ref.url)) - end - end - finish() end end) @@ -967,26 +1010,19 @@ end --- Stop current copilot output and optionally reset the chat ten show the help message. ---@param reset boolean? function M.stop(reset) - local stopped = false + local stopped = client:stop() if reset then - client:reset() M.chat:clear() vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) - state.last_prompt = nil - state.last_response = nil -- Clear the selection if state.source then M.set_selection(state.source.bufnr, 0, 0, true) end - - stopped = true - else - stopped = client:stop() end - if stopped then + if stopped or reset then finish(reset) end end @@ -1009,15 +1045,10 @@ function M.save(name, history_path) return end - local prompt = M.chat:get_prompt() - local history = vim.list_slice(client.history) - if prompt then - table.insert(history, { - content = prompt.content, - role = 'user', - }) + local history = vim.deepcopy(M.chat.messages) + for _, message in ipairs(history) do + message.section = nil end - history_path = vim.fs.normalize(history_path) vim.fn.mkdir(history_path, 'p') history_path = history_path .. '/' .. name .. '.json' @@ -1059,32 +1090,11 @@ function M.load(name, history_path) }, }) - client:reset() - M.chat:clear() - - client.history = history - for i, message in ipairs(history) do - if message.role == 'user' then - if i > 1 then - M.chat:append('\n\n') - end - M.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - M.chat:append(message.content) - elseif message.role == 'assistant' then - M.chat:append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n') - M.chat:append(message.content) - end - end - log.info('Loaded history from ' .. history_path) - if #history > 0 then - local last = history[#history] - if last and last.role == 'user' then - M.chat:append('\n\n') - M.chat:finish() - return - end + M.stop(true) + for _, message in ipairs(history) do + M.chat:add_message(message) end finish(#history == 0) @@ -1100,11 +1110,20 @@ function M.log_level(level) plugin = PLUGIN_NAME, level = level, outfile = M.config.log_path, + fmt_msg = function(is_console, mode_name, src_path, src_line, msg) + local nameupper = mode_name:upper() + if is_console then + return string.format('[%s] %s', nameupper, msg) + else + local lineinfo = src_path .. ':' .. src_line + return string.format('[%-6s%s] %s: %s\n', nameupper, os.date(), lineinfo, msg) + end + end, }, true) end --- Set up the plugin ----@param config CopilotChat.config? +---@param config CopilotChat.config.Config? function M.setup(config) M.config = vim.tbl_deep_extend('force', require('CopilotChat.config'), config or {}) state.highlights_loaded = false @@ -1130,8 +1149,7 @@ function M.setup(config) M.chat:delete() end M.chat = require('CopilotChat.ui.chat')( - M.config.question_header, - M.config.answer_header, + M.config.headers, M.config.separator, utils.key_to_info('show_help', M.config.mappings.show_help), function(bufnr) diff --git a/lua/CopilotChat/integrations/fzflua.lua b/lua/CopilotChat/integrations/fzflua.lua deleted file mode 100644 index 174d0139..00000000 --- a/lua/CopilotChat/integrations/fzflua.lua +++ /dev/null @@ -1,42 +0,0 @@ -local fzflua = require('fzf-lua') -local chat = require('CopilotChat') -local utils = require('CopilotChat.utils') - -local M = {} - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: fzf-lua options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - utils.return_to_normal_mode() - opts = vim.tbl_extend('force', { - prompt = pick_actions.prompt .. '> ', - 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 - return - end - vim.defer_fn(function() - chat.ask(pick_actions.actions[selected[1]].prompt, pick_actions.actions[selected[1]]) - end, 100) - end, - }, - }, opts or {}) - - fzflua.fzf_exec(vim.tbl_keys(pick_actions.actions), opts) -end - -return M diff --git a/lua/CopilotChat/integrations/snacks.lua b/lua/CopilotChat/integrations/snacks.lua deleted file mode 100644 index 14a2daaa..00000000 --- a/lua/CopilotChat/integrations/snacks.lua +++ /dev/null @@ -1,54 +0,0 @@ -local snacks = require('snacks') -local chat = require('CopilotChat') -local utils = require('CopilotChat.utils') - -local M = {} - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: snacks options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - utils.return_to_normal_mode() - opts = vim.tbl_extend('force', { - items = vim.tbl_map(function(name) - return { - id = name, - text = name, - file = name, - preview = { - text = pick_actions.actions[name].prompt, - ft = 'text', - }, - } - end, vim.tbl_keys(pick_actions.actions)), - preview = 'preview', - win = { - preview = { - wo = { - wrap = true, - linebreak = true, - }, - }, - }, - title = pick_actions.prompt, - confirm = function(picker) - local selected = picker:current() - if selected then - local action = pick_actions.actions[selected.id] - vim.defer_fn(function() - chat.ask(action.prompt, action) - end, 100) - end - picker:close() - end, - }, opts or {}) - - snacks.picker(opts) -end - -return M diff --git a/lua/CopilotChat/integrations/telescope.lua b/lua/CopilotChat/integrations/telescope.lua deleted file mode 100644 index 5e14d913..00000000 --- a/lua/CopilotChat/integrations/telescope.lua +++ /dev/null @@ -1,65 +0,0 @@ -local actions = require('telescope.actions') -local action_state = require('telescope.actions.state') -local pickers = require('telescope.pickers') -local finders = require('telescope.finders') -local themes = require('telescope.themes') -local conf = require('telescope.config').values -local previewers = require('telescope.previewers') -local chat = require('CopilotChat') -local utils = require('CopilotChat.utils') - -local M = {} - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: Telescope options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - utils.return_to_normal_mode() - - if not (opts and opts.theme) then - opts = themes.get_dropdown(opts or {}) - end - - pickers - .new(opts, { - prompt_title = pick_actions.prompt, - finder = finders.new_table({ - results = vim.tbl_keys(pick_actions.actions), - }), - previewer = previewers.new_buffer_previewer({ - define_preview = function(self, entry) - vim.api.nvim_win_set_option(self.state.winid, 'wrap', true) - vim.api.nvim_buf_set_lines( - self.state.bufnr, - 0, - -1, - false, - vim.split(pick_actions.actions[entry[1]].prompt or '', '\n') - ) - end, - }), - sorter = conf.generic_sorter(opts), - attach_mappings = function(prompt_bufnr) - actions.select_default:replace(function() - actions.close(prompt_bufnr) - local selected = action_state.get_selected_entry() - if not selected or vim.tbl_isempty(selected) then - return - end - - vim.defer_fn(function() - chat.ask(pick_actions.actions[selected[1]].prompt, pick_actions.actions[selected[1]]) - end, 100) - end) - return true - end, - }) - :find() -end - -return M diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/resources.lua similarity index 81% rename from lua/CopilotChat/context.lua rename to lua/CopilotChat/resources.lua index 3d50ca3d..da79d6ec 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/resources.lua @@ -1,4 +1,4 @@ ----@class CopilotChat.context.symbol +---@class CopilotChat.resources.Symbol ---@field name string? ---@field signature string ---@field type string @@ -7,16 +7,6 @@ ---@field end_row number ---@field end_col number ----@class CopilotChat.context.embed ----@field content string ----@field filename string ----@field filetype string ----@field outline string? ----@field diagnostics table? ----@field symbols table? ----@field embedding table? ----@field score number? - local async = require('plenary.async') local log = require('plenary.log') local client = require('CopilotChat.client') @@ -94,9 +84,9 @@ local function spatial_distance_cosine(a, b) end --- Rank data by relatedness to the query ----@param query CopilotChat.context.embed ----@param data table ----@return table +---@param query CopilotChat.client.EmbeddedResource +---@param data table +---@return table local function data_ranked_by_relatedness(query, data) for _, item in ipairs(data) do local score = spatial_distance_cosine(item.embedding, query.embedding) @@ -189,8 +179,8 @@ end --- Rank data by symbols and filenames ---@param query string ----@param data table ----@return table +---@param data table +---@return table local function data_ranked_by_symbols(query, data) -- Get query trigrams including compound versions local query_trigrams = {} @@ -211,7 +201,7 @@ local function data_ranked_by_symbols(query, data) local max_score = 0 for _, entry in ipairs(data) do - local basename = utils.filename(entry.filename):gsub('%..*$', '') + local basename = utils.filename(entry.name):gsub('%..*$', '') -- Get trigrams for basename and compound version local file_trigrams = get_trigrams(basename) @@ -327,9 +317,9 @@ end --- Build an outline and symbols from a string ---@param content string ---@param ft string ----@return string?, table? +---@return string?, table? local function get_outline(content, ft) - if not ft or ft == '' or ft == 'text' or ft == 'raw' then + if not ft or ft == '' then return nil end @@ -399,47 +389,36 @@ end --- Get data for a file ---@param filename string ----@param filetype string? ----@return CopilotChat.context.embed? -function M.get_file(filename, filetype) +---@return string?, string? +function M.get_file(filename) + local filetype = utils.filetype(filename) if not filetype then return nil end - 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 { - content = cached.content, - _modified = cached._modified, - filename = filename, - filetype = filetype, + local data = file_cache[filename] + if not data or data._modified < modified then + local content = utils.read_file(filename) + if not content or content == '' then + return nil + end + data = { + content = content, + _modified = modified, } + file_cache[filename] = data end - local content = utils.read_file(filename) - if not content or content == '' then - return nil - end - - local out = { - content = content, - filename = filename, - filetype = filetype, - _modified = modified, - } - - file_cache[filename] = out - return out + return data.content, utils.filetype_to_mimetype(filetype) end --- Get data for a buffer ---@param bufnr number ----@return CopilotChat.context.embed? +---@return string?, string? function M.get_buffer(bufnr) if not utils.buf_valid(bufnr) then return nil @@ -450,23 +429,18 @@ function M.get_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, - score = 0.1, - diagnostics = utils.diagnostics(bufnr), - } + return table.concat(content, '\n'), utils.filetype_to_mimetype(vim.bo[bufnr].filetype) end --- Get the content of an URL ---@param url string ----@return CopilotChat.context.embed? +---@return string?, string? function M.get_url(url) if not url or url == '' then return nil end + local ft = utils.filetype(url) local content = url_cache[url] if not content then local ok, out = async.util.apcall(utils.system, { 'lynx', '-dump', url }) @@ -504,37 +478,41 @@ function M.get_url(url) url_cache[url] = content end + return content, utils.filetype_to_mimetype(ft) +end + +--- Transform a resource into a format suitable for the client +---@param resource CopilotChat.config.functions.Result +---@return CopilotChat.client.Resource +function M.to_resource(resource) return { - content = content, - filename = url, - filetype = 'text', + name = utils.uri_to_filename(resource.uri), + type = utils.mimetype_to_filetype(resource.mimetype), + data = resource.data, } end ---- Filter embeddings based on the query +--- Process resources based on the query ---@param prompt string ---@param model string ----@param headless boolean ----@param embeddings table ----@return table -function M.filter_embeddings(prompt, model, headless, embeddings) +---@param resources table +---@return table +function M.process_resources(prompt, model, resources) -- If we dont need to embed anything, just return directly - if #embeddings < MULTI_FILE_THRESHOLD then - return embeddings + if #resources < MULTI_FILE_THRESHOLD then + return resources end notify.publish(notify.STATUS, 'Preparing embedding outline') - for _, input in ipairs(embeddings) do - -- Precalculate hash and attributes for caching - local hash = input.filename .. utils.quick_hash(input.content) + -- Get the outlines for each resource + for _, input in ipairs(resources) do + local hash = input.name .. utils.quick_hash(input.data) input._hash = hash - input.filename = input.filename or 'unknown' - input.filetype = input.filetype or 'text' local outline = outline_cache[hash] if not outline then - local outline_text, symbols = get_outline(input.content, input.filetype) + local outline_text, symbols = get_outline(input.data, input.type) if outline_text then outline = { outline = outline_text, @@ -555,32 +533,18 @@ function M.filter_embeddings(prompt, model, headless, embeddings) -- Build query from history and prompt local query = prompt - if not headless then - query = table.concat( - vim - .iter(client.history) - :filter(function(m) - return m.role == 'user' - end) - :map(function(m) - return vim.trim(m.content) - end) - :totable(), - '\n' - ) .. '\n' .. prompt - end -- Rank embeddings by symbols - embeddings = data_ranked_by_symbols(query, embeddings) - log.debug('Ranked data:', #embeddings) - for i, item in ipairs(embeddings) do - log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) + resources = data_ranked_by_symbols(query, resources) + log.debug('Ranked data:', #resources) + for i, item in ipairs(resources) do + log.debug(string.format('%s: %s - %s', i, item.score, item.name)) end -- Prepare embeddings for processing local to_process = {} local results = {} - for _, input in ipairs(embeddings) do + for _, input in ipairs(resources) do local hash = input._hash local embed = embedding_cache[hash] if embed then @@ -591,14 +555,13 @@ function M.filter_embeddings(prompt, model, headless, embeddings) end end table.insert(to_process, { - content = query, - filename = 'query', - filetype = 'raw', + type = 'text', + data = query, }) -- Embed the data and process the results for _, input in ipairs(client:embed(to_process, model)) do - if input.filetype ~= 'raw' then + if input._hash then embedding_cache[input._hash] = input.embedding end table.insert(results, input) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 2254c913..8bef366c 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,18 +1,16 @@ ----@class CopilotChat.select.selection +---@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 = {} --- Select and process current visual selection --- @param source CopilotChat.source ---- @return CopilotChat.select.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, '<')) @@ -35,18 +33,17 @@ function M.visual(source) return { content = lines_content, - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = start_line, end_line = finish_line, bufnr = bufnr, - diagnostics = utils.diagnostics(bufnr, start_line, finish_line), } end --- Select and process whole buffer --- @param source CopilotChat.source ---- @return CopilotChat.select.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) @@ -54,22 +51,19 @@ function M.buffer(source) return nil end - local out = { + return { content = table.concat(lines, '\n'), - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = 1, end_line = #lines, bufnr = bufnr, } - - out.diagnostics = utils.diagnostics(bufnr, out.start_line, out.end_line) - return out end --- Select and process current line --- @param source CopilotChat.source ---- @return CopilotChat.select.selection|nil +--- @return CopilotChat.select.Selection|nil function M.line(source) local bufnr = source.bufnr local winnr = source.winnr @@ -79,22 +73,19 @@ function M.line(source) return nil end - local out = { + return { content = line, - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + 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 = utils.diagnostics(bufnr, out.start_line, out.end_line) - return out end --- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. --- @param source CopilotChat.source ---- @return CopilotChat.select.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, '[')) @@ -117,12 +108,11 @@ function M.unnamed(source) return { content = lines_content, - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = start_line, end_line = finish_line, bufnr = bufnr, - diagnostics = utils.diagnostics(bufnr, start_line, finish_line), } end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 82af9789..f54e9435 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -14,71 +14,71 @@ function CopilotChatFoldExpr(lnum, separator) end local HEADER_PATTERNS = { - '%[file:.+%]%((.+)%) line:(%d+)-?(%d*)', - '%[file:(.+)%] line:(%d+)-?(%d*)', + '^```?(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', + '^```(%w+)$', } ---@param header? string ----@return string?, number?, number? +---@return string?, string?, number?, number? local function match_header(header) if not header then return end for _, pattern in ipairs(HEADER_PATTERNS) do - local filename, start_line, end_line = header:match(pattern) - if filename then - return utils.filepath(filename), tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 + local type, path, start_line, end_line = header:match(pattern) + if path then + return type, path, tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 + elseif type then + return type, 'block' end end end ----@class CopilotChat.ui.Chat.Section.Block.Header +---@class CopilotChat.ui.chat.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 +---@class CopilotChat.ui.chat.Block +---@field header CopilotChat.ui.chat.Header ---@field start_line number ---@field end_line number ---@field content string? ----@class CopilotChat.ui.Chat.Section ----@field answer boolean +---@class CopilotChat.ui.chat.Section ---@field start_line number ---@field end_line number ----@field blocks table ----@field content string? +---@field blocks table + +---@class CopilotChat.ui.chat.Message : CopilotChat.client.Message +---@field id string +---@field section CopilotChat.ui.chat.Section? ----@class CopilotChat.ui.Chat : CopilotChat.ui.Overlay +---@class CopilotChat.ui.chat.Chat : CopilotChat.ui.overlay.Overlay ---@field winnr number? ----@field config CopilotChat.config.shared ----@field layout CopilotChat.config.Layout? ----@field sections table ----@field references table +---@field config CopilotChat.config.Shared ---@field token_count number? ---@field token_max_count number? ----@field private question_header string ----@field private answer_header string +---@field messages table +---@field private layout CopilotChat.config.Layout? +---@field private headers table ---@field private separator string ---@field private header_ns number ----@field private spinner CopilotChat.ui.Spinner ----@field private chat_overlay CopilotChat.ui.Overlay -local Chat = class(function(self, question_header, answer_header, separator, help, on_buf_create) +---@field private spinner CopilotChat.ui.spinner.Spinner +---@field private chat_overlay CopilotChat.ui.overlay.Overlay +local Chat = class(function(self, headers, separator, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) self.winnr = nil - self.sections = {} self.config = {} - self.layout = nil - self.references = {} self.token_count = nil self.token_max_count = nil + self.messages = {} - self.question_header = question_header - self.answer_header = answer_header + self.layout = nil + self.headers = headers or {} self.separator = separator self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') @@ -110,10 +110,10 @@ function Chat:focused() return self:visible() and vim.api.nvim_get_current_win() == self.winnr end ---- Get the closest section to the cursor. ----@param type? "answer"|"question" If specified, only considers sections of the given type ----@return CopilotChat.ui.Chat.Section? -function Chat:get_closest_section(type) +--- Get the closest message to the cursor. +---@param role string? If specified, only considers sections of the given role +---@return CopilotChat.ui.chat.Message? +function Chat:get_closest_message(role) if not self:visible() then return nil end @@ -121,25 +121,23 @@ function Chat:get_closest_section(type) self:render() local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) local cursor_line = cursor_pos[1] - local closest_section = nil + local closest_message = nil local max_line_below_cursor = -1 - for _, section in ipairs(self.sections) do - local matches_type = not type - or (type == 'answer' and section.answer) - or (type == 'question' and not section.answer) - - if matches_type and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then + for _, message in ipairs(self.messages) do + local section = message.section + local matches_role = not role or message.role == role + if matches_role and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then max_line_below_cursor = section.start_line - closest_section = section + closest_message = message end end - return closest_section + return closest_message end --- Get the closest code block to the cursor. ----@return CopilotChat.ui.Chat.Section.Block? +---@return CopilotChat.ui.chat.Block? function Chat:get_closest_block() if not self:visible() then return nil @@ -151,7 +149,8 @@ function Chat:get_closest_block() local closest_block = nil local max_line_below_cursor = -1 - for _, section in pairs(self.sections) do + for _, message in pairs(self.messages) do + local section = message.section 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 @@ -163,39 +162,20 @@ function Chat:get_closest_block() return closest_block end ---- Get the prompt in the chat window. ----@return CopilotChat.ui.Chat.Section? -function Chat:get_prompt() +--- Get last message by role in the chat window. +---@return CopilotChat.ui.chat.Message? +function Chat:get_message(role) if not self:visible() then return end - self:render() - local section = self.sections[#self.sections] - if not section or section.answer then - return - end - - return section -end - ---- Set the prompt in the chat window. ----@param prompt string? -function Chat:set_prompt(prompt) - if not self:visible() then - return - end - - local section = self:get_prompt() - if not section then - return + for i = #self.messages, 1, -1 do + local message = self.messages[i] + local matches_role = not role or message.role == role + if matches_role then + return message + end end - - local modifiable = vim.bo[self.bufnr].modifiable - vim.bo[self.bufnr].modifiable = true - local lines = prompt and vim.split('\n' .. prompt, '\n') or {} - vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 1, section.end_line, false, lines) - vim.bo[self.bufnr].modifiable = modifiable end --- Add a sticky line to the prompt in the chat window. @@ -205,8 +185,8 @@ function Chat:add_sticky(sticky) return end - local prompt = self:get_prompt() - if not prompt then + local prompt = self:get_message('user') + if not prompt or not prompt.section then return end @@ -239,7 +219,7 @@ function Chat:add_sticky(sticky) return end - insert_line = prompt.start_line + insert_line - 1 + insert_line = prompt.section.start_line + insert_line - 1 local to_insert = first_one and { '> ' .. sticky, '' } or { '> ' .. sticky } local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true @@ -265,7 +245,7 @@ function Chat:overlay(opts) end --- Open the chat window. ----@param config CopilotChat.config.shared +---@param config CopilotChat.config.Shared function Chat:open(config) self:validate() @@ -415,6 +395,88 @@ function Chat:finish() end end +function Chat:add_message(message, replace) + local current_message = self.messages[#self.messages] + local needs_header = false + + -- Check if we need to add a header (role change or first message) + if not current_message or current_message.role ~= message.role then + needs_header = true + end + + -- Add appropriate header based on role + if needs_header then + message.id = message.id or utils.uuid() + local header = self.headers[message.role] + if current_message then + header = '\n' .. header + end + self:append(header .. '(' .. message.id .. ')' .. self.separator .. '\n\n') + elseif replace and current_message then + self:append('') + self:render() + + current_message.content = message.content + local section = current_message.section + + if section then + vim.bo[self.bufnr].modifiable = true + vim.api.nvim_buf_set_lines( + self.bufnr, + section.start_line - 1, + section.end_line, + false, + vim.split(message.content, '\n') + ) + vim.bo[self.bufnr].modifiable = false + end + + self:append('') + return + end + + -- Handle message content combining or creation + if current_message and current_message.role == message.role then + current_message.content = current_message.content .. message.content + self:append(message.content) + else + table.insert(self.messages, message) + self:append(message.content) + end +end + +function Chat:remove_message(role) + if not self:visible() then + return + end + + self:render() + local message = self:get_closest_message(role) + if not message then + return + end + + local section = message.section + if not section then + return + end + + -- Remove the section from the buffer + vim.bo[self.bufnr].modifiable = true + vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 2, section.end_line + 1, false, {}) + vim.bo[self.bufnr].modifiable = false + + -- Remove the message from the messages list + for i, msg in ipairs(self.messages) do + if msg.id == message.id then + table.remove(self.messages, i) + break + end + end + + self:render() +end + --- Append text to the chat window. ---@param str string function Chat:append(str) @@ -451,9 +513,9 @@ end --- Clear the chat window. function Chat:clear() self:validate() - self.references = {} self.token_count = nil self.token_max_count = nil + self.messages = {} vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) vim.bo[self.bufnr].modifiable = false @@ -493,81 +555,79 @@ end function Chat:render() 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 new_messages = {} + local current_message = nil local current_block = nil + local function parse_header(header, line) + return line:match('^' .. vim.pesc(header) .. '%(([%w%-]+)%)' .. vim.pesc(self.separator) .. '$') + end + for l, line in ipairs(lines) do - local separator_found = false - - if line == self.answer_header .. self.separator then - separator_found = true - if current_section then - current_section.end_line = l - 1 - current_section.content = - vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) - table.insert(sections, current_section) - end - current_section = { - answer = true, - start_line = l + 1, - blocks = {}, - } - elseif line == self.question_header .. self.separator then - separator_found = true - if current_section then - current_section.end_line = l - 1 - current_section.content = - vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) - 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 - current_section.content = - vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) - table.insert(sections, current_section) - end - end + -- Detect section header with ID + for header_name, header_value in pairs(self.headers) do + local id = parse_header(header_value, line) + if id then + -- Draw the separator as virtual text over the header line, hiding the id and anything after the header + if self.config.highlight_headers then + local sep_col = vim.fn.strwidth(header_value) + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep_col, { + virt_text = { + { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, + }, + virt_text_win_col = sep_col, + priority = 200, + strict = false, + }) + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + end_col = sep_col, + hl_group = 'CopilotChatHeader', + priority = 100, + strict = false, + }) + end - -- Highlight separators - if self.config.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, { - virt_text_win_col = sep, - virt_text = { - { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, - }, - priority = 100, - strict = false, - }) - -- header hl group - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { - end_col = sep + 1, - hl_group = 'CopilotChatHeader', - priority = 100, - strict = false, - }) - end + -- Finish previous message + if current_message then + current_message.section.end_line = l - 1 + current_message.content = vim.trim( + table.concat( + vim.list_slice(lines, current_message.section.start_line, current_message.section.end_line), + '\n' + ) + ) + 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]) + -- Find existing message by id or create new + local old_msg = nil + for _, msg in ipairs(self.messages) do + if msg.id == id then + old_msg = msg + break + end + end + if not old_msg then + old_msg = { id = id, role = header_name } end - filename = filename or 'code-block' + -- Attach section info + old_msg.section = { + role = header_name, + start_line = l + 1, + blocks = {}, + } + table.insert(new_messages, old_msg) + current_message = old_msg + current_block = nil + break + end + end + + -- Code blocks + if current_message and current_message.role == 'assistant' then + local filetype, filename, start_line, end_line = match_header(line) + if filetype and filename and not current_block then current_block = { header = { filename = filename, @@ -577,18 +637,96 @@ function Chat:render() }, start_line = l + 1, } + local text = string.format('[%s] %s', filetype, filename) + if start_line and end_line then + text = text .. string.format(' lines %d-%d', start_line, end_line) + end + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l, 0, { + virt_lines_above = true, + virt_lines = { { { text, 'CopilotChatAnnotationHeader' } } }, + priority = 100, + strict = false, + }) elseif line == '```' and current_block then current_block.end_line = l - 1 current_block.content = table.concat(vim.list_slice(lines, current_block.start_line, current_block.end_line), '\n') - table.insert(current_section.blocks, current_block) + table.insert(current_message.section.blocks, current_block) current_block = nil end end + + -- If last line, finish last message + if l == #lines and current_message then + current_message.section.end_line = l + current_message.content = vim.trim( + table.concat(vim.list_slice(lines, current_message.section.start_line, current_message.section.end_line), '\n') + ) + end + + -- Highlight response calls + for _, message in ipairs(self.messages) do + for _, tool_call in ipairs(message.tool_calls or {}) do + if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then + vim.api.nvim_buf_add_highlight(self.bufnr, self.header_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) + if not utils.empty(tool_call.arguments) then + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + virt_lines = vim.tbl_map(function(json_line) + return { { json_line, 'CopilotChatAnnotation' } } + end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), + priority = 100, + strict = false, + }) + end + break + end + end + end + end + + -- Replace self.messages with new_messages (preserving tool_calls, etc.) + self.messages = new_messages + + -- Show tool call details as virt lines + for _, message in ipairs(self.messages) do + if message.tool_calls and #message.tool_calls > 0 then + local section = message.section + if section and section.end_line then + local virt_lines = { { { 'Tool calls:', 'CopilotChatAnnotationHeader' } } } + for _, tc in ipairs(message.tool_calls) do + table.insert(virt_lines, { { string.format(' %s:%s', tc.name, tostring(tc.id)), 'CopilotChatAnnotation' } }) + for _, json_line in ipairs(vim.split(vim.inspect(utils.json_decode(tc.arguments)), '\n')) do + table.insert(virt_lines, { { ' ' .. json_line, 'CopilotChatAnnotation' } }) + end + end + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.end_line - 1, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + priority = 100, + strict = false, + }) + end + end + + if message.tool_call_id then + local section = message.section + if section and section.start_line then + local virt_lines = { + { { 'Tool: ' .. message.tool_call_id, 'CopilotChatAnnotationHeader' } }, + } + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.start_line, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + priority = 100, + strict = false, + }) + end + end end - local last_section = sections[#sections] - if last_section and not last_section.answer then + -- Show help as before, using last user message + local last_message = self.messages[#self.messages] + if last_message and last_message.role == 'user' then local msg = self.config.show_help and self.help or '' if self.token_count and self.token_max_count then if msg ~= '' then @@ -596,29 +734,10 @@ function Chat:render() 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) - - if not utils.empty(self.references) and self.config.references_display == 'virtual' then - msg = 'References:\n' - for _, ref in ipairs(self.references) do - msg = msg .. ' ' .. ref.name .. '\n' - end - - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, last_section.start_line - 2, 0, { - hl_mode = 'combine', - priority = 100, - virt_lines_above = true, - virt_lines = vim.tbl_map(function(t) - return { { t, 'CopilotChatHelp' } } - end, vim.split(msg, '\n')), - }) - end + self:show_help(msg, last_message.section.start_line - last_message.section.end_line - 1) else self:show_help() end - - self.sections = sections end --- Get the last line and column of the chat window. diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 9b70cb5e..a23c022e 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 : Class +---@class CopilotChat.ui.overlay.Overlay : Class ---@field bufnr number? ---@field protected name string ---@field protected help string @@ -19,10 +19,6 @@ local Overlay = class(function(self, name, help, on_buf_create) self.on_hide = nil self.help_ns = vim.api.nvim_create_namespace('copilot-chat-help') - 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) }) end) --- Show the overlay buffer @@ -38,7 +34,6 @@ function Overlay:show(text, winnr, filetype, syntax, on_show, on_hide) end self:validate() - vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) text = text .. '\n' self.cursor = vim.api.nvim_win_get_cursor(winnr) @@ -122,7 +117,6 @@ function Overlay:restore(winnr, bufnr) end vim.api.nvim_win_set_buf(winnr, bufnr) - vim.api.nvim_win_set_hl_ns(winnr, 0) if self.cursor then vim.api.nvim_win_set_cursor(winnr, self.cursor) diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index 4b69ae44..0f582032 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -14,7 +14,7 @@ local spinner_frames = { '⠏', } ----@class CopilotChat.ui.Spinner : Class +---@class CopilotChat.ui.spinner.Spinner : Class ---@field bufnr number ---@field status string? ---@field private index number diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index afd92c5d..1cf1a151 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,6 +1,7 @@ local async = require('plenary.async') local curl = require('plenary.curl') local scandir = require('plenary.scandir') +local log = require('plenary.log') local M = {} M.timers = {} @@ -102,6 +103,24 @@ function M.ordered_map() } end +--- Convert arguments to a table +---@param ... any The arguments +---@return table +function M.to_table(...) + local result = {} + for i = 1, select('#', ...) do + local x = select(i, ...) + if type(x) == 'table' then + for _, v in ipairs(x) do + table.insert(result, v) + end + elseif x ~= nil then + table.insert(result, x) + end + end + return result +end + ---@class StringBuffer ---@field add fun(self:StringBuffer, s:string) ---@field set fun(self:StringBuffer, s:string) @@ -149,26 +168,6 @@ function M.temp_file(text) return temp_file end ---- Blend a color with the neovim background ----@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 - - 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() @@ -179,11 +178,6 @@ function M.return_to_normal_mode() end end ---- Mark a function as deprecated -function M.deprecate(old, new) - vim.deprecate(old, new, '3.0.X', 'CopilotChat.nvim', false) -end - --- Debounce a function function M.debounce(id, fn, delay) if M.timers[id] then @@ -193,21 +187,6 @@ 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 --- Check if the buffer is not a terminal ---@param bufnr number? The buffer number @@ -246,6 +225,53 @@ function M.filetype(filename) return ft end +--- Get the mimetype from filetype +---@param filetype string? +---@return string +function M.filetype_to_mimetype(filetype) + if not filetype or filetype == '' then + return 'text/plain' + end + if filetype == 'json' or filetype == 'yaml' then + return 'application/' .. filetype + end + if filetype == 'html' or filetype == 'css' then + return 'text/' .. filetype + end + return 'text/x-' .. filetype +end + +--- Get the filetype from mimetype +---@param mimetype string? +---@return string +function M.mimetype_to_filetype(mimetype) + if not mimetype or mimetype == '' then + return 'text' + end + + local out = mimetype:gsub('^text/x%-', '') + out = out:gsub('^text/', '') + out = out:gsub('^application/', '') + out = out:gsub('^image/', '') + out = out:gsub('^video/', '') + out = out:gsub('^audio/', '') + return out +end + +--- Convert a URI to a file name +---@param uri string The URI +---@return string +function M.uri_to_filename(uri) + if not uri or uri == '' then + return uri + end + local ok, fname = pcall(vim.uri_to_fname, uri) + if not ok or M.empty(fname) then + return uri + end + return fname +end + --- Get the file name ---@param filepath string The file path ---@return string @@ -253,13 +279,6 @@ function M.filename(filepath) return vim.fs.basename(filepath) 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() @@ -291,6 +310,13 @@ function M.make_string(...) x = vim.inspect(x) else x = tostring(x) + while true do + local new_x = x:gsub('^[^:]+:%d+: ', '') + if new_x == x then + break + end + x = new_x + end end t[#t + 1] = x @@ -329,8 +355,10 @@ end ---@param opts table? The options ---@async M.curl_get = async.wrap(function(url, opts, callback) + log.debug('GET request:', url, opts) local args = { on_error = function(err) + log.debug('GET error:', err) callback(nil, err and err.stderr or err) end, } @@ -339,6 +367,7 @@ M.curl_get = async.wrap(function(url, opts, callback) args = vim.tbl_deep_extend('force', args, opts or {}) args.callback = function(response) + log.debug('GET response:', response) if response and not vim.startswith(tostring(response.status), '20') then callback(response, response.body) return @@ -366,9 +395,10 @@ end, 3) ---@param opts table? The options ---@async M.curl_post = async.wrap(function(url, opts, callback) + log.debug('POST request:', url, opts) local args = { - callback = callback, on_error = function(err) + log.debug('POST error:', err) callback(nil, err and err.stderr or err) end, } @@ -376,13 +406,8 @@ M.curl_post = async.wrap(function(url, opts, callback) args = vim.tbl_deep_extend('force', M.curl_args, args) args = vim.tbl_deep_extend('force', args, opts or {}) - if args.json_response then - args.headers = vim.tbl_deep_extend('force', args.headers or {}, { - Accept = 'application/json', - }) - end - args.callback = function(response) + log.debug('POST response:', url, response) if response and not vim.startswith(tostring(response.status), '20') then callback(response, response.body) return @@ -402,6 +427,12 @@ M.curl_post = async.wrap(function(url, opts, callback) end end + if args.json_response then + args.headers = vim.tbl_deep_extend('force', args.headers or {}, { + Accept = 'application/json', + }) + end + if args.json_request then args.headers = vim.tbl_deep_extend('force', args.headers or {}, { ['Content-Type'] = 'application/json', @@ -413,7 +444,18 @@ M.curl_post = async.wrap(function(url, opts, callback) curl.post(url, args) end, 3) ----@class CopilotChat.utils.scan_dir_opts +local function filter_files(files, max_count) + files = vim.tbl_filter(function(file) + return file ~= '' and M.filetype(file) ~= nil + end, files) + if max_count and max_count > 0 then + files = vim.list_slice(files, 1, max_count) + end + + return files +end + +---@class CopilotChat.utils.ScanOpts ---@field max_count number? The maximum number of files to scan ---@field max_depth number? The maximum depth to scan ---@field glob? string The glob pattern to match files @@ -421,30 +463,19 @@ end, 3) ---@field no_ignore? boolean Whether to respect or ignore .gitignore --- Scan a directory ----@param path string The directory path ----@param opts CopilotChat.utils.scan_dir_opts? The options +---@param path string +---@param opts CopilotChat.utils.ScanOpts? ---@async -M.scan_dir = async.wrap(function(path, opts, callback) +M.glob = async.wrap(function(path, opts, callback) opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) - local function filter_files(files) - files = vim.tbl_filter(function(file) - return file ~= '' and M.filetype(file) ~= nil - end, files) - if opts.max_count and opts.max_count > 0 then - files = vim.list_slice(files, 1, opts.max_count) - end - - return files - end - -- Use ripgrep if available if vim.fn.executable('rg') == 1 then local cmd = { 'rg' } - if opts.glob then + if opts.pattern then table.insert(cmd, '-g') - table.insert(cmd, opts.glob) + table.insert(cmd, opts.pattern) end if opts.max_depth then @@ -466,7 +497,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) vim.system(cmd, { text = true }, function(result) local files = {} if result and result.code == 0 and result.stdout ~= '' then - files = filter_files(vim.split(result.stdout, '\n')) + files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) end callback(files) @@ -484,12 +515,71 @@ M.scan_dir = async.wrap(function(path, opts, callback) search_pattern = opts.glob and M.glob_to_pattern(opts.glob) or nil, respect_gitignore = not opts.no_ignore, on_exit = function(files) - callback(filter_files(files)) + callback(filter_files(files, opts.max_count)) end, }) ) end, 3) +--- Grep a directory +---@param path string The path to search +---@param opts CopilotChat.utils.ScanOpts? +M.grep = async.wrap(function(path, opts, callback) + opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) + local cmd = {} + + if vim.fn.executable('rg') == 1 then + table.insert(cmd, 'rg') + + if opts.max_depth then + table.insert(cmd, '--max-depth') + table.insert(cmd, tostring(opts.max_depth)) + end + + if opts.no_ignore then + table.insert(cmd, '--no-ignore') + end + + if opts.hidden then + table.insert(cmd, '--hidden') + end + + table.insert(cmd, '--files-with-matches') + table.insert(cmd, '--ignore-case') + + if opts.pattern then + table.insert(cmd, '-e') + table.insert(cmd, "'" .. opts.pattern .. "'") + end + + table.insert(cmd, path) + elseif vim.fn.executable('grep') == 1 then + table.insert(cmd, 'grep') + table.insert(cmd, '-rli') + + if opts.pattern then + table.insert(cmd, '-e') + table.insert(cmd, "'" .. opts.pattern .. "'") + end + + table.insert(cmd, path) + end + + if M.empty(cmd) then + error('No executable found for grep') + return + end + + vim.system(cmd, { text = true }, function(result) + local files = {} + if result and result.code == 0 and result.stdout ~= '' then + files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) + end + + callback(files) + end) +end, 3) + --- Get last modified time of a file ---@param path string The file path ---@return number? @@ -587,6 +677,46 @@ M.ts_parse = async.wrap(function(parser, callback) end end, 2) +--- Wait for a user input +M.input = async.wrap(function(opts, callback) + local fn = function() + vim.ui.input(opts, function(input) + if input == nil or input == '' then + callback(nil) + return + end + + callback(input) + end) + end + + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() + end +end, 2) + +--- Select an item from a list +M.select = async.wrap(function(choices, opts, callback) + local fn = function() + vim.ui.select(choices, opts, function(item) + if item == nil or item == '' then + callback(nil) + return + end + + callback(item) + end) + end + + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() + end +end, 3) + --- Get the info for a key. ---@param name string ---@param key table @@ -770,40 +900,4 @@ function M.glob_to_pattern(g) return p end ----@class CopilotChat.Diagnostic ----@field content string ----@field start_line number ----@field end_line number ----@field severity string - ---- Get diagnostics in a given range ---- @param bufnr number ---- @param start_line number? ---- @param end_line number? ---- @return table|nil -function M.diagnostics(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 (not start_line or lnum >= start_line) and (not end_line or lnum <= end_line) then - table.insert(range_diagnostics, { - severity = severity[diagnostic.severity], - content = diagnostic.message, - start_line = lnum, - end_line = diagnostic.end_lnum and diagnostic.end_lnum + 1 or lnum, - }) - end - end - - return #range_diagnostics > 0 and range_diagnostics or nil -end - return M diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index 5674d6bc..de5e0158 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -8,14 +8,29 @@ if vim.fn.has('nvim-' .. min_version) ~= 1 then return end +local group = vim.api.nvim_create_augroup('CopilotChat', {}) + -- Setup highlights -vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { link = 'Keyword', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatInput', { link = 'Special', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) +local function setup_highlights() + vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { link = 'Keyword', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatAnnotation', { link = 'ColorColumn', default = true }) + + local fg = vim.api.nvim_get_hl(0, { name = 'CopilotChatStatus', link = false }).fg + local bg = vim.api.nvim_get_hl(0, { name = 'CopilotChatAnnotation', link = false }).bg + vim.api.nvim_set_hl(0, 'CopilotChatAnnotationHeader', { fg = fg, bg = bg }) +end +vim.api.nvim_create_autocmd('ColorScheme', { + group = group, + callback = function() + setup_highlights() + end, +}) +setup_highlights() -- Setup commands vim.api.nvim_create_user_command('CopilotChat', function(args) @@ -39,10 +54,6 @@ vim.api.nvim_create_user_command('CopilotChatModels', function() local chat = require('CopilotChat') chat.select_model() end, { force = true }) -vim.api.nvim_create_user_command('CopilotChatAgents', function() - local chat = require('CopilotChat') - chat.select_agent() -end, { force = true }) vim.api.nvim_create_user_command('CopilotChatOpen', function() local chat = require('CopilotChat') chat.open() @@ -90,7 +101,7 @@ end, { nargs = '*', force = true, complete = complete_load }) -- 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 = vim.api.nvim_create_augroup('CopilotChat', {}), + group = group, callback = function() vim.w.cchat_cwd = vim.fn.getcwd() end, From 32de1bbfacfb7905afb521c7ea22deaa92442d1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 00:22:08 +0000 Subject: [PATCH 394/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 312 +++++++++++++++----------------------------- 1 file changed, 106 insertions(+), 206 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e5863a11..f8e9ea04 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 24 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -14,8 +14,8 @@ Table of Contents *CopilotChat-table-of-contents* - Commands |CopilotChat-commands| - Key Mappings |CopilotChat-key-mappings| - Prompts |CopilotChat-prompts| - - Models and Agents |CopilotChat-models-and-agents| - - Contexts |CopilotChat-contexts| + - Models |CopilotChat-models| + - Functions |CopilotChat-functions| - Selections |CopilotChat-selections| - Providers |CopilotChat-providers| 4. Configuration |CopilotChat-configuration| @@ -36,14 +36,14 @@ Table of Contents *CopilotChat-table-of-contents* CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities directly into your editor. It provides: -- 🤖 GitHub Copilot Chat integration with official model and agent support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) +- 🤖 GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) - 💻 Rich workspace context powered by smart embeddings system -- 🔒 Explicit context sharing - only sends what you specifically request, either as context or selection (by default visual selection) -- 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, LM Studio, Mistral.ai and more) +- 🔒 Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) +- 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) - 📝 Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts -- 🔄 Extensible context providers for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚡ Efficient token usage with tiktoken token counting and memory management +- 🔄 Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) +- ⚡ Efficient token usage with tiktoken token counting and history management ============================================================================== @@ -85,8 +85,7 @@ Plugin features that use picker: - `:CopilotChatPrompts` - for selecting prompts - `:CopilotChatModels` - for selecting models -- `:CopilotChatAgents` - for selecting agents -- `#:` - for selecting context input +- `#:` - for selecting function input ============================================================================== @@ -103,7 +102,7 @@ LAZY.NVIM *CopilotChat-lazy.nvim* { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, - build = "make tiktoken", -- Only on MacOS or Linux + build = "make tiktoken", opts = { -- See Configuration section for options }, @@ -181,7 +180,6 @@ Commands are used to control the chat interface: :CopilotChatLoad ? Load chat history :CopilotChatPrompts View/select prompt templates :CopilotChatModels View/select available models - :CopilotChatAgents View/select available agents :CopilotChat Use specific prompt template KEY MAPPINGS *CopilotChat-key-mappings* @@ -292,7 +290,7 @@ Define your own system prompts in the configuration (similar to `prompts`): STICKY PROMPTS ~ Sticky prompts persist across chat sessions. They’re useful for maintaining -context or agent selection. They work as follows: +model or resource selection. They work as follows: 1. Prefix text with `>` using markdown blockquote syntax 2. The prompt will be copied at the start of every new chat prompt @@ -301,7 +299,7 @@ context or agent selection. They work as follows: Examples: >markdown - > #files + > #glob:`*.lua` > List all files in the workspace > @models Using Mistral-small @@ -313,17 +311,13 @@ You can also set default sticky prompts in the configuration: >lua { sticky = { - '@models Using Mistral-small', - '#files', + '#glob:*.lua', } } < -MODELS AND AGENTS *CopilotChat-models-and-agents* - - -MODELS ~ +MODELS *CopilotChat-models* You can control which AI model to use in three ways: @@ -337,72 +331,84 @@ For supported models, see: - GitHub Marketplace Models (experimental, limited usage) -AGENTS ~ +FUNCTIONS *CopilotChat-functions* + +Functions provide additional information and behaviour to the chat. Tools can +be organized into groups by setting the `group` property. Tools assigned to a +group are not automatically made available to the LLM - they must be explicitly +activated. To use grouped tools in your prompt, include `@group_name` in your +message. This allows the LLM to access and use all tools in that group during +the current interaction. Add tools using `#tool_name[:input]` syntax: -Agents determine the AI assistant’s capabilities. Control agents in three -ways: + -------------------------------------------------------------------------- + Function Input Description + Support + ------------- ------------ ----------------------------------------------- + buffer ✓ (name) Retrieves content from a specific buffer -1. List available agents with `:CopilotChatAgents` -2. Set agent in prompt with `@agent_name` -3. Configure default agent via `agent` config key + buffers ✓ (scope) Fetches content from multiple buffers + (listed/visible) -The default "noop" agent is `none`. For more information: + diagnostics ✓ (scope) Collects code diagnostics (errors, warnings) -- Extension Agents Documentation -- Available Agents + file ✓ (path) Reads content from a specified file path + gitdiff ✓ (sha) Retrieves git diff information + (unstaged/staged/sha) -CONTEXTS *CopilotChat-contexts* + gitstatus - Retrieves git status information -Contexts provide additional information to the chat. Add context using -`#context_name[:input]` syntax: + glob ✓ (pattern) Lists filenames matching a pattern in workspace - Context Input Support Description - ----------- --------------- ------------------------------------- - buffer ✓ (number) Current or specified buffer content - buffers ✓ (type) All buffers content (listed/all) - file ✓ (path) Content of specified file - files ✓ (glob) Workspace files - filenames ✓ (glob) Workspace file names - git ✓ (ref) Git diff (unstaged/staged/commit) - url ✓ (url) Content from URL - register ✓ (name) Content of vim register - quickfix - Quickfix list file contents - system ✓ (command) Output of shell command + grep ✓ (pattern) Searches for a pattern across files in + workspace - [!TIP] The AI is aware of these context providers and may request additional - context if needed by asking you to input a specific context command like - `#file:path/to/file.js`. + quickfix - Includes content of files in quickfix list + + register ✓ (register) Provides access to specified Vim register + + url ✓ (url) Fetches content from a specified URL + -------------------------------------------------------------------------- Examples: >markdown - > #buffer - > #buffer:2 - > #files:\*.lua - > #filenames + > #buffer:init.lua + > #buffers:visible + > #diagnostics:current + > #file:path/to/file.js > #git:staged + > #glob:`**/*.lua` + > #grep:`function setup` + > #quickfix + > #register:+ > #url:https://example.com - > #system:`ls -la | grep lua` < -Define your own contexts in the configuration with input handling and -resolution: +Define your own functions in the configuration with input handling and schema: >lua { - contexts = { + functions = { birthday = { - input = function(callback) - vim.ui.select({ 'user', 'napoleon' }, { - prompt = 'Select birthday> ', - }, callback) - end, + description = "Retrieves birthday information for a person", + uri = "birthday://{name}", + schema = { + type = 'object', + required = { 'name' }, + properties = { + name = { + type = 'string', + enum = { 'Alice', 'Bob', 'Charlie' }, + description = "Person's name", + }, + }, + }, resolve = function(input) return { { - content = input .. ' birthday info', - filename = input .. '_birthday', - filetype = 'text', + uri = 'birthday://' .. input.name, + mimetype = 'text/plain', + data = input.name .. ' birthday info', } } end @@ -412,10 +418,10 @@ resolution: < -EXTERNAL CONTEXTS ~ +EXTERNAL FUNCTIONS ~ -For external contexts, see the contexts discussion page -. +For external functions implementations, see the discussion page +. SELECTIONS *CopilotChat-selections* @@ -481,9 +487,6 @@ Custom providers can implement these methods: -- Optional: Get available models get_models?(headers: table): table, - - -- Optional: Get available agents - get_agents?(headers: table): table, } < @@ -511,19 +514,17 @@ Below are all available configuration options with their default values: system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- 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 or array of contexts to use (can be specified manually in prompt via #). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - temperature = 0.1, -- GPT result temperature + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) - callback = nil, -- Function called when full response is received (retuned string is stored to history) - remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions + callback = nil, -- Function called when full response is received + remember_as_sticky = true, -- Remember model as sticky prompts when asking questions -- default selection -- see select.lua for implementation - selection = select.visual, + selection = require('CopilotChat.select').visual, -- default window options window = { @@ -541,9 +542,9 @@ Below are all available configuration options with their default values: }, 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) - references_display = 'virtual', -- 'virtual', 'write', Display references in chat as virtual text or write to buffer 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 @@ -561,129 +562,29 @@ Below are all available configuration options with their default values: log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file 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 + headers = { + user = '## User ', -- Header to use for user questions + assistant = '## Copilot ', -- Header to use for AI answers + tool = '## Tool ', -- Header to use for tool calls + }, + separator = '───', -- Separator to use in chat -- default providers -- see config/providers.lua for implementation - providers = { - copilot = { - }, - github_models = { - }, - copilot_embeddings = { - }, - }, + providers = require('CopilotChat.config.providers'), - -- default contexts - -- see config/contexts.lua for implementation - contexts = { - buffer = { - }, - buffers = { - }, - file = { - }, - files = { - }, - git = { - }, - url = { - }, - register = { - }, - quickfix = { - }, - system = { - } - }, + -- default functions + -- see config/functions.lua for implementation + functions = require('CopilotChat.config.functions'), -- default prompts -- see config/prompts.lua for implementation - prompts = { - Explain = { - prompt = 'Write an explanation for the selected code as paragraphs of text.', - system_prompt = 'COPILOT_EXPLAIN', - }, - Review = { - prompt = 'Review the selected code.', - system_prompt = 'COPILOT_REVIEW', - }, - Fix = { - prompt = 'There is a problem in this code. Identify the issues and rewrite the code with fixes. Explain what was wrong and how your changes address the problems.', - }, - Optimize = { - prompt = 'Optimize the selected code to improve performance and readability. Explain your optimization strategy and the benefits of your changes.', - }, - Docs = { - prompt = 'Please add documentation comments to the selected code.', - }, - Tests = { - prompt = 'Please generate tests for my code.', - }, - Commit = { - prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - context = 'git:staged', - }, - }, + prompts = require('CopilotChat.config.prompts'), -- default mappings -- see config/mappings.lua for implementation - mappings = { - complete = { - insert = '', - }, - close = { - normal = 'q', - insert = '', - }, - reset = { - normal = '', - insert = '', - }, - submit_prompt = { - normal = '', - insert = '', - }, - toggle_sticky = { - normal = 'grr', - }, - clear_stickies = { - normal = 'grx', - }, - accept_diff = { - normal = '', - insert = '', - }, - jump_to_diff = { - normal = 'gj', - }, - quickfix_answers = { - normal = 'gqa', - }, - quickfix_diffs = { - normal = 'gqd', - }, - yank_diff = { - normal = 'gy', - register = '"', -- Default register to use for yanking - }, - show_diff = { - normal = 'gd', - full_diff = false, -- Show full diff instead of unified diff when showing diff window - }, - show_info = { - normal = 'gi', - }, - show_context = { - normal = 'gc', - }, - show_help = { - normal = 'gh', - }, - }, + mappings = require('CopilotChat.config.mappings'), } < @@ -719,8 +620,8 @@ Types of copilot highlights: - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help messages in chat buffer (help, references) - `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, contexts) -- `CopilotChatInput` - Input highlight in chat buffer (for contexts) +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) ============================================================================== @@ -736,8 +637,7 @@ CORE *CopilotChat-core* chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references - chat.resolve_context() -- Resolve context embeddings (WARN: async, requires plenary.async.run) - chat.resolve_agent() -- Resolve agent from prompt (WARN: async, requires plenary.async.run) + chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -755,10 +655,9 @@ CORE *CopilotChat-core* chat.get_selection() -- Get the current selection chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection - -- Prompt & Context Management + -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector - chat.select_agent() -- Open agent selector chat.prompts() -- Get all available prompts -- Completion @@ -787,10 +686,12 @@ You can also access the chat window UI methods through the `chat.chat` object: window:visible() -- Check if chat window is visible window:focused() -- Check if chat window is focused + -- Message Management + window:get_message(role) -- Get last chat message by role (user, assistant, tool) + window:add_message({ role, content }, replace) -- Add or replace a message in chat + window:add_sticky(sticky) -- Add sticky prompt to chat message + -- Content Management - window:get_prompt() -- Get current prompt from chat window - window:set_prompt(prompt) -- Set prompt in chat window - window:add_sticky(sticky) -- Add sticky prompt to chat window window:append(text) -- Append text to chat window window:clear() -- Clear chat window content window:finish() -- Finish writing to chat window @@ -800,9 +701,9 @@ You can also access the chat window UI methods through the `chat.chat` object: window:focus() -- Focus the chat window -- Advanced Features - window:get_closest_section() -- Get section closest to cursor - window:get_closest_block() -- Get code block closest to cursor - window:overlay(opts) -- Show overlay with specified options + window:get_closest_message(role) -- Get message closest to cursor + window:get_closest_block(role) -- Get code block closest to cursor + window:overlay(opts) -- Show overlay with specified options < @@ -811,19 +712,18 @@ EXAMPLE USAGE *CopilotChat-example-usage* >lua -- Open chat, ask a question and handle response require("CopilotChat").open() - require("CopilotChat").ask("Explain this code", { + require("CopilotChat").ask("#buffer Explain this code", { callback = function(response) vim.notify("Got response: " .. response:sub(1, 50) .. "...") return response end, - context = "buffer" }) -- Save and load chat history require("CopilotChat").save("my_debugging_session") require("CopilotChat").load("my_debugging_session") - -- Use custom context and model + -- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", context = {"buffer", "git:staged"} @@ -880,7 +780,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖This project follows the all-contributors specification. Contributions of any kind are welcome! From b4b7f9c2bb34d43b18dbbe0a889881630e217bc3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 04:30:14 +0200 Subject: [PATCH 395/728] fix: update to latest lua actions and update README (#1196) Signed-off-by: Tomas Slusny --- .github/workflows/ci.yml | 8 +++++--- README.md | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5831a56..0a96b7f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,12 +53,14 @@ jobs: version: nightly - name: luajit - uses: leafo/gh-actions-lua@v10 + uses: leafo/gh-actions-lua@v11 with: - luaVersion: "luajit-openresty" + luaVersion: "luajit-2.1" - name: luarocks - uses: leafo/gh-actions-luarocks@v4 + uses: leafo/gh-actions-luarocks@v5 + with: + luarocksVersion: "3.12.2" - name: run test shell: bash diff --git a/README.md b/README.md index 6c6eb93f..0535af19 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,7 @@ Default mappings in the chat interface: | - | `gqd` | Add all diffs from chat to quickfix list | | - | `gy` | Yank nearest diff to register | | - | `gd` | Show diff between source and nearest diff | -| - | `gi` | Show info about current chat | -| - | `gc` | Show current chat context | +| - | `gc` | Show info about current chat | | - | `gh` | Show help message | The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: @@ -648,7 +647,7 @@ require("CopilotChat").load("my_debugging_session") -- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", - context = {"buffer", "git:staged"} + sticky = {"#buffer", "#gitdiff:staged"} }) ``` From 84a5728b91970b162a116ad000c7084eeef4315c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 02:30:31 +0000 Subject: [PATCH 396/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f8e9ea04..d2567481 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -200,8 +200,7 @@ Default mappings in the chat interface: - gqd Add all diffs from chat to quickfix list - gy Yank nearest diff to register - gd Show diff between source and nearest diff - - gi Show info about current chat - - gc Show current chat context + - gc Show info about current chat - gh Show help message The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: @@ -726,7 +725,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* -- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", - context = {"buffer", "git:staged"} + sticky = {"#buffer", "#gitdiff:staged"} }) < From a9e2e657f5b5b8ec212283466038688d98ea4038 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 04:39:05 +0200 Subject: [PATCH 397/728] chore: allow manual dispatch on actions (#1197) Signed-off-by: Tomas Slusny --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 4 ++-- .github/workflows/todo.yml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a96b7f3..e722714f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: jobs: lint: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a809e59e..cc8398cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,8 @@ name: Release on: push: - branches: - - main + branches: [main] + workflow_dispatch: permissions: contents: write diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index d70ff362..ebf3e56e 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -1,6 +1,7 @@ name: "Convert TODO to Issue" on: push: + branches: [main] workflow_dispatch: inputs: MANUAL_COMMIT_REF: From dd0616661505a3c4892ddcdb9517b720a74e59b8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 05:28:13 +0200 Subject: [PATCH 398/728] fix(functions): properly handle multiple tool calls at once (#1198) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 ++- lua/CopilotChat/ui/chat.lua | 29 ++++++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1f592349..c4da582e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -926,9 +926,10 @@ function M.ask(prompt, config) for _, tool in ipairs(resolved_tools) do M.chat:add_message({ + id = tool.id, role = 'tool', tool_call_id = tool.id, - content = tool.result .. '\n', + content = '\n' .. tool.result .. '\n', }) end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index f54e9435..0b1fdc32 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -397,22 +397,23 @@ end function Chat:add_message(message, replace) local current_message = self.messages[#self.messages] - local needs_header = false + local is_new = not current_message + or current_message.role ~= message.role + or (message.id and current_message.id ~= message.id) - -- Check if we need to add a header (role change or first message) - if not current_message or current_message.role ~= message.role then - needs_header = true - end - - -- Add appropriate header based on role - if needs_header then + if is_new then + -- Add appropriate header based on role and generate a new ID if not provided message.id = message.id or utils.uuid() local header = self.headers[message.role] if current_message then header = '\n' .. header end + + table.insert(self.messages, message) self:append(header .. '(' .. message.id .. ')' .. self.separator .. '\n\n') + self:append(message.content) elseif replace and current_message then + -- Replace the content of the current message self:append('') self:render() @@ -432,15 +433,9 @@ function Chat:add_message(message, replace) end self:append('') - return - end - - -- Handle message content combining or creation - if current_message and current_message.role == message.role then - current_message.content = current_message.content .. message.content - self:append(message.content) else - table.insert(self.messages, message) + -- Append to the current message + current_message.content = current_message.content .. message.content self:append(message.content) end end @@ -561,7 +556,7 @@ function Chat:render() local current_block = nil local function parse_header(header, line) - return line:match('^' .. vim.pesc(header) .. '%(([%w%-]+)%)' .. vim.pesc(self.separator) .. '$') + return line:match('^' .. vim.pesc(header) .. '%(([^)]+)%)' .. vim.pesc(self.separator) .. '$') end for l, line in ipairs(lines) do From e0df6d1242af29b6262b0eb3e4248568c57c4b3e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 05:34:36 +0200 Subject: [PATCH 399/728] fix(quickfix): use new chat messages instead of old chat sections for populating qf (#1199) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 58 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index a527f0c3..bdcb93ce 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -263,18 +263,18 @@ return { normal = 'gqa', callback = function() local items = {} - for i, section in ipairs(copilot.chat.sections) do - if section.role == 'assistant' then - local prev_section = copilot.chat.sections[i - 1] + for i, message in ipairs(copilot.chat.messages) do + if message.section and message.role == 'assistant' then + local prev_message = copilot.chat.messages[i - 1] local text = '' - if prev_section then - text = prev_section.content + if prev_message then + text = prev_message.content end table.insert(items, { bufnr = copilot.chat.bufnr, - lnum = section.start_line, - end_lnum = section.end_line, + lnum = message.section.start_line, + end_lnum = message.section.end_line, text = text, }) end @@ -291,32 +291,34 @@ return { local selection = copilot.get_selection() local items = {} - for _, section in ipairs(copilot.chat.sections) do - for _, block in ipairs(section.blocks) do - local header = block.header + for _, message in ipairs(copilot.chat.messages) do + if message.section then + for _, block in ipairs(message.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 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 - 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 + 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 = copilot.chat.bufnr, - lnum = block.start_line, - end_lnum = block.end_line, - text = text, - }) + table.insert(items, { + bufnr = copilot.chat.bufnr, + lnum = block.start_line, + end_lnum = block.end_line, + text = text, + }) + end end - end - vim.fn.setqflist(items) - vim.cmd('copen') + vim.fn.setqflist(items) + vim.cmd('copen') + end end, }, From 946069a03946ce35619cbacc3a6757819d096ac5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 16:45:55 +0200 Subject: [PATCH 400/728] fix(functions): properly resolve defaults for diagnostics (#1201) Closes #1200 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 4 ++-- lua/CopilotChat/functions.lua | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 07b11573..1644a03f 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -258,12 +258,12 @@ return { diagnostics = { group = 'copilot', - uri = 'neovim://diagnostics/{scope}', + uri = 'neovim://diagnostics/{scope}/{severity}', description = 'Collects code diagnostics (errors, warnings, etc.) from specified buffers. Helpful for troubleshooting and fixing code issues.', schema = { type = 'object', - required = { 'scope' }, + required = { 'scope', 'severity' }, properties = { scope = { type = 'string', diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 8a28ca81..6cc287bb 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -136,8 +136,7 @@ function M.parse_input(input, schema) local prop_names = sorted_propnames(schema) -- Map input parts to schema properties in sorted order - local i = 1 - for _, prop_name in ipairs(prop_names) do + for i, prop_name in ipairs(prop_names) do local prop_schema = schema.properties[prop_name] local value = not utils.empty(parts[i]) and parts[i] or nil if value == nil and prop_schema.default ~= nil then @@ -145,10 +144,6 @@ function M.parse_input(input, schema) end result[prop_name] = value - i = i + 1 - if i > #parts then - break - end end return result From 6ac77aaa68a0ce7fe3c8c41622ab1986f8f6d2c7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 17:09:55 +0200 Subject: [PATCH 401/728] feat(resources)!: add option to enable resource processing (#1202) This adds option to enable resource processing, disabled by default. BREAKING CHANGE: intelligent resource processing is now disabled by default, use config.resource_processing: true to reenable Signed-off-by: Tomas Slusny --- README.md | 4 +++- lua/CopilotChat/client.lua | 20 +++----------------- lua/CopilotChat/config.lua | 5 ++++- lua/CopilotChat/init.lua | 14 +++++++++----- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0535af19..931ee8ab 100644 --- a/README.md +++ b/README.md @@ -445,10 +445,12 @@ Below are all available configuration options with their default values: tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember model as sticky prompts when asking questions + remember_as_sticky = true, -- Remember config as sticky prompts when asking questions -- default selection -- see select.lua for implementation diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index b6460d0a..93143754 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -61,9 +61,7 @@ local class = utils.class --- Constants local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' local LINE_CHARACTERS = 100 -local BIG_FILE_THRESHOLD = 1000 * LINE_CHARACTERS local BIG_EMBED_THRESHOLD = 200 * LINE_CHARACTERS -local TRUNCATED = '... (truncated)' --- Resolve provider function ---@param model string @@ -103,16 +101,9 @@ end --- Generate content block with line numbers, truncating if necessary ---@param content string ----@param threshold number: The threshold for truncation ---@param start_line number?: The starting line number ---@return string -local function generate_content_block(content, threshold, start_line) - local total_chars = #content - if total_chars > threshold then - content = content:sub(1, threshold) - content = content .. '\n' .. TRUNCATED - end - +local function generate_content_block(content, start_line) if start_line ~= nil then local lines = vim.split(content, '\n') local total_lines = #lines @@ -144,12 +135,7 @@ local function generate_selection_message(selection) if selection.start_line and selection.end_line then out = out .. string.format('Excerpt from %s, lines %s to %s:\n', filename, selection.start_line, selection.end_line) end - out = out - .. string.format( - '```%s\n%s\n```', - filetype, - generate_content_block(content, BIG_FILE_THRESHOLD, selection.start_line) - ) + out = out .. string.format('```%s\n%s\n```', filetype, generate_content_block(content, selection.start_line)) return { content = out, @@ -167,7 +153,7 @@ local function generate_resource_messages(resources) return resource.data and resource.data ~= '' end) :map(function(resource) - local content = generate_content_block(resource.data, BIG_FILE_THRESHOLD, 1) + local content = generate_content_block(resource.data, 1) return { content = string.format(RESOURCE_FORMAT, resource.name, resource.type, content), diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 5e900219..c456a5b1 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -17,6 +17,7 @@ ---@field model string? ---@field tools string|table|nil ---@field sticky string|table|nil +---@field resource_processing boolean? ---@field temperature number? ---@field headless boolean? ---@field callback nil|fun(response: string, source: CopilotChat.source) @@ -57,10 +58,12 @@ return { tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember model as sticky prompts when asking questions + remember_as_sticky = true, -- Remember config as sticky prompts when asking questions -- default selection selection = require('CopilotChat.select').visual, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c4da582e..426ccad3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -903,11 +903,15 @@ function M.ask(prompt, config) local ok, err = pcall(async.run, function() local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) - local query_ok, processed_resources = pcall(resources.process_resources, prompt, selected_model, resolved_resources) - if query_ok then - resolved_resources = processed_resources - else - log.warn('Failed to process resources', processed_resources) + + if config.resource_processing then + local query_ok, processed_resources = + pcall(resources.process_resources, prompt, selected_model, resolved_resources) + if query_ok then + resolved_resources = processed_resources + else + log.warn('Failed to process resources', processed_resources) + end end prompt = vim.trim(prompt) From 885801c69f812dbe89c719760f0bca2e51ab060c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 15:10:11 +0000 Subject: [PATCH 402/728] 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 d2567481..5e852bdf 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -516,10 +516,12 @@ Below are all available configuration options with their default values: tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember model as sticky prompts when asking questions + remember_as_sticky = true, -- Remember config as sticky prompts when asking questions -- default selection -- see select.lua for implementation From 9d9b2809e1240f9525752ae145799b88d22cd7af Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 20:58:37 +0200 Subject: [PATCH 403/728] feat(ui): improve chat responsiveness by starting spinner early (#1205) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 15 ++------------- lua/CopilotChat/ui/chat.lua | 35 ++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 426ccad3..4e00344b 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -729,19 +729,6 @@ function M.open(config) utils.return_to_normal_mode() M.chat:open(config) - - local message = M.chat:get_message('user') - if message then - local prompt = insert_sticky(message.content, config) - if prompt then - M.chat:add_message({ - role = 'user', - content = '\n' .. prompt, - }, true) - M.chat:finish() - end - end - M.chat:follow() M.chat:focus() end @@ -869,6 +856,8 @@ function M.ask(prompt, config) M.open(config) end + M.chat:start() + local sticky = {} local in_code_block = false for _, line in ipairs(vim.split(prompt, '\n')) do diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 0b1fdc32..669dfa6c 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -382,6 +382,21 @@ function Chat:follow() vim.api.nvim_win_set_cursor(self.winnr, { last_line + 1, last_column }) end +--- Prepare the chat window for writing. +function Chat:start() + self:validate() + + if self:focused() then + utils.return_to_normal_mode() + end + + if self.spinner then + self.spinner:start() + end + + vim.bo[self.bufnr].modifiable = false +end + --- Finish writing to the chat window. function Chat:finish() if not self.spinner then @@ -414,9 +429,7 @@ function Chat:add_message(message, replace) self:append(message.content) elseif replace and current_message then -- Replace the content of the current message - self:append('') self:render() - current_message.content = message.content local section = current_message.section @@ -430,9 +443,8 @@ function Chat:add_message(message, replace) vim.split(message.content, '\n') ) vim.bo[self.bufnr].modifiable = false + self:append('') end - - self:append('') else -- Append to the current message current_message.content = current_message.content .. message.content @@ -476,15 +488,6 @@ end ---@param str string function Chat:append(str) self:validate() - vim.bo[self.bufnr].modifiable = true - - if self:focused() then - utils.return_to_normal_mode() - end - - if self.spinner then - self.spinner:start() - end -- Decide if we should follow cursor after appending text. local should_follow_cursor = self.config.auto_follow_cursor @@ -496,13 +499,14 @@ function Chat:append(str) end local last_line, last_column, _ = self:last() + + vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_text(self.bufnr, last_line, last_column, last_line, last_column, vim.split(str, '\n')) + vim.bo[self.bufnr].modifiable = false if should_follow_cursor then self:follow() end - - vim.bo[self.bufnr].modifiable = false end --- Clear the chat window. @@ -548,6 +552,7 @@ end --- Render the chat window. ---@protected function Chat:render() + self:validate() 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) From dab50896c7e1e80142dd297e6fc75590735b3e9c Mon Sep 17 00:00:00 2001 From: Danilo Horta Date: Mon, 28 Jul 2025 21:58:46 +0100 Subject: [PATCH 404/728] fix: update sticky reference for commit messages (#1207) Changes the commit prompt's sticky reference from '#git:staged' to '#gitdiff:staged' to properly match the expected format for retrieving staged changes when generating commit messages. --- lua/CopilotChat/config/prompts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 9fca7d8e..2b56bdda 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -204,6 +204,6 @@ return { Commit = { prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - sticky = '#git:staged', + sticky = '#gitdiff:staged', }, } From cbb846f9c3979bc8fe5dc7589cdab542adcd224b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:00:00 +0200 Subject: [PATCH 405/728] docs: add danilohorta as a contributor for code (#1208) * 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 d335de60..3e77a246 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -424,6 +424,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/42291930?v=4", "profile": "https://github.com/AtifChy", "contributions": ["code", "doc"] + }, + { + "login": "danilohorta", + "name": "Danilo Horta", + "avatar_url": "https://avatars.githubusercontent.com/u/214497460?v=4", + "profile": "https://github.com/danilohorta", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 931ee8ab..c093a813 100644 --- a/README.md +++ b/README.md @@ -777,6 +777,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Fredrik Averpil
Fredrik Averpil

💻 Aaron D Borden
Aaron D Borden

💻 Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

💻 📖 + Danilo Horta
Danilo Horta

💻 From e632470171cd82a95c2675360120833c159e7ae0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 00:48:01 +0200 Subject: [PATCH 406/728] fix(functions): if enum returns only 1 choice auto accept it (#1209) Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 6cc287bb..c23957e5 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -166,9 +166,14 @@ function M.enter_input(schema, source) if not schema.required or vim.tbl_contains(schema.required, prop_name) then if cfg.enum then local choices = type(cfg.enum) == 'table' and cfg.enum or cfg.enum(source) - local choice = utils.select(choices, { - prompt = string.format('Select %s> ', prop_name), - }) + local choice + if #choices == 1 then + choice = choices[1] + else + choice = utils.select(choices, { + prompt = string.format('Select %s> ', prop_name), + }) + end table.insert(out, choice or '') elseif cfg.type == 'boolean' then From 4c80c0bbb7d3018908ee99165f3a43cc582aeec4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 22:48:23 +0000 Subject: [PATCH 407/728] 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 5e852bdf..aaaa5e72 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -781,7 +781,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 1d6911fef13952c9b56347485f090baeff77a7e4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 09:53:21 +0200 Subject: [PATCH 408/728] fix: add back sticky loading on opening window (#1210) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 4e00344b..6e2de658 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -729,6 +729,19 @@ function M.open(config) utils.return_to_normal_mode() M.chat:open(config) + + local message = M.chat:get_message('user') + if message then + local prompt = insert_sticky(message.content, config) + if prompt then + M.chat:add_message({ + role = 'user', + content = '\n' .. prompt, + }, true) + M.chat:finish() + end + end + M.chat:follow() M.chat:focus() end From bfd1e22216146546ca3b9c2eb8f7afd220998601 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Jul 2025 07:53:38 +0000 Subject: [PATCH 409/728] 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 aaaa5e72..799f0f62 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 8a5cda1d90c4d4756dda39cfd748e52cbcde5a99 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 11:27:37 +0200 Subject: [PATCH 410/728] fix(functions): if schema.properties is empty, do not send schema (#1211) This breaks stuff especially with github mcp server Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index c23957e5..c9d8488b 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -48,10 +48,16 @@ local function filter_schema(tbl) return tbl end + if utils.empty(tbl.properties) then + return nil + end + local result = {} for k, v in pairs(tbl) do - if type(v) ~= 'function' and k ~= 'examples' then - result[k] = type(v) == 'table' and filter_schema(v) or v + if not utils.empty(v) then + if type(v) ~= 'function' and k ~= 'examples' then + result[k] = type(v) == 'table' and filter_schema(v) or v + end end end return result From d905917a025e4c056db28b3082dd474475bad8cd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 12:35:07 +0200 Subject: [PATCH 411/728] fix(functions): properly escape percent signs in uri inputs (#1212) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 6e2de658..9900e62f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -366,9 +366,12 @@ function M.resolve_functions(prompt, config) -- Resolve and process all tools for _, pattern in ipairs(matches:keys()) do - local match = matches:get(pattern) - local out = expand_tool(match.word, match.input) or pattern - prompt = prompt:gsub(vim.pesc(pattern), out, 1) + if not utils.empty(pattern) then + local match = matches:get(pattern) + local out = expand_tool(match.word, match.input) or pattern + out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub + prompt = prompt:gsub(vim.pesc(pattern), out, 1) + end end return functions.parse_tools(enabled_tools), resolved_resources, resolved_tools, prompt From c87f6af0a15cfdaf09826d6fdf5f15ef6a0f782f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 17:21:39 +0200 Subject: [PATCH 412/728] chore: unify client:models function and improve variable naming (#1213) Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 40 +++++++++----------------------------- lua/CopilotChat/init.lua | 32 ++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 93143754..b0a48b85 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -216,15 +216,13 @@ end ---@class CopilotChat.client.Client : Class ---@field private providers table ---@field private provider_cache table ----@field private models table? +---@field private model_cache table? ---@field private current_job string? ----@field private headers table? local Client = class(function(self) self.providers = {} self.provider_cache = {} - self.models = nil + self.model_cache = nil self.current_job = nil - self.headers = nil end) --- Authenticate with GitHub and get the required headers @@ -246,9 +244,9 @@ end --- Fetch models from the Copilot API ---@return table -function Client:fetch_models() - if self.models then - return self.models +function Client:models() + if self.model_cache then + return self.model_cache end local models = {} @@ -282,8 +280,8 @@ function Client:fetch_models() end log.debug('Fetched models:', #vim.tbl_keys(models)) - self.models = models - return self.models + self.model_cache = models + return self.model_cache end --- Ask a question to Copilot @@ -299,7 +297,7 @@ function Client:ask(prompt, opts) log.debug('Resources:', #opts.resources) log.debug('History:', #opts.history) - local models = self:fetch_models() + local models = self:models() local model_config = models[opts.model] if not model_config then error('Model not found: ' .. opts.model) @@ -573,26 +571,6 @@ function Client:ask(prompt, opts) } end ---- List available models ----@return table -function Client:list_models() - local models = self:fetch_models() - local result = vim.tbl_keys(models) - - table.sort(result, function(a, b) - a = models[a] - b = models[b] - if a.provider ~= b.provider then - return a.provider < b.provider - end - return a.id < b.id - end) - - return vim.tbl_map(function(id) - return models[id] - end, result) -end - --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ---@param model string @@ -603,7 +581,7 @@ function Client:embed(inputs, model) return inputs end - local models = self:fetch_models() + local models = self:models() local ok, provider_name, embed = pcall(resolve_provider_function, 'embed', model, models, self.providers) if not ok then ---@diagnostic disable-next-line: return-type-mismatch diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9900e62f..9ef35e85 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -120,6 +120,26 @@ local function update_highlights() end end +--- List available models. +--- @return CopilotChat.client.Model[] +local function list_models() + local models = client:models() + local result = vim.tbl_keys(models) + + table.sort(result, function(a, b) + a = models[a] + b = models[b] + if a.provider ~= b.provider then + return a.provider < b.provider + end + return a.id < b.id + end) + + return vim.tbl_map(function(id) + return models[id] + end, result) +end + --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) @@ -284,8 +304,8 @@ function M.resolve_functions(prompt, config) }) end - -- Resolve each tool reference - local function expand_tool(name, input) + -- Resolve each function reference + local function expand_function(name, input) notify.publish(notify.STATUS, 'Running function: ' .. name) local tool_id = nil @@ -368,7 +388,7 @@ function M.resolve_functions(prompt, config) for _, pattern in ipairs(matches:keys()) do if not utils.empty(pattern) then local match = matches:get(pattern) - local out = expand_tool(match.word, match.input) or pattern + local out = expand_function(match.word, match.input) or pattern out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub prompt = prompt:gsub(vim.pesc(pattern), out, 1) end @@ -440,7 +460,7 @@ function M.resolve_model(prompt, config) local models = vim.tbl_map(function(model) return model.id - end, client:list_models()) + end, list_models()) local selected_model = config.model or '' prompt = prompt:gsub('%$' .. WORD, function(match) @@ -600,7 +620,7 @@ end ---@return table ---@async function M.complete_items() - local models = client:list_models() + local models = list_models() local prompts_to_use = M.prompts() local items = {} @@ -767,7 +787,7 @@ end --- Select default Copilot GPT model. function M.select_model() async.run(function() - local models = client:list_models() + local models = list_models() local choices = vim.tbl_map(function(model) return { id = model.id, From b738fb40de3a4bcbb835b8ff6ab2d171acc5d2dd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 18:05:29 +0200 Subject: [PATCH 413/728] fix: check for explicit uri input properly (#1214) Previous check for empty schema broke this, the check should be done only after input type is checked Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index c9d8488b..dbcf5bcd 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -129,14 +129,14 @@ end ---@param schema table? ---@return table function M.parse_input(input, schema) - if not schema or not schema.properties then - return {} - end - if type(input) == 'table' then return input end + if not schema or not schema.properties then + return {} + end + local parts = vim.split(input or '', INPUT_SEPARATOR) local result = {} local prop_names = sorted_propnames(schema) From 450fcecf2f71d0469e9c98f5967252092714ed03 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 21:04:45 +0200 Subject: [PATCH 414/728] feat: display group as kind when listing resources (#1215) 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 9ef35e85..72155fbd 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -702,7 +702,7 @@ function M.complete_items() items[#items + 1] = { word = '#' .. tool.name, abbr = tool.name, - kind = 'resource', + kind = M.config.functions[tool.name].group or 'resource', info = info, menu = uri, icase = 1, From 9c4501e7ae92020f2d9b828086016ee70e7fa52c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 10:27:02 +0200 Subject: [PATCH 415/728] feat(providers)!: new github models api, in-built authorization without copilot.vim dep (#1218) - Switch to new github models api. This brings device code as prerequisite so add support for retrieving device code as well - With device code flow, add support for it for regular copilot auth as well - Disable github_models provider by default as now it requires device code flow BREAKING CHANGE: github_models provider is now disabled by default, enable with `providers.github_models.disabled = false` Closes #1140 Signed-off-by: Tomas Slusny --- README.md | 11 +- lua/CopilotChat/config/providers.lua | 212 ++++++++++++++++++--------- lua/CopilotChat/init.lua | 1 + lua/CopilotChat/notify.lua | 1 + lua/CopilotChat/ui/chat.lua | 6 + lua/CopilotChat/utils.lua | 23 +++ 6 files changed, 178 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index c093a813..5daa432f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities - [Neovim 0.10.0+](https://neovim.io/) - Older versions are not officially supported - [curl](https://curl.se/) - Version 8.0.0+ recommended for best compatibility - [Copilot chat in the IDE](https://github.com/settings/copilot) enabled in GitHub settings +- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) - Plugin dependency > [!WARNING] > For Neovim < 0.11.0, add `noinsert` or `noselect` to your `completeopt` otherwise chat autocompletion will not work. @@ -39,6 +40,8 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities ## Optional Dependencies +- [copilot.vim](https://github.com/github/copilot.vim) - For `:Copilot setup` authorization, otherwise in-built method i used + - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - For accurate token counting - Arch Linux: 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 - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` @@ -72,7 +75,6 @@ return { { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, build = "make tiktoken", @@ -92,7 +94,6 @@ Similar to the lazy setup, you can use the following configuration: ```vim call plug#begin() -Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' Plug 'CopilotC-Nvim/CopilotChat.nvim' call plug#end() @@ -112,9 +113,7 @@ EOF mkdir -p ~/.config/nvim/pack/copilotchat/start 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 https://github.com/CopilotC-Nvim/CopilotChat.nvim ``` @@ -392,8 +391,8 @@ Providers are modules that implement integration with different AI providers. ### Built-in Providers - `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone +- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) +- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers ### Provider Interface diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 65619117..0e406bd6 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,70 +1,160 @@ +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') +local plenary_utils = require('plenary.async.util') local EDITOR_VERSION = 'Neovim/' .. vim.version().major .. '.' .. vim.version().minor .. '.' .. vim.version().patch -local cached_github_token = nil +local token_cache = nil +local unsaved_token_cache = {} +local function load_tokens() + if token_cache then + return token_cache + end + + local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') + local cache_file = config_path .. '/tokens.json' + local file = utils.read_file(cache_file) + if file then + token_cache = vim.json.decode(file) + else + token_cache = {} + end + + return token_cache +end + +local function get_token(tag) + if unsaved_token_cache[tag] then + return unsaved_token_cache[tag] + end + + local tokens = load_tokens() + return tokens[tag] +end -local function config_path() - local config = vim.fs.normalize('$XDG_CONFIG_HOME') - if config and vim.uv.fs_stat(config) then - return config +local function set_token(tag, token, save) + if not save then + unsaved_token_cache[tag] = token + return token end - if vim.fn.has('win32') > 0 then - config = vim.fs.normalize('$LOCALAPPDATA') - if not config or not vim.uv.fs_stat(config) then - config = vim.fs.normalize('$HOME/AppData/Local') + + local tokens = load_tokens() + tokens[tag] = token + local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') + utils.write_file(config_path .. '/tokens.json', vim.json.encode(tokens)) + return token +end + +--- Get the github token using device flow +---@return string +local function github_device_flow(tag, client_id, scope) + local function request_device_code() + local res = utils.curl_post('https://github.com/login/device/code', { + body = { + client_id = client_id, + scope = scope, + }, + headers = { ['Accept'] = 'application/json' }, + }) + + local data = vim.json.decode(res.body) + return data + end + + local function poll_for_token(device_code, interval) + while true do + plenary_utils.sleep(interval * 1000) + + local res = utils.curl_post('https://github.com/login/oauth/access_token', { + body = { + client_id = client_id, + device_code = device_code, + grant_type = 'urn:ietf:params:oauth:grant-type:device_code', + }, + headers = { ['Accept'] = 'application/json' }, + }) + local data = vim.json.decode(res.body) + if data.access_token then + return data.access_token + elseif data.error ~= 'authorization_pending' then + error('Auth error: ' .. (data.error or 'unknown')) + end end - else - config = vim.fs.normalize('$HOME/.config') end - if config and vim.uv.fs_stat(config) then - return config + + local token = get_token(tag) + if token then + return token end + + local code_data = request_device_code() + notify.publish( + notify.MESSAGE, + '[' .. tag .. '] Visit ' .. code_data.verification_uri .. ' and enter code: ' .. code_data.user_code + ) + notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for GitHub models authorization...') + token = poll_for_token(code_data.device_code, code_data.interval) + return set_token(tag, token, true) end --- Get the github copilot oauth cached token (gu_ token) ---@return string -local function get_github_token() - if cached_github_token then - return cached_github_token +local function get_github_token(tag) + local function config_path() + local config = vim.fs.normalize('$XDG_CONFIG_HOME') + if config and vim.uv.fs_stat(config) then + return config + end + if vim.fn.has('win32') > 0 then + config = vim.fs.normalize('$LOCALAPPDATA') + if not config or not vim.uv.fs_stat(config) then + config = vim.fs.normalize('$HOME/AppData/Local') + end + else + config = vim.fs.normalize('$HOME/.config') + end + if config and vim.uv.fs_stat(config) then + return config + end + end + + local token = get_token(tag) + if token then + return token end -- loading token from the environment only in GitHub Codespaces - local token = os.getenv('GITHUB_TOKEN') local codespaces = os.getenv('CODESPACES') + token = os.getenv('GITHUB_TOKEN') if token and codespaces then - cached_github_token = token - return token + return set_token(tag, token, false) end -- loading token from the file local config_path = config_path() - if not config_path then - error('Failed to find config path for GitHub token') - end + if config_path then + -- token can be sometimes in apps.json sometimes in hosts.json + local file_paths = { + config_path .. '/github-copilot/hosts.json', + config_path .. '/github-copilot/apps.json', + } - -- token can be sometimes in apps.json sometimes in hosts.json - local file_paths = { - config_path .. '/github-copilot/hosts.json', - config_path .. '/github-copilot/apps.json', - } - - for _, file_path in ipairs(file_paths) do - local file_data = utils.read_file(file_path) - if file_data then - local parsed_data = utils.json_decode(file_data) - if parsed_data then - for key, value in pairs(parsed_data) do - if string.find(key, 'github.com') then - cached_github_token = value.oauth_token - return value.oauth_token + for _, file_path in ipairs(file_paths) do + local file_data = utils.read_file(file_path) + if file_data then + local parsed_data = utils.json_decode(file_data) + if parsed_data then + for key, value in pairs(parsed_data) do + if string.find(key, 'github.com') and value and value.oauth_token then + return set_token(tag, value.oauth_token, true) + end end end end end end - error('Failed to find GitHub token') + return github_device_flow(tag, 'Iv1.b507a08c87ecfe98', '') end ---@class CopilotChat.config.providers.Options @@ -97,7 +187,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token(), + ['Authorization'] = 'Token ' .. get_github_token('copilot'), }, }) @@ -284,30 +374,19 @@ M.copilot = { } M.github_models = { + disabled = true, embed = 'copilot_embeddings', get_headers = function() return { - ['Authorization'] = 'Bearer ' .. get_github_token(), - ['x-ms-useragent'] = EDITOR_VERSION, - ['x-ms-user-agent'] = EDITOR_VERSION, + ['Authorization'] = 'Bearer ' .. github_device_flow('github_models', 'Ov23liqtJusaUH38tIoK', 'read:user copilot'), } end, get_models = function(headers) - local response, err = utils.curl_post('https://api.catalog.azureml.ms/asset-gallery/v1.0/models', { - headers = headers, - json_request = true, + local response, err = utils.curl_get('https://models.github.ai/catalog/models', { json_response = true, - body = { - filters = { - { field = 'freePlayground', values = { 'true' }, operator = 'eq' }, - { field = 'labels', values = { 'latest' }, operator = 'eq' }, - }, - order = { - { field = 'displayName', direction = 'asc' }, - }, - }, + headers = headers, }) if err then @@ -315,26 +394,19 @@ M.github_models = { end return vim - .iter(response.body.summaries) - :filter(function(model) - return vim.tbl_contains(model.inferenceTasks, 'chat-completion') - end) + .iter(response.body) :map(function(model) - local context_window = model.modelLimits.textLimits.inputContextWindow - local max_output_tokens = model.modelLimits.textLimits.maxOutputTokens - local max_input_tokens = context_window - max_output_tokens - if max_input_tokens <= 0 then - max_output_tokens = 4096 - max_input_tokens = context_window - max_output_tokens - end - + local max_output_tokens = model.limits.max_output_tokens + local max_input_tokens = model.limits.max_input_tokens return { - id = model.name, - name = model.displayName, + id = model.id, + name = model.name, tokenizer = 'o200k_base', max_input_tokens = max_input_tokens, max_output_tokens = max_output_tokens, - streaming = true, + streaming = vim.tbl_contains(model.capabilities, 'streaming'), + tools = vim.tbl_contains(model.capabilities, 'tool-calling'), + version = model.version, } end) :totable() @@ -344,7 +416,7 @@ M.github_models = { prepare_output = M.copilot.prepare_output, get_url = function() - return 'https://models.inference.ai.azure.com/chat/completions' + return 'https://models.github.ai/inference/chat/completions' end, } diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 72155fbd..71e80f3f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -893,6 +893,7 @@ function M.ask(prompt, config) end M.chat:start() + M.chat:append('\n') local sticky = {} local in_code_block = false diff --git a/lua/CopilotChat/notify.lua b/lua/CopilotChat/notify.lua index db1af837..99aa499a 100644 --- a/lua/CopilotChat/notify.lua +++ b/lua/CopilotChat/notify.lua @@ -3,6 +3,7 @@ local log = require('plenary.log') local M = {} M.STATUS = 'status' +M.MESSAGE = 'message' M.listeners = {} diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 669dfa6c..bcd513fd 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -1,5 +1,6 @@ local Overlay = require('CopilotChat.ui.overlay') local Spinner = require('CopilotChat.ui.spinner') +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = utils.class @@ -95,6 +96,11 @@ local Chat = class(function(self, headers, separator, help, on_buf_create) end, }) end) + + notify.listen(notify.MESSAGE, function(msg) + utils.schedule_main() + self:append('\n' .. msg .. '\n') + end) end, Overlay) --- Returns whether the chat window is visible. diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 1cf1a151..b1400837 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -615,6 +615,29 @@ function M.read_file(path) return data end +--- Write data to a file +---@param path string The file path +---@param data string The data to write +---@return boolean +function M.write_file(path, data) + M.schedule_main() + vim.fn.mkdir(vim.fn.fnamemodify(path, ':p:h'), 'p') + + local err, fd = async.uv.fs_open(path, 'w', 438) + if err or not fd then + return false + end + + local err = async.uv.fs_write(fd, data, 0) + if err then + async.uv.fs_close(fd) + return false + end + + async.uv.fs_close(fd) + return true +end + --- Call a system command ---@param cmd table The command ---@async From f45a7ee8da7ef15fd88f8a83ea7b76b50c8d6eb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 08:27:25 +0000 Subject: [PATCH 416/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 799f0f62..979b45fc 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 30 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -52,6 +52,7 @@ capabilities directly into your editor. It provides: - Neovim 0.10.0+ - Older versions are not officially supported - curl - Version 8.0.0+ recommended for best compatibility - Copilot chat in the IDE enabled in GitHub settings +- plenary.nvim - Plugin dependency [!WARNING] For Neovim < 0.11.0, add `noinsert` or `noselect` to your @@ -61,12 +62,16 @@ capabilities directly into your editor. It provides: OPTIONAL DEPENDENCIES *CopilotChat-optional-dependencies* -- tiktoken_core - For accurate token counting +- copilot.vim - For `:Copilot setup` + authorization, otherwise in-built method i used +- tiktoken_core - For accurate token + counting - Arch Linux: Install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from lua-tiktoken releases and save as `tiktoken_core.so` in your Lua path - git - For git diff context features -- ripgrep - For improved search performance +- ripgrep - For improved search + performance - lynx - For improved URL context features @@ -99,7 +104,6 @@ LAZY.NVIM *CopilotChat-lazy.nvim* { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, build = "make tiktoken", @@ -121,7 +125,6 @@ Similar to the lazy setup, you can use the following configuration: >vim call plug#begin() - Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' Plug 'CopilotC-Nvim/CopilotChat.nvim' call plug#end() @@ -142,9 +145,7 @@ MANUAL *CopilotChat-manual* mkdir -p ~/.config/nvim/pack/copilotchat/start 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 https://github.com/CopilotC-Nvim/CopilotChat.nvim < @@ -456,8 +457,8 @@ Providers are modules that implement integration with different AI providers. BUILT-IN PROVIDERS ~ - `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone +- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) +- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers PROVIDER INTERFACE ~ From d9f4e29c3b46b827443b1832209d22d05c1a69af Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 10:34:23 +0200 Subject: [PATCH 417/728] fix(healthcheck): chance copilot.vim dependency to optional (#1219) Signed-off-by: Tomas Slusny --- lua/CopilotChat/health.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index cf1e568a..a75416ee 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -98,8 +98,8 @@ function M.check() if has_copilot or copilot_loaded then ok('copilot: ' .. (has_copilot and 'copilot.lua' or 'copilot.vim')) else - error( - 'copilot: missing, required for 2 factor authentication. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' + warn( + 'copilot: missing, optional for improved auth implementation. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' ) end From 950fdb6ab56754929d4db91c73139b33e645deec Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 11:16:16 +0200 Subject: [PATCH 418/728] feat(functions): automatically parse schema from url templates (#1220) Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 52 ++++++++++++++++++++++++++++++----- lua/CopilotChat/init.lua | 34 ++++++++++++++--------- lua/CopilotChat/tiktoken.lua | 10 +++++-- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index dbcf5bcd..04c5b103 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -3,6 +3,7 @@ local utils = require('CopilotChat.utils') local M = {} local INPUT_SEPARATOR = ';;' +local URI_PARAM_PATTERN = '{([^}:*]+)[^}]*}' local function sorted_propnames(schema) local prop_names = vim.tbl_keys(schema.properties) @@ -63,6 +64,17 @@ local function filter_schema(tbl) return result end +--- Convert a URI template to a URL by replacing parameters with values from input +---@param uri_template string The URI template containing parameters in the form {param} +---@param input table A table containing parameter values, e.g., { path = '/my/file.txt' } +---@return string The resulting URL with parameters replaced +function M.uri_to_url(uri_template, input) + -- Replace {param} in the template with input[param] or empty string + return (uri_template:gsub(URI_PARAM_PATTERN, function(param) + return input[param] or '' + end)) +end + ---@param uri string The URI to parse ---@param pattern string The pattern to match against (e.g., 'file://{path}') ---@return table|nil inputs Extracted parameters or nil if no match @@ -73,7 +85,7 @@ function M.match_uri(uri, pattern) -- Extract parameter names from the pattern local param_names = {} - for param in pattern:gmatch('{([^}:*]+)[^}]*}') do + for param in pattern:gmatch(URI_PARAM_PATTERN) do table.insert(param_names, param) -- Replace {param} with a capture group in our Lua pattern -- Use non-greedy capture to handle multiple params properly @@ -102,6 +114,37 @@ function M.match_uri(uri, pattern) return result end +---@param tool CopilotChat.config.functions.Function +function M.parse_schema(tool) + local schema = tool.schema + + -- If schema is missing but uri is present, generate a default schema from uri + if not schema and tool.uri then + -- Extract parameter names from the uri pattern, e.g. file://{path} + local param_names = {} + for param in tool.uri:gmatch(URI_PARAM_PATTERN) do + table.insert(param_names, param) + end + if #param_names > 0 then + schema = { + type = 'object', + properties = {}, + required = {}, + } + for _, param in ipairs(param_names) do + schema.properties[param] = { type = 'string' } + table.insert(schema.required, param) + end + end + end + + if schema then + schema = filter_schema(schema) + end + + return schema +end + --- Prepare the schema for use ---@param tools table ---@return table @@ -110,16 +153,11 @@ function M.parse_tools(tools) table.sort(tool_names) return vim.tbl_map(function(name) local tool = tools[name] - local schema = tool.schema - - if schema then - schema = filter_schema(schema) - end return { name = name, description = tool.description, - schema = schema, + schema = M.parse_schema(tool), } end, tool_names) end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 71e80f3f..91aaf143 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -245,6 +245,12 @@ end ---@async function M.resolve_functions(prompt, config) config, prompt = M.resolve_prompt(prompt, config) + + local tools = {} + for _, tool in ipairs(functions.parse_tools(M.config.functions)) do + tools[tool.name] = tool + end + local enabled_tools = {} local resolved_resources = {} local resolved_tools = {} @@ -271,7 +277,7 @@ function M.resolve_functions(prompt, config) for _, match in ipairs(matches) do for name, tool in pairs(M.config.functions) do if name == match or tool.group == match then - enabled_tools[name] = tool + enabled_tools[name] = true end end end @@ -311,7 +317,7 @@ function M.resolve_functions(prompt, config) local tool_id = nil if not utils.empty(tool_calls) then for _, tool_call in ipairs(tool_calls) do - if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) and enabled_tools[name] then + if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) then input = utils.empty(tool_call.arguments) and {} or utils.json_decode(tool_call.arguments) tool_id = tool_call.id break @@ -319,7 +325,7 @@ function M.resolve_functions(prompt, config) end end - local tool = enabled_tools[name] + local tool = M.config.functions[name] if not tool then -- Check if input matches uri for tool_name, tool_spec in pairs(M.config.functions) do @@ -334,20 +340,16 @@ function M.resolve_functions(prompt, config) end end end - if not tool and not tool_id then - tool = M.config.functions[name] - end if not tool then - -- If tool is not found, return the original pattern return nil end - if not tool_id and not tool.uri then - -- If this is a tool that is not resource and was not called by LLM, reject it + if tool_id and not enabled_tools[name] and not tool.uri then return nil end + local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, tool.schema), state.source or {}, prompt) + local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, input) if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else @@ -394,7 +396,12 @@ function M.resolve_functions(prompt, config) end end - return functions.parse_tools(enabled_tools), resolved_resources, resolved_tools, prompt + return vim.tbl_map(function(name) + return tools[name] + end, vim.tbl_keys(enabled_tools)), + resolved_resources, + resolved_tools, + prompt end --- Resolve the final prompt and config from prompt template. @@ -574,9 +581,10 @@ function M.trigger_complete(without_input) if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then local found_tool = M.config.functions[prefix:sub(2, -2)] - if found_tool and found_tool.schema then + local found_schema = found_tool and functions.parse_schema(found_tool) + if found_tool and found_schema then async.run(function() - local value = functions.enter_input(found_tool.schema, state.source) + local value = functions.enter_input(found_schema, state.source) if not value then return end diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index a4582cb4..dde3d2b5 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -92,7 +92,13 @@ function M.encode(prompt) if type(prompt) ~= 'string' then error('Prompt must be a string') end - return tiktoken_core.encode(prompt) + + local ok, result = pcall(tiktoken_core.encode, prompt) + if not ok then + return nil + end + + return result end --- Count the tokens in a prompt @@ -105,7 +111,7 @@ function M.count(prompt) local tokens = M.encode(prompt) if not tokens then - return 0 + return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count end return #tokens end From c03bd1df78b276aa5be2f173c2a31ad273164f15 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 11:20:25 +0200 Subject: [PATCH 419/728] fix(functions): properly send prompt as 3rd function resolve param (#1221) 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 91aaf143..39e40959 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -349,7 +349,7 @@ function M.resolve_functions(prompt, config) local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, input) + local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, prompt) if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else From 1f96d53c3f10f176ca25065a23e610d7b4a72b99 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 12:02:37 +0200 Subject: [PATCH 420/728] fix(ui): fix check for auto follow cursor (#1222) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 2 -- lua/CopilotChat/ui/chat.lua | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 39e40959..5baa0fa8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -970,8 +970,6 @@ function M.ask(prompt, config) content = '\n' .. tool.result .. '\n', }) end - - M.chat:follow() end local ask_ok, ask_response = pcall(client.ask, client, prompt, { diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index bcd513fd..55964682 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -501,7 +501,7 @@ function Chat:append(str) 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. - should_follow_cursor = current_pos[1] == line_count + should_follow_cursor = current_pos[1] >= line_count - 1 end local last_line, last_column, _ = self:last() From 294bcb620ff66183e142cd8a43a7c77d5bc77a16 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 13:22:38 +0200 Subject: [PATCH 421/728] fix(providers): do not save copilot.vim token (#1223) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/providers.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 0e406bd6..eee655a8 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -92,7 +92,7 @@ local function github_device_flow(tag, client_id, scope) notify.MESSAGE, '[' .. tag .. '] Visit ' .. code_data.verification_uri .. ' and enter code: ' .. code_data.user_code ) - notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for GitHub models authorization...') + notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for authorization...') token = poll_for_token(code_data.device_code, code_data.interval) return set_token(tag, token, true) end @@ -146,7 +146,7 @@ local function get_github_token(tag) if parsed_data then for key, value in pairs(parsed_data) do if string.find(key, 'github.com') and value and value.oauth_token then - return set_token(tag, value.oauth_token, true) + return set_token(tag, value.oauth_token, false) end end end @@ -187,7 +187,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token('copilot'), + ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), }, }) From 67ed258c6ccc0a9bfbb6dfcbe3d5e19e22888e73 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 17:24:51 +0200 Subject: [PATCH 422/728] fix(ui): do not allow empty separator (#1224) This even working before was a miracle, the plugin is fairly reliant on separator not being empty string Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/init.lua | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5daa432f..e10f3d61 100644 --- a/README.md +++ b/README.md @@ -473,7 +473,7 @@ Below are all available configuration options with their default values: 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) + highlight_headers = true, -- Highlight headers in chat 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 diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index c456a5b1..f2f2c708 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -86,7 +86,7 @@ return { 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) + highlight_headers = true, -- Highlight headers in chat 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 diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5baa0fa8..fa2e4308 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1162,7 +1162,8 @@ end --- Set up the plugin ---@param config CopilotChat.config.Config? function M.setup(config) - M.config = vim.tbl_deep_extend('force', require('CopilotChat.config'), config or {}) + local default_config = require('CopilotChat.config') + M.config = vim.tbl_deep_extend('force', default_config, config or {}) state.highlights_loaded = false -- Save proxy and insecure settings @@ -1181,6 +1182,13 @@ function M.setup(config) M.log_level(M.config.log_level) end + if not M.config.separator or M.config.separator == '' then + log.warn( + 'Empty separator is not allowed, using default separator instead. Set `separator` in config to change this.' + ) + M.config.separator = default_config.separator + end + if M.chat then M.chat:close(state.source and state.source.bufnr or nil) M.chat:delete() From 1b5cb07d0ef679d14f0896b724de47c640c69a6d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 15:25:16 +0000 Subject: [PATCH 423/728] 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 979b45fc..5bbef619 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -546,7 +546,7 @@ Below are all available configuration options with their default values: 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) + highlight_headers = true, -- Highlight headers in chat 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 From 8071a6979b5569ce03f7f4d7192814da4c2d4e0b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 19:19:44 +0200 Subject: [PATCH 424/728] feat(ui): highlight copilotchat keywords (#1225) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 55964682..0f7663a5 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -66,7 +66,6 @@ end ---@field private layout CopilotChat.config.Layout? ---@field private headers table ---@field private separator string ----@field private header_ns number ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay local Chat = class(function(self, headers, separator, help, on_buf_create) @@ -81,7 +80,6 @@ local Chat = class(function(self, headers, separator, help, on_buf_create) self.layout = nil self.headers = headers or {} self.separator = separator - self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') self.spinner = Spinner() self.chat_overlay = Overlay('copilot-overlay', 'q to close', function(bufnr) @@ -559,7 +557,10 @@ end ---@protected function Chat:render() self:validate() - vim.api.nvim_buf_clear_namespace(self.bufnr, self.header_ns, 0, -1) + + local highlight_ns = vim.api.nvim_create_namespace('copilot-chat-headers') + vim.api.nvim_buf_clear_namespace(self.bufnr, highlight_ns, 0, -1) + local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) local new_messages = {} @@ -578,7 +579,7 @@ function Chat:render() -- Draw the separator as virtual text over the header line, hiding the id and anything after the header if self.config.highlight_headers then local sep_col = vim.fn.strwidth(header_value) - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep_col, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, sep_col, { virt_text = { { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, }, @@ -586,7 +587,7 @@ function Chat:render() priority = 200, strict = false, }) - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { end_col = sep_col, hl_group = 'CopilotChatHeader', priority = 100, @@ -647,7 +648,7 @@ function Chat:render() if start_line and end_line then text = text .. string.format(' lines %d-%d', start_line, end_line) end - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l, 0, { virt_lines_above = true, virt_lines = { { { text, 'CopilotChatAnnotationHeader' } } }, priority = 100, @@ -674,9 +675,9 @@ function Chat:render() for _, message in ipairs(self.messages) do for _, tool_call in ipairs(message.tool_calls or {}) do if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then - vim.api.nvim_buf_add_highlight(self.bufnr, self.header_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) if not utils.empty(tool_call.arguments) then - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { virt_lines = vim.tbl_map(function(json_line) return { { json_line, 'CopilotChatAnnotation' } } end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), @@ -688,6 +689,20 @@ function Chat:render() end end end + + -- Highlight keywords + -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it + local patterns = { + '()#?#[^ ]+()', + '()@[^ ]+()', + '()%$[^ ]+()', + '()/[^ ]+()', + } + for _, pattern in ipairs(patterns) do + for s, e in line:gmatch(pattern) do + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) + end + end end -- Replace self.messages with new_messages (preserving tool_calls, etc.) @@ -705,7 +720,7 @@ function Chat:render() table.insert(virt_lines, { { ' ' .. json_line, 'CopilotChatAnnotation' } }) end end - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.end_line - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, section.end_line - 1, 0, { virt_lines = virt_lines, virt_lines_above = true, priority = 100, @@ -720,7 +735,7 @@ function Chat:render() local virt_lines = { { { 'Tool: ' .. message.tool_call_id, 'CopilotChatAnnotationHeader' } }, } - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.start_line, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, section.start_line, 0, { virt_lines = virt_lines, virt_lines_above = true, priority = 100, From b124b94264140a5d352512b38b7a46d85ee59b24 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 22:54:27 +0200 Subject: [PATCH 425/728] fix(functions): use vim.filetype.match for non bulk file reads (#1226) To improve accuracy, use vim.filetype.match instead of plenary filetype when not filtering lists of files. Closes #1181 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 1 + lua/CopilotChat/utils.lua | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 1644a03f..65363811 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -37,6 +37,7 @@ return { }, resolve = function(input) + utils.schedule_main() local data, mimetype = resources.get_file(input.path) if not data then error('File not found: ' .. input.path) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index b1400837..ae3e6313 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -214,15 +214,7 @@ end ---@param filename string The file name ---@return string|nil function M.filetype(filename) - local filetype = require('plenary.filetype') - local ft = filetype.detect(filename, { - fs_access = false, - }) - - if ft == '' then - return nil - end - return ft + return vim.filetype.match({ filename = filename }) end --- Get the mimetype from filetype @@ -445,8 +437,22 @@ M.curl_post = async.wrap(function(url, opts, callback) end, 3) local function filter_files(files, max_count) + local filetype = require('plenary.filetype') + files = vim.tbl_filter(function(file) - return file ~= '' and M.filetype(file) ~= nil + if file == nil or file == '' then + return false + end + + local ft = filetype.detect(file, { + fs_access = false, + }) + + if ft == '' or not ft then + return false + end + + return true end, files) if max_count and max_count > 0 then files = vim.list_slice(files, 1, max_count) From a01bbd6779f4bee23c29ebcfe0d2f5fa5664b5bf Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 23:03:15 +0200 Subject: [PATCH 426/728] feat(ui): add window.blend option for controllin float transparency (#1227) Closes #1126 Signed-off-by: Tomas Slusny --- README.md | 1 + lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/ui/chat.lua | 1 + 3 files changed, 4 insertions(+) diff --git a/README.md b/README.md index e10f3d61..7958837a 100644 --- a/README.md +++ b/README.md @@ -468,6 +468,7 @@ Below are all available configuration options with their default values: 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 + blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent }, show_help = true, -- Shows help message as virtual lines when waiting for user input diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f2f2c708..8809fdc6 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -11,6 +11,7 @@ ---@field title string? ---@field footer string? ---@field zindex number? +---@field blend number? ---@class CopilotChat.config.Shared ---@field system_prompt string? @@ -81,6 +82,7 @@ return { 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 + blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent }, show_help = true, -- Shows help message as virtual lines when waiting for user input diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 0f7663a5..e2cf7503 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -289,6 +289,7 @@ function Chat:open(config) } self.winnr = vim.api.nvim_open_win(self.bufnr, false, win_opts) + vim.wo[self.winnr].winblend = window.blend or 0 elseif layout == 'vertical' then local orig = vim.api.nvim_get_current_win() local cmd = 'vsplit' From 72ac912877a55ea6c61d803dee38704c9e7c255c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 21:03:35 +0000 Subject: [PATCH 427/728] 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 5bbef619..02932d3c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -541,6 +541,7 @@ Below are all available configuration options with their default values: 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 + blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent }, show_help = true, -- Shows help message as virtual lines when waiting for user input From 1713ce6c8ec700a7833236a8dadfae8a0742b14d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 31 Jul 2025 16:11:18 +0200 Subject: [PATCH 428/728] feat(providers): add info output to panel for copilot with stats (#1229) Signed-off-by: Tomas Slusny --- README.md | 3 ++ lua/CopilotChat/client.lua | 31 +++++++++++++++++ lua/CopilotChat/config/mappings.lua | 10 ++++++ lua/CopilotChat/config/providers.lua | 51 ++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/README.md b/README.md index 7958837a..19e790c6 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,9 @@ Custom providers can implement these methods: -- Optional: Embeddings provider name or function embed?: string|function, + -- Optional: Extra info about the provider displayed in info panel + get_info?(): string[] + -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index b0a48b85..11c353b6 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -284,6 +284,37 @@ function Client:models() return self.model_cache end +--- Get information about all providers +---@return table +function Client:info() + local infos = {} + local now = math.floor(os.time()) + local CACHE_TTL = 300 -- 5 minutes + + for provider_name, provider in pairs(self.providers) do + if not provider.disabled and provider.get_info then + local cache = self.provider_cache[provider_name] + if cache and cache.info and cache.info_expires_at and cache.info_expires_at > now then + infos[provider_name] = cache.info + else + local ok, info = pcall(provider.get_info, self:authenticate(provider_name)) + if ok then + infos[provider_name] = info + if cache then + cache.info = info + cache.info_expires_at = now + CACHE_TTL + end + else + log.warn('Failed to get info for provider ' .. provider_name .. ': ' .. info) + end + end + end + end + + log.debug('Fetched provider infos:', #vim.tbl_keys(infos)) + return infos +end + --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.client.AskOptions: Options for the request diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index bdcb93ce..73cb25a0 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,5 +1,6 @@ local async = require('plenary.async') local copilot = require('CopilotChat') +local client = require('CopilotChat.client') local utils = require('CopilotChat.utils') ---@class CopilotChat.config.mappings.Diff @@ -439,6 +440,7 @@ return { local system_prompt = config.system_prompt async.run(function() + local infos = client:info() local selected_model = copilot.resolve_model(prompt, config) local selected_tools, resolved_resources = copilot.resolve_functions(prompt, config) selected_tools = vim.tbl_map(function(tool) @@ -451,6 +453,14 @@ return { table.insert(lines, '**Temp Files**: `' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`') table.insert(lines, '') + for provider, infolines in pairs(infos) do + table.insert(lines, '**Provider**: `' .. provider .. '`') + for _, line in ipairs(infolines) do + table.insert(lines, line) + end + table.insert(lines, '') + end + if source and utils.buf_valid(source.bufnr) then local source_name = vim.api.nvim_buf_get_name(source.bufnr) table.insert(lines, '**Source**: `' .. source_name .. '`') diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index eee655a8..5b35a2b4 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -171,6 +171,7 @@ end ---@class CopilotChat.config.providers.Provider ---@field disabled nil|boolean ---@field get_headers nil|fun():table,number? +---@field get_info nil|fun(headers:table):string[] ---@field get_models nil|fun(headers:table):table ---@field embed nil|string|fun(inputs:table, headers:table):table ---@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table @@ -204,6 +205,56 @@ M.copilot = { response.body.expires_at end, + get_info = function(headers) + local response, err = utils.curl_get('https://api.github.com/copilot_internal/user', { + json_response = true, + headers = { + ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + }, + }) + + if err then + error(err) + end + + local stats = response.body + local lines = {} + + if not stats or not stats.quota_snapshots then + return { 'No Copilot stats available.' } + end + + local function usage_line(name, snap) + if not snap then + return + end + + table.insert(lines, string.format(' **%s**', name)) + + if snap.unlimited then + table.insert(lines, ' Usage: Unlimited') + else + local used = snap.entitlement - snap.remaining + local percent = snap.entitlement > 0 and (used / snap.entitlement * 100) or 0 + table.insert(lines, string.format(' Usage: %d / %d (%.1f%%)', used, snap.entitlement, percent)) + table.insert(lines, string.format(' Remaining: %d', snap.remaining)) + if snap.overage_permitted ~= nil then + table.insert(lines, ' Overage: ' .. (snap.overage_permitted and 'Permitted' or 'Not Permitted')) + end + end + end + + usage_line('Premium requests', stats.quota_snapshots.premium_interactions) + usage_line('Chat', stats.quota_snapshots.chat) + usage_line('Completions', stats.quota_snapshots.completions) + + if stats.quota_reset_date then + table.insert(lines, string.format(' **Quota** resets on: %s', stats.quota_reset_date)) + end + + return lines + end, + get_models = function(headers) local response, err = utils.curl_get('https://api.githubcopilot.com/models', { json_response = true, From 776d4d4d8f693c0ded0d235d9195f6ddef20a8a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 31 Jul 2025 14:11:41 +0000 Subject: [PATCH 429/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 02932d3c..ff6f1ec8 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 30 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 31 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -473,6 +473,9 @@ Custom providers can implement these methods: -- Optional: Embeddings provider name or function embed?: string|function, + -- Optional: Extra info about the provider displayed in info panel + get_info?(): string[] + -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, From f53069c595a3b12bbe8b9b711917f9ef33c22a0a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 31 Jul 2025 17:38:14 +0200 Subject: [PATCH 430/728] fix: properly validate source window when retrieving cwd (#1231) Closes #1230 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index fa2e4308..d6dd3061 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -498,6 +498,9 @@ function M.set_source(source_winnr) bufnr = source_bufnr, winnr = source_winnr, cwd = function() + if not vim.api.nvim_win_is_valid(source_winnr) then + return '.' + end local dir = vim.w[source_winnr].cchat_cwd if not dir or dir == '' then return '.' From 82be513c07a27f55860d55144c54040d1c93cf2a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:20:29 +0200 Subject: [PATCH 431/728] fix(chat): improve how sticky prompts are stored and parsed (#1233) Preserve sticky from current user message as well when calling .ask directly. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 66 +++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d6dd3061..0fda4bad 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -38,14 +38,24 @@ local state = { ---@param prompt string ---@param config CopilotChat.config.Shared local function insert_sticky(prompt, config) + local existing_prompt = M.chat:get_message('user') + local combined_prompt = (existing_prompt and existing_prompt.content or '') .. '\n' .. (prompt or '') local lines = vim.split(prompt or '', '\n') local stickies = utils.ordered_map() local sticky_indices = {} + local in_code_block = false + for _, line in ipairs(vim.split(combined_prompt, '\n')) do + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then + stickies:set(vim.trim(line:sub(3)), true) + end + end for i, line in ipairs(lines) do if vim.startswith(line, '> ') then table.insert(sticky_indices, i) - stickies:set(vim.trim(line:sub(3)), true) end end for i = #sticky_indices, 1, -1 do @@ -99,6 +109,20 @@ local function insert_sticky(prompt, config) return table.concat(prompt_lines, '\n') end +local function store_sticky(prompt) + local sticky = {} + local in_code_block = false + for _, line in ipairs(vim.split(prompt, '\n')) do + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then + table.insert(sticky, line:sub(3)) + end + end + state.sticky = sticky +end + --- Update the highlights for chat buffer local function update_highlights() local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') @@ -498,11 +522,10 @@ function M.set_source(source_winnr) bufnr = source_bufnr, winnr = source_winnr, cwd = function() - if not vim.api.nvim_win_is_valid(source_winnr) then - return '.' - end - local dir = vim.w[source_winnr].cchat_cwd - if not dir or dir == '' then + local ok, dir = pcall(function() + return vim.w[source_winnr].cchat_cwd + end) + if not ok or not dir or dir == '' then return '.' end return dir @@ -764,6 +787,7 @@ function M.open(config) M.chat:open(config) + -- Add sticky values from provided config when opening the chat local message = M.chat:get_message('user') if message then local prompt = insert_sticky(message.content, config) @@ -772,7 +796,6 @@ function M.open(config) role = 'user', content = '\n' .. prompt, }, true) - M.chat:finish() end end @@ -889,37 +912,30 @@ function M.ask(prompt, config) vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) config = vim.tbl_deep_extend('force', M.config, config or {}) - prompt = insert_sticky(prompt, config) - prompt = vim.trim(prompt) + -- Stop previous conversation and open window if not config.headless then if config.clear_chat_on_new_prompt then M.stop(true) elseif client:stop() then finish() end - if not M.chat:focused() then M.open(config) end + else + update_source() + end + + -- Resolve prompt after window is opened + prompt = insert_sticky(prompt, config) + prompt = vim.trim(prompt) + -- Prepare chat + if not config.headless then + store_sticky(prompt) M.chat:start() M.chat:append('\n') - - local sticky = {} - local in_code_block = false - for _, line in ipairs(vim.split(prompt, '\n')) do - if line:match('^```') then - in_code_block = not in_code_block - end - if vim.startswith(line, '> ') and not in_code_block then - table.insert(sticky, line:sub(3)) - end - end - - state.sticky = sticky - else - update_source() end -- Resolve prompt references From fc93d1c535bf9538a0a036f118b1034930ee5eb9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:24:08 +0200 Subject: [PATCH 432/728] fix(chat): properly reset modifiable after modifying it (#1234) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index e2cf7503..049973f8 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -439,6 +439,7 @@ function Chat:add_message(message, replace) local section = current_message.section if section then + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines( self.bufnr, @@ -447,7 +448,7 @@ function Chat:add_message(message, replace) false, vim.split(message.content, '\n') ) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable self:append('') end else @@ -474,9 +475,10 @@ function Chat:remove_message(role) end -- Remove the section from the buffer + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 2, section.end_line + 1, false, {}) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable -- Remove the message from the messages list for i, msg in ipairs(self.messages) do @@ -505,9 +507,10 @@ function Chat:append(str) local last_line, last_column, _ = self:last() + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_text(self.bufnr, last_line, last_column, last_line, last_column, vim.split(str, '\n')) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable if should_follow_cursor then self:follow() @@ -520,9 +523,11 @@ function Chat:clear() self.token_count = nil self.token_max_count = nil self.messages = {} + + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable end --- Create the chat window buffer. From dec3127e4f373875d7fd50854e221ed8dc0e061f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:27:05 +0200 Subject: [PATCH 433/728] fix(utils): remove temp file after curl request is done (#1235) Closes #1194 Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index ae3e6313..0b42fffb 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -398,8 +398,16 @@ M.curl_post = async.wrap(function(url, opts, callback) args = vim.tbl_deep_extend('force', M.curl_args, args) args = vim.tbl_deep_extend('force', args, opts or {}) + local temp_file_path = nil + args.callback = function(response) log.debug('POST response:', url, response) + if temp_file_path then + local ok, err = pcall(os.remove, temp_file_path) + if not ok then + log.debug('Failed to remove temp file:', temp_file_path, err) + end + end if response and not vim.startswith(tostring(response.status), '20') then callback(response, response.body) return @@ -430,7 +438,8 @@ M.curl_post = async.wrap(function(url, opts, callback) ['Content-Type'] = 'application/json', }) - args.body = M.temp_file(vim.json.encode(args.body)) + temp_file_path = M.temp_file(vim.json.encode(args.body)) + args.body = temp_file_path end curl.post(url, args) From 425ff0c48906a94ca522f6d2e98e4b39057e4fd4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:30:59 +0200 Subject: [PATCH 434/728] fix(chat): highlight keywords only in user messages (#1236) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 049973f8..06b35d64 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -669,6 +669,22 @@ function Chat:render() end end + -- Keywords + if current_message and current_message.role == 'user' then + -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it + local patterns = { + '()#?#[^ ]+()', + '()@[^ ]+()', + '()%$[^ ]+()', + '()/[^ ]+()', + } + for _, pattern in ipairs(patterns) do + for s, e in line:gmatch(pattern) do + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) + end + end + end + -- If last line, finish last message if l == #lines and current_message then current_message.section.end_line = l @@ -695,20 +711,6 @@ function Chat:render() end end end - - -- Highlight keywords - -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it - local patterns = { - '()#?#[^ ]+()', - '()@[^ ]+()', - '()%$[^ ]+()', - '()/[^ ]+()', - } - for _, pattern in ipairs(patterns) do - for s, e in line:gmatch(pattern) do - vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) - end - end end -- Replace self.messages with new_messages (preserving tool_calls, etc.) From 1a17534c17e6ae9f5417df08b8c0eec434c47875 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:45:30 +0200 Subject: [PATCH 435/728] fix(chat): show messages in overlay (#1237) message can be triggered from other places other than chat so just show chat overlay instead Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 +-- lua/CopilotChat/ui/chat.lua | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0fda4bad..dd9fb70c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1213,8 +1213,7 @@ function M.setup(config) M.chat:delete() end M.chat = require('CopilotChat.ui.chat')( - M.config.headers, - M.config.separator, + M.config, utils.key_to_info('show_help', M.config.mappings.show_help), function(bufnr) for name, _ in pairs(M.config.mappings) do diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 06b35d64..5605ee16 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -68,18 +68,18 @@ end ---@field private separator string ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay -local Chat = class(function(self, headers, separator, help, on_buf_create) +local Chat = class(function(self, config, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) self.winnr = nil - self.config = {} + self.config = config self.token_count = nil self.token_max_count = nil self.messages = {} self.layout = nil - self.headers = headers or {} - self.separator = separator + self.headers = config.headers + self.separator = config.separator self.spinner = Spinner() self.chat_overlay = Overlay('copilot-overlay', 'q to close', function(bufnr) @@ -97,7 +97,12 @@ local Chat = class(function(self, headers, separator, help, on_buf_create) notify.listen(notify.MESSAGE, function(msg) utils.schedule_main() - self:append('\n' .. msg .. '\n') + + if not self:visible() then + self:open(self.config) + end + + self:overlay({ text = msg }) end) end, Overlay) From 7c82936f2126b106af1b1bf0f9ae4d42dd45fcad Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 02:00:10 +0200 Subject: [PATCH 436/728] fix(prompt): be more specific when definining what is resource (#1238) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/prompts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 2b56bdda..8bb5efd9 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -29,7 +29,7 @@ If tools are explicitly defined in your system context: - Execute actions directly when you indicate you'll do so, without asking for permission. - Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel. - Before using tools to retrieve information, check if it's already available in context: - 1. Content shared via "#:" references or headers + 1. Resources shared via "# " headers and referenced via "##" links 2. Code blocks with file path labels 3. Other contextual sharing like selected text or conversation history - If you don't have explicit tool definitions in your system context, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. From 718f48b120f98081a556df0caadd916511490ecb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 00:00:28 +0000 Subject: [PATCH 437/728] 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 ff6f1ec8..bde68614 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 31 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 02cf9e52634b3e3d45beb2c4e5bbc17da28aef64 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 02:24:01 +0200 Subject: [PATCH 438/728] feat(health): add temp dir writable check (#1239) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 1 - lua/CopilotChat/health.lua | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 73cb25a0..3bd339cc 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -450,7 +450,6 @@ return { utils.schedule_main() table.insert(lines, '**Logs**: `' .. copilot.config.log_path .. '`') table.insert(lines, '**History**: `' .. copilot.config.history_path .. '`') - table.insert(lines, '**Temp Files**: `' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`') table.insert(lines, '') for provider, infolines in pairs(infos) do diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index a75416ee..acfc5bd2 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -55,6 +55,25 @@ function M.check() error('setup: not called, required for plugin to work. See `:h CopilotChat-installation`.') end + start('CopilotChat.nvim [filesystem]') + + local testfile = os.tmpname() + local f = io.open(testfile, 'w') + local writable = false + if f then + f:write('test') + f:close() + writable = true + end + if writable then + ok('temp dir: writable (' .. testfile .. ')') + os.remove(testfile) + else + local stat = vim.loop.fs_stat(vim.fn.fnamemodify(testfile, ':h')) + local perms = stat and string.format('%o', stat.mode % 512) or 'unknown' + error('temp dir: not writable. Permissions: ' .. perms .. ' (dir: ' .. vim.fn.fnamemodify(testfile, ':h') .. ')') + end + start('CopilotChat.nvim [commands]') local curl_version = run_command('curl', '--version') From 01d38b27ea2183302c743dac09b27611d09d7591 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 02:41:03 +0200 Subject: [PATCH 439/728] feat(providers): prioritize gh clie auth if available for github models (#1240) Also add healthchecks Signed-off-by: Tomas Slusny --- README.md | 2 -- lua/CopilotChat/config/providers.lua | 35 ++++++++++++++++++++++++---- lua/CopilotChat/health.lua | 11 ++++++--- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 19e790c6..333b8495 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,6 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities ## Optional Dependencies -- [copilot.vim](https://github.com/github/copilot.vim) - For `:Copilot setup` authorization, otherwise in-built method i used - - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - For accurate token counting - Arch Linux: 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 - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5b35a2b4..d2ac7976 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -99,7 +99,7 @@ end --- Get the github copilot oauth cached token (gu_ token) ---@return string -local function get_github_token(tag) +local function get_github_copilot_token(tag) local function config_path() local config = vim.fs.normalize('$XDG_CONFIG_HOME') if config and vim.uv.fs_stat(config) then @@ -157,6 +157,33 @@ local function get_github_token(tag) return github_device_flow(tag, 'Iv1.b507a08c87ecfe98', '') end +local function get_github_models_token(tag) + local token = get_token(tag) + if token then + return token + end + + -- loading token from the environment only in GitHub Codespaces + local codespaces = os.getenv('CODESPACES') + token = os.getenv('GITHUB_TOKEN') + if token and codespaces then + return set_token(tag, token, false) + end + + -- loading token from gh cli if available + if vim.fn.executable('gh') == 0 then + local result = utils.system({ 'gh', 'auth', 'token', '-h', 'github.com' }) + if result and result.code == 0 and result.stdout then + local gh_token = vim.trim(result.stdout) + if gh_token ~= '' and not gh_token:find('no oauth token') then + return set_token(tag, gh_token, false) + end + end + end + + return github_device_flow(tag, '178c6fc778ccc68e1d6a', 'read:user copilot') +end + ---@class CopilotChat.config.providers.Options ---@field model CopilotChat.client.Model ---@field temperature number? @@ -188,7 +215,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), }, }) @@ -209,7 +236,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/user', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), }, }) @@ -430,7 +457,7 @@ M.github_models = { get_headers = function() return { - ['Authorization'] = 'Bearer ' .. github_device_flow('github_models', 'Ov23liqtJusaUH38tIoK', 'read:user copilot'), + ['Authorization'] = 'Bearer ' .. get_github_models_token('github_models'), } end, diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index acfc5bd2..1c8bc3b4 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -55,8 +55,6 @@ function M.check() error('setup: not called, required for plugin to work. See `:h CopilotChat-installation`.') end - start('CopilotChat.nvim [filesystem]') - local testfile = os.tmpname() local f = io.open(testfile, 'w') local writable = false @@ -104,6 +102,13 @@ function M.check() ok('lynx: ' .. lynx_version) end + local gh_version = run_command('gh', '--version') + if gh_version == false then + warn('gh: missing, optional for improved GitHub authorization. See "https://cli.github.com/".') + else + ok('gh: ' .. gh_version) + end + start('CopilotChat.nvim [dependencies]') if lualib_installed('plenary') then @@ -118,7 +123,7 @@ function M.check() ok('copilot: ' .. (has_copilot and 'copilot.lua' or 'copilot.vim')) else warn( - 'copilot: missing, optional for improved auth implementation. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' + 'copilot: missing, optional for improved Copilot authorization. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' ) end From 3264dd25ef9d82862c88fe57c1ed2fcacab37c1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 00:41:24 +0000 Subject: [PATCH 440/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index bde68614..4400881d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -62,16 +62,12 @@ capabilities directly into your editor. It provides: OPTIONAL DEPENDENCIES *CopilotChat-optional-dependencies* -- copilot.vim - For `:Copilot setup` - authorization, otherwise in-built method i used -- tiktoken_core - For accurate token - counting +- tiktoken_core - For accurate token counting - Arch Linux: Install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from lua-tiktoken releases and save as `tiktoken_core.so` in your Lua path - git - For git diff context features -- ripgrep - For improved search - performance +- ripgrep - For improved search performance - lynx - For improved URL context features From ea4168476a0fdbd5bf40a4a769d6c1dc998929eb Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 03:05:39 +0200 Subject: [PATCH 441/728] feat(mappings): use C-Space as default completion trigger instead of (#1241) To prevent conflicts with copilot.vim and copilot.lua by default, confusing people Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 3bd339cc..aaaa0ea6 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -137,7 +137,7 @@ end ---@field show_help CopilotChat.config.mapping|false|nil return { complete = { - insert = '', + insert = '', callback = function() copilot.trigger_complete() end, From 653111bbbbc8fa6f8aa8a9bf5f820c6f926c9b89 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 03:08:30 +0200 Subject: [PATCH 442/728] chore: update readme and do not show empty choices for completion (#1242) Signed-off-by: Tomas Slusny --- README.md | 32 ++++++++++++++++---------------- lua/CopilotChat/functions.lua | 4 +++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 333b8495..d9d5d433 100644 --- a/README.md +++ b/README.md @@ -149,22 +149,22 @@ Commands are used to control the chat interface: Default mappings in the chat interface: -| Insert | Normal | Action | -| ------- | ------- | ------------------------------------------ | -| `` | - | Trigger/accept completion menu for tokens | -| `` | `q` | Close the chat window | -| `` | `` | Reset and clear the chat window | -| `` | `` | Submit the current prompt | -| - | `grr` | Toggle sticky prompt for line under cursor | -| - | `grx` | Clear all sticky prompts in prompt | -| `` | `` | Accept nearest diff | -| - | `gj` | Jump to section of nearest diff | -| - | `gqa` | Add all answers from chat to quickfix list | -| - | `gqd` | Add all diffs from chat to quickfix list | -| - | `gy` | Yank nearest diff to register | -| - | `gd` | Show diff between source and nearest diff | -| - | `gc` | Show info about current chat | -| - | `gh` | Show help message | +| Insert | Normal | Action | +| ----------- | ------- | ------------------------------------------ | +| `` | - | Trigger/accept completion menu for tokens | +| `` | `q` | Close the chat window | +| `` | `` | Reset and clear the chat window | +| `` | `` | Submit the current prompt | +| - | `grr` | Toggle sticky prompt for line under cursor | +| - | `grx` | Clear all sticky prompts in prompt | +| `` | `` | Accept nearest diff | +| - | `gj` | Jump to section of nearest diff | +| - | `gqa` | Add all answers from chat to quickfix list | +| - | `gqd` | Add all diffs from chat to quickfix list | +| - | `gy` | Yank nearest diff to register | +| - | `gd` | Show diff between source and nearest diff | +| - | `gc` | Show info about current chat | +| - | `gh` | Show help message | The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 04c5b103..782510ea 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -211,7 +211,9 @@ function M.enter_input(schema, source) if cfg.enum then local choices = type(cfg.enum) == 'table' and cfg.enum or cfg.enum(source) local choice - if #choices == 1 then + if #choices == 0 then + choice = nil + elseif #choices == 1 then choice = choices[1] else choice = utils.select(choices, { From cbea42c9cf3bab433b02674b696cd7d540eb1f52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 01:08:46 +0000 Subject: [PATCH 443/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 4400881d..716fbd92 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -183,22 +183,22 @@ KEY MAPPINGS *CopilotChat-key-mappings* Default mappings in the chat interface: - Insert Normal Action - -------- -------- -------------------------------------------- - - Trigger/accept completion menu for tokens - q Close the chat window - Reset and clear the chat window - Submit the current prompt - - grr Toggle sticky prompt for line under cursor - - grx Clear all sticky prompts in prompt - Accept nearest diff - - gj Jump to section of nearest diff - - gqa Add all answers from chat to quickfix list - - gqd Add all diffs from chat to quickfix list - - gy Yank nearest diff to register - - gd Show diff between source and nearest diff - - gc Show info about current chat - - gh Show help message + Insert Normal Action + ----------- -------- -------------------------------------------- + - Trigger/accept completion menu for tokens + q Close the chat window + Reset and clear the chat window + Submit the current prompt + - grr Toggle sticky prompt for line under cursor + - grx Clear all sticky prompts in prompt + Accept nearest diff + - gj Jump to section of nearest diff + - gqa Add all answers from chat to quickfix list + - gqd Add all diffs from chat to quickfix list + - gy Yank nearest diff to register + - gd Show diff between source and nearest diff + - gc Show info about current chat + - gh Show help message The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: From f7a3228f155d0533197ac79b0e08582e504d0399 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 10:36:25 +0200 Subject: [PATCH 444/728] fix(functions): properly filter tool schema from functions (#1243) Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 782510ea..6e936a3d 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -44,12 +44,12 @@ local function sorted_propnames(schema) return prop_names end -local function filter_schema(tbl) +local function filter_schema(tbl, root) if type(tbl) ~= 'table' then return tbl end - if utils.empty(tbl.properties) then + if root and utils.empty(tbl.properties) then return nil end @@ -139,7 +139,7 @@ function M.parse_schema(tool) end if schema then - schema = filter_schema(schema) + schema = filter_schema(schema, true) end return schema From d1d155e50193e28a3ec00f8e21d6f11445f96ea1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 11:41:43 +0200 Subject: [PATCH 445/728] fix(chat): properly replace all message data when replacing message (#1244) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 5605ee16..fe70feff 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -440,7 +440,11 @@ function Chat:add_message(message, replace) elseif replace and current_message then -- Replace the content of the current message self:render() - current_message.content = message.content + + for k, v in pairs(message) do + current_message[k] = v + end + local section = current_message.section if section then From c3d00484c42065a883db0fb859c686e277012d6c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 11:54:00 +0200 Subject: [PATCH 446/728] fix(chat): do not allow sending empty prompt (#1245) Closes #1189 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index dd9fb70c..1c25eda5 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -991,6 +991,14 @@ function M.ask(prompt, config) end end + if utils.empty(prompt) and utils.empty(resolved_tools) then + if not config.headless then + M.chat:remove_message('user') + finish() + end + return + end + local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, history = M.chat.messages, From ced388c97b313ea235809824ed501970b155e59f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 12:46:36 +0200 Subject: [PATCH 447/728] feat(prompts): add configurable response language (#1246) Closes #1086 Signed-off-by: Tomas Slusny --- README.md | 8 ++++---- lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/config/prompts.lua | 1 + lua/CopilotChat/init.lua | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d9d5d433..3355f1b7 100644 --- a/README.md +++ b/README.md @@ -171,14 +171,13 @@ The mappings can be customized by setting the `mappings` table in your configura - `normal`: Key for normal mode - `insert`: Key for insert mode -For example, to change the submit prompt mapping or show_diff full diff option: +For example, to change the complete mapping to Tab or show_diff full diff option: ```lua { mappings = { - submit_prompt = { - normal = 's', - insert = '' + complete = { + insert = '' } show_diff = { full_diff = true @@ -444,6 +443,7 @@ Below are all available configuration options with their default values: model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + language = 'English', -- Default language to use for answers resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 8809fdc6..30ad2bd8 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -18,6 +18,7 @@ ---@field model string? ---@field tools string|table|nil ---@field sticky string|table|nil +---@field language string? ---@field resource_processing boolean? ---@field temperature number? ---@field headless boolean? @@ -58,6 +59,7 @@ return { model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + language = 'English', -- Default language to use for answers resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 8bb5efd9..8764f914 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -2,6 +2,7 @@ local COPILOT_BASE = [[ When asked for your name, you must respond with "GitHub Copilot". Follow the user's requirements carefully & to the letter. Keep your answers short and impersonal. +Always answer in {LANGUAGE} unless explicitly asked otherwise. The user works in editor called Neovim which has these core concepts: - Buffer: An in-memory text content that may be associated with a file diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1c25eda5..67848dd8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -473,6 +473,7 @@ function M.resolve_prompt(prompt, config) if config.system_prompt then config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) + config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) if state.source then config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end From aa5e50ee15cfdb0e8cb66ecab9f928ed0fc148ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 10:46:52 +0000 Subject: [PATCH 448/728] 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 716fbd92..8ecf90b9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -205,14 +205,14 @@ configuration. Each mapping can have: - `normal`: Key for normal mode - `insert`: Key for insert mode -For example, to change the submit prompt mapping or show_diff full diff option: +For example, to change the complete mapping to Tab or show_diff full diff +option: >lua { mappings = { - submit_prompt = { - normal = 's', - insert = '' + complete = { + insert = '' } show_diff = { full_diff = true @@ -515,6 +515,7 @@ Below are all available configuration options with their default values: model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + language = 'English', -- Default language to use for answers resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) From ed28296abb59fe083855d6f70371fbccbc939a92 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 18:29:20 +0200 Subject: [PATCH 449/728] docs: properly use functions instead of tools in README (#1248) Signed-off-by: Tomas Slusny --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3355f1b7..9917c928 100644 --- a/README.md +++ b/README.md @@ -286,24 +286,24 @@ For supported models, see: ## Functions -Functions provide additional information and behaviour to the chat. -Tools can be organized into groups by setting the `group` property. Tools assigned to a group are not automatically made available to the LLM - they must be explicitly activated. -To use grouped tools in your prompt, include `@group_name` in your message. This allows the LLM to access and use all tools in that group during the current interaction. -Add tools using `#tool_name[:input]` syntax: - -| Function | Input Support | Description | -| ------------- | ------------- | ------------------------------------------------------ | -| `buffer` | ✓ (name) | Retrieves content from a specific buffer | -| `buffers` | ✓ (scope) | Fetches content from multiple buffers (listed/visible) | -| `diagnostics` | ✓ (scope) | Collects code diagnostics (errors, warnings) | -| `file` | ✓ (path) | Reads content from a specified file path | -| `gitdiff` | ✓ (sha) | Retrieves git diff information (unstaged/staged/sha) | -| `gitstatus` | - | Retrieves git status information | -| `glob` | ✓ (pattern) | Lists filenames matching a pattern in workspace | -| `grep` | ✓ (pattern) | Searches for a pattern across files in workspace | -| `quickfix` | - | Includes content of files in quickfix list | -| `register` | ✓ (register) | Provides access to specified Vim register | -| `url` | ✓ (url) | Fetches content from a specified URL | +Functions provide additional information and behaviour to the chat. +Functions can be organized into groups by setting the `group` property. +Functions can be made available to the LLM with `@group_name` or `@function_name` syntax. LLM will then be able to use them in responses as tool calls. +If function has URI, they can also be used directly in prompt with `#function_name[:input]` syntax for providing context as resources. + +| Function | Input Support | URI | Description | +| ------------- | ------------- | --- | ------------------------------------------------------ | +| `buffer` | ✓ (name) | ✓ | Retrieves content from a specific buffer | +| `buffers` | ✓ (scope) | ✓ | Fetches content from multiple buffers (listed/visible) | +| `diagnostics` | ✓ (scope) | ✓ | Collects code diagnostics (errors, warnings) | +| `file` | ✓ (path) | ✓ | Reads content from a specified file path | +| `gitdiff` | ✓ (sha) | ✓ | Retrieves git diff information (unstaged/staged/sha) | +| `gitstatus` | - | ✓ | Retrieves git status information | +| `glob` | ✓ (pattern) | ✓ | Lists filenames matching a pattern in workspace | +| `grep` | ✓ (pattern) | ✓ | Searches for a pattern across files in workspace | +| `quickfix` | - | ✓ | Includes content of files in quickfix list | +| `register` | ✓ (register) | ✓ | Provides access to specified Vim register | +| `url` | ✓ (url) | ✓ | Fetches content from a specified URL | Examples: @@ -318,6 +318,9 @@ Examples: > #quickfix > #register:+ > #url:https://example.com +> @glob +> @grep +> @file ``` Define your own functions in the configuration with input handling and schema: @@ -563,7 +566,7 @@ local chat = require("CopilotChat") chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references -chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM +chat.resolve_functions() -- Resolve functions that are available for automatic use by LLM (WARN: async, requires plenary.async.run) chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -619,6 +622,7 @@ window:add_sticky(sticky) -- Add sticky prompt to chat mess -- Content Management window:append(text) -- Append text to chat window window:clear() -- Clear chat window content +window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window -- Navigation From 40a88e13dac6d15427eeac951b7d2274613971ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 16:29:43 +0000 Subject: [PATCH 450/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 57 ++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8ecf90b9..1e5a908f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -329,42 +329,43 @@ For supported models, see: FUNCTIONS *CopilotChat-functions* -Functions provide additional information and behaviour to the chat. Tools can -be organized into groups by setting the `group` property. Tools assigned to a -group are not automatically made available to the LLM - they must be explicitly -activated. To use grouped tools in your prompt, include `@group_name` in your -message. This allows the LLM to access and use all tools in that group during -the current interaction. Add tools using `#tool_name[:input]` syntax: +Functions provide additional information and behaviour to the chat. Functions +can be organized into groups by setting the `group` property. Functions can be +made available to the LLM with `@group_name` or `@function_name` syntax. LLM +will then be able to use them in responses as tool calls. If function has URI, +they can also be used directly in prompt with `#function_name[:input]` syntax +for providing context as resources. - -------------------------------------------------------------------------- - Function Input Description - Support - ------------- ------------ ----------------------------------------------- - buffer ✓ (name) Retrieves content from a specific buffer + ------------------------------------------------------------------------------- + Function Input URI Description + Support + ------------- ------------ ----- ---------------------------------------------- + buffer ✓ (name) ✓ Retrieves content from a specific buffer - buffers ✓ (scope) Fetches content from multiple buffers - (listed/visible) + buffers ✓ (scope) ✓ Fetches content from multiple buffers + (listed/visible) - diagnostics ✓ (scope) Collects code diagnostics (errors, warnings) + diagnostics ✓ (scope) ✓ Collects code diagnostics (errors, warnings) - file ✓ (path) Reads content from a specified file path + file ✓ (path) ✓ Reads content from a specified file path - gitdiff ✓ (sha) Retrieves git diff information - (unstaged/staged/sha) + gitdiff ✓ (sha) ✓ Retrieves git diff information + (unstaged/staged/sha) - gitstatus - Retrieves git status information + gitstatus - ✓ Retrieves git status information - glob ✓ (pattern) Lists filenames matching a pattern in workspace + glob ✓ (pattern) ✓ Lists filenames matching a pattern in + workspace - grep ✓ (pattern) Searches for a pattern across files in - workspace + grep ✓ (pattern) ✓ Searches for a pattern across files in + workspace - quickfix - Includes content of files in quickfix list + quickfix - ✓ Includes content of files in quickfix list - register ✓ (register) Provides access to specified Vim register + register ✓ (register) ✓ Provides access to specified Vim register - url ✓ (url) Fetches content from a specified URL - -------------------------------------------------------------------------- + url ✓ (url) ✓ Fetches content from a specified URL + ------------------------------------------------------------------------------- Examples: >markdown @@ -378,6 +379,9 @@ Examples: > #quickfix > #register:+ > #url:https://example.com + > @glob + > @grep + > @file < Define your own functions in the configuration with input handling and schema: @@ -640,7 +644,7 @@ CORE *CopilotChat-core* chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references - chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM + chat.resolve_functions() -- Resolve functions that are available for automatic use by LLM (WARN: async, requires plenary.async.run) chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -697,6 +701,7 @@ You can also access the chat window UI methods through the `chat.chat` object: -- Content Management window:append(text) -- Append text to chat window window:clear() -- Clear chat window content + window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window -- Navigation From 9fd068f5d6a0ca00fc739a98f29125cb577b2dfa Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 23:19:27 +0200 Subject: [PATCH 451/728] fix(files): use also plenary filetype on top of vim.filetype.match (#1250) I thought its fine to go back to vim.filetype.match but its as unreliable as ever. Closes #1249 Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 0b42fffb..251238ca 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -214,7 +214,17 @@ end ---@param filename string The file name ---@return string|nil function M.filetype(filename) - return vim.filetype.match({ filename = filename }) + local filetype = require('plenary.filetype') + + local ft = filetype.detect(filename, { + fs_access = false, + }) + + if ft == '' or not ft then + return vim.filetype.match({ filename = filename }) + end + + return ft end --- Get the mimetype from filetype From 3509cf0971c59ba79fbcd618d82910f8567a7929 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 14:16:56 +0200 Subject: [PATCH 452/728] fix(functions): change neovim://buffer to just buffer:// to avoid conflicts (#1252) also make quickfix function properly support buffer resources Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 30 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 65363811..25c3f6c5 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -118,7 +118,7 @@ return { buffer = { group = 'copilot', - uri = 'neovim://buffer/{name}', + uri = 'buffer://{name}', description = 'Retrieves content from a specific buffer. Useful for discussing or analyzing code from a particular file that is currently loaded.', schema = { @@ -162,7 +162,7 @@ return { end return { { - uri = 'neovim://buffer/' .. name, + uri = 'buffer://' .. name, mimetype = mimetype, data = data, }, @@ -172,7 +172,7 @@ return { buffers = { group = 'copilot', - uri = 'neovim://buffers/{scope}', + uri = 'buffers://{scope}', description = 'Fetches content from multiple buffers. Helps with discussing or analyzing code across multiple files simultaneously.', schema = { @@ -204,7 +204,7 @@ return { return nil end return { - uri = 'neovim://buffer/' .. name, + uri = 'buffer://' .. name, mimetype = mimetype, data = data, } @@ -229,23 +229,35 @@ return { return {} end - local unique_files = {} + local file_to_bufnr = {} for _, item in ipairs(items) do local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) if filename then - unique_files[filename] = true + if item.bufnr and utils.buf_valid(item.bufnr) then + file_to_bufnr[filename] = item.bufnr + else + file_to_bufnr[filename] = false + end end end return vim - .iter(vim.tbl_keys(unique_files)) + .iter(vim.tbl_keys(file_to_bufnr)) :map(function(file) - local data, mimetype = resources.get_file(file) + local bufnr = file_to_bufnr[file] + local data, mimetype, uri + if bufnr and bufnr ~= false then + data, mimetype = resources.get_buffer(bufnr) + uri = 'buffer://' .. file + else + data, mimetype = resources.get_file(file) + uri = 'file://' .. file + end if not data then return nil end return { - uri = 'file://' .. file, + uri = uri, mimetype = mimetype, data = data, } From eec50b885165da1e404b9effcc0f4d52e5f66f3b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 12:17:17 +0000 Subject: [PATCH 453/728] 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 1e5a908f..9888219d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 01 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 4cd53a48f04fe45331e4845fabd32a81e46f4d2f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 15:36:02 +0200 Subject: [PATCH 454/728] docs(README): improve readme (#1253) * docs(README): improve readme Signed-off-by: Tomas Slusny * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update readme Signed-off-by: Tomas Slusny * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update again Signed-off-by: Tomas Slusny * more readme updates Signed-off-by: Tomas Slusny * more updates Signed-off-by: Tomas Slusny --------- Signed-off-by: Tomas Slusny Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 488 ++++++++++++++++++------------------------------------ 1 file changed, 162 insertions(+), 326 deletions(-) diff --git a/README.md b/README.md index 9917c928..b0ea7e5e 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,24 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 -CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities directly into your editor. It provides: +CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. -- 🤖 GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) -- 💻 Rich workspace context powered by smart embeddings system -- 🔒 Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) -- 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) -- 📝 Interactive chat UI with completion, diffs and quickfix integration -- 🎯 Powerful prompt system with composable templates and sticky prompts -- 🔄 Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚡ Efficient token usage with tiktoken token counting and history management +- 🤖 **Multiple AI Models** - GitHub Copilot (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash) + custom providers (Ollama, Mistral.ai) +- 🔧 **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval +- 🔒 **Explicit Control** - Only shares what you specifically request - no background data collection +- 📝 **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration +- 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context +- ⚡ **Efficient** - Smart token usage with tiktoken counting and history management +- 🔌 **Extensible** - [Custom functions](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions) and [providers](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/providers), plus integrations like [mcphub.nvim](https://github.com/ravitemer/mcphub.nvim) -# Requirements +# Installation + +## Requirements -- [Neovim 0.10.0+](https://neovim.io/) - Older versions are not officially supported -- [curl](https://curl.se/) - Version 8.0.0+ recommended for best compatibility +- [Neovim 0.10.0+](https://neovim.io/) +- [curl 8.0.0+](https://curl.se/) - [Copilot chat in the IDE](https://github.com/settings/copilot) enabled in GitHub settings -- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) - Plugin dependency +- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) > [!WARNING] > For Neovim < 0.11.0, add `noinsert` or `noselect` to your `completeopt` otherwise chat autocompletion will not work. @@ -44,7 +45,6 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities - Arch Linux: 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 - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) and save as `tiktoken_core.so` in your Lua path - - [git](https://git-scm.com/) - For git diff context features - [ripgrep](https://github.com/BurntSushi/ripgrep) - For improved search performance - [lynx](https://lynx.invisible-island.net/) - For improved URL context features @@ -58,14 +58,6 @@ For various plugin pickers to work correctly, you need to replace `vim.ui.select - [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#%EF%B8%8F-config) - enable `ui_select` config - [mini.pick](https://github.com/echasnovski/mini.pick/blob/main/lua/mini/pick.lua#L1229) - set `vim.ui.select = require('mini.pick').ui_select` -Plugin features that use picker: - -- `:CopilotChatPrompts` - for selecting prompts -- `:CopilotChatModels` - for selecting models -- `#:` - for selecting function input - -# Installation - ## [lazy.nvim](https://github.com/folke/lazy.nvim) ```lua @@ -73,23 +65,18 @@ return { { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions + { "nvim-lua/plenary.nvim", branch = "master" }, }, build = "make tiktoken", opts = { -- See Configuration section for options }, - -- See Commands section for default commands if you want to lazy load on them }, } ``` -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) -Similar to the lazy setup, you can use the following configuration: - ```vim call plug#begin() Plug 'nvim-lua/plenary.nvim' @@ -97,40 +84,41 @@ Plug 'CopilotC-Nvim/CopilotChat.nvim' call plug#end() lua << EOF -require("CopilotChat").setup { - -- See Configuration section for options -} +require("CopilotChat").setup() EOF ``` -## Manual +# Core Concepts -1. Put the files in the right place +- **Resources** (`#`) - Add specific content (files, git diffs, URLs) to your prompt +- **Tools** (`@`) - Give LLM access to functions it can call with your approval +- **Sticky Prompts** (`> `) - Persist context across single chat session +- **Models** (`$`) - Specify which AI model to use for the chat +- **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -``` -mkdir -p ~/.config/nvim/pack/copilotchat/start -cd ~/.config/nvim/pack/copilotchat/start +## Examples -git clone https://github.com/nvim-lua/plenary.nvim -git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim -``` +```markdown +# Add specific file to context -2. Add to your configuration (e.g. `~/.config/nvim/init.lua`) +#file:src/main.lua -```lua -require("CopilotChat").setup { - -- See Configuration section for options -} +# Give LLM access to workspace tools + +@copilot What files are in this project? + +# Sticky prompt that persists + +> #buffer:current +> You are a helpful coding assistant ``` -See [@deathbeam](https://github.com/deathbeam) for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua) +When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdiff` etc. You'll see the proposed function call and can approve/reject it before execution. -# Features +# Usage ## Commands -Commands are used to control the chat interface: - | Command | Description | | -------------------------- | ----------------------------- | | `:CopilotChat ?` | Open chat with optional input | @@ -147,8 +135,6 @@ Commands are used to control the chat interface: ## Key Mappings -Default mappings in the chat interface: - | Insert | Normal | Action | | ----------- | ------- | ------------------------------------------ | | `` | - | Trigger/accept completion menu for tokens | @@ -166,73 +152,125 @@ Default mappings in the chat interface: | - | `gc` | Show info about current chat | | - | `gh` | Show help message | -The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: +## Built-in Functions + +| Function | Description | Example Usage | +| ------------- | ------------------------------------------------ | ---------------------- | +| `buffer` | Retrieves content from a specific buffer | `#buffer` | +| `buffers` | Fetches content from multiple buffers | `#buffers:visible` | +| `diagnostics` | Collects code diagnostics (errors, warnings) | `#diagnostics:current` | +| `file` | Reads content from a specified file path | `#file:path/to/file` | +| `gitdiff` | Retrieves git diff information | `#gitdiff:staged` | +| `gitstatus` | Retrieves git status information | `#gitstatus` | +| `glob` | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | +| `grep` | Searches for a pattern across files in workspace | `#grep:TODO` | +| `quickfix` | Includes content of files in quickfix list | `#quickfix` | +| `register` | Provides access to specified Vim register | `#register:+` | +| `url` | Fetches content from a specified URL | `#url:https://...` | + +## Predefined Prompts + +Use with `:CopilotChat` or reference with `/PromptName`: + +- `/Explain` - Explain selected code +- `/Review` - Code review +- `/Fix` - Fix bugs +- `/Optimize` - Performance improvements +- `/Docs` - Add documentation +- `/Tests` - Generate tests +- `/Commit` - Commit message + +# Configuration -- `normal`: Key for normal mode -- `insert`: Key for insert mode +For all available configuration options, see [`lua/CopilotChat/config.lua`](lua/CopilotChat/config.lua). -For example, to change the complete mapping to Tab or show_diff full diff option: +## Quick Setup + +Most users only need to configure a few options: ```lua { - mappings = { - complete = { - insert = '' - } - show_diff = { - full_diff = true - } - } + model = 'gpt-4.1', -- AI model to use + temperature = 0.1, -- Lower = focused, higher = creative + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float' + width = 0.5, -- 50% of screen width + }, + auto_insert_mode = true, -- Enter insert mode when opening } ``` -## Prompts +## Window & Appearance -### Predefined Prompts +```lua +{ + window = { + layout = 'float', + width = 80, -- Fixed width in columns + height = 20, -- Fixed height in rows + border = 'rounded', -- 'single', 'double', 'rounded', 'solid' + title = '🤖 AI Assistant', + zindex = 100, -- Ensure window stays on top + }, -Predefined prompt templates for common tasks. Reference them with `/PromptName` in chat, use `:CopilotChat` or `:CopilotChatPrompts` to select them: + headers = { + user = '👤 You: ', + assistant = '🤖 Copilot: ', + tool = '🔧 Tool: ', + }, + separator = '━━', + show_folds = false, -- Disable folding for cleaner look +} +``` -| Prompt | Description | -| ---------- | ------------------------------------------------ | -| `Explain` | Write an explanation for the selected code | -| `Review` | Review the selected code | -| `Fix` | Rewrite the code with bug fixes | -| `Optimize` | Optimize code for performance and readability | -| `Docs` | Add documentation comments to the code | -| `Tests` | Generate tests for the code | -| `Commit` | Write commit message using commitizen convention | +## Buffer Behavior -Define your own prompts in the configuration: +```lua +-- Auto-command to customize chat buffer behavior +vim.api.nvim_create_autocmd('BufEnter', { + pattern = 'copilot-*', + callback = function() + vim.opt_local.relativenumber = false + vim.opt_local.number = false + vim.opt_local.conceallevel = 0 + end, +}) +``` + +## Highlights + +You can customize colors by setting highlight groups in your config: ```lua -{ - prompts = { - MyCustomPrompt = { - prompt = 'Explain how it works.', - system_prompt = 'You are very good at explaining stuff', - mapping = 'ccmc', - description = 'My custom prompt description', - } - } -} +-- In your colorscheme or init.lua +vim.api.nvim_set_hl(0, 'CopilotChatHeader', { fg = '#7C3AED', bold = true }) +vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { fg = '#374151' }) +vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { fg = '#10B981', italic = true }) ``` -### System Prompts +Types of copilot highlights: -System prompts define the AI model's behavior. Reference them with `/PROMPT_NAME` in chat: +- `CopilotChatHeader` - Header highlight in chat buffer +- `CopilotChatSeparator` - Separator highlight in chat buffer +- `CopilotChatStatus` - Status and spinner in chat buffer +- `CopilotChatHelp` - Help messages in chat buffer (help, references) +- `CopilotChatSelection` - Selection highlight in source buffer +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) -| Prompt | Description | -| ---------------------- | ------------------------------------------ | -| `COPILOT_BASE` | All prompts should be built on top of this | -| `COPILOT_INSTRUCTIONS` | Base instructions | -| `COPILOT_EXPLAIN` | Adds coding tutor behavior | -| `COPILOT_REVIEW` | Adds code review behavior with diagnostics | +## Prompts -Define your own system prompts in the configuration (similar to `prompts`): +Define your own prompts in the configuration: ```lua { prompts = { + MyCustomPrompt = { + prompt = 'Explain how it works.', + system_prompt = 'You are very good at explaining stuff', + mapping = 'ccmc', + description = 'My custom prompt description', + }, Yarrr = { system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', }, @@ -243,86 +281,8 @@ Define your own system prompts in the configuration (similar to `prompts`): } ``` -### Sticky Prompts - -Sticky prompts persist across chat sessions. They're useful for maintaining model or resource selection. They work as follows: - -1. Prefix text with `> ` using markdown blockquote syntax -2. The prompt will be copied at the start of every new chat prompt -3. Edit sticky prompts freely while maintaining the `> ` prefix - -Examples: - -```markdown -> #glob:`*.lua` -> List all files in the workspace - -> @models Using Mistral-small -> What is 1 + 11 -``` - -You can also set default sticky prompts in the configuration: - -```lua -{ - sticky = { - '#glob:*.lua', - } -} -``` - -## Models - -You can control which AI model to use in three ways: - -1. List available models with `:CopilotChatModels` -2. Set model in prompt with `$model_name` -3. Configure default model via `model` config key - -For supported models, see: - -- [Copilot Chat Models](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat) -- [GitHub Marketplace Models](https://github.com/marketplace/models) (experimental, limited usage) - ## Functions -Functions provide additional information and behaviour to the chat. -Functions can be organized into groups by setting the `group` property. -Functions can be made available to the LLM with `@group_name` or `@function_name` syntax. LLM will then be able to use them in responses as tool calls. -If function has URI, they can also be used directly in prompt with `#function_name[:input]` syntax for providing context as resources. - -| Function | Input Support | URI | Description | -| ------------- | ------------- | --- | ------------------------------------------------------ | -| `buffer` | ✓ (name) | ✓ | Retrieves content from a specific buffer | -| `buffers` | ✓ (scope) | ✓ | Fetches content from multiple buffers (listed/visible) | -| `diagnostics` | ✓ (scope) | ✓ | Collects code diagnostics (errors, warnings) | -| `file` | ✓ (path) | ✓ | Reads content from a specified file path | -| `gitdiff` | ✓ (sha) | ✓ | Retrieves git diff information (unstaged/staged/sha) | -| `gitstatus` | - | ✓ | Retrieves git status information | -| `glob` | ✓ (pattern) | ✓ | Lists filenames matching a pattern in workspace | -| `grep` | ✓ (pattern) | ✓ | Searches for a pattern across files in workspace | -| `quickfix` | - | ✓ | Includes content of files in quickfix list | -| `register` | ✓ (register) | ✓ | Provides access to specified Vim register | -| `url` | ✓ (url) | ✓ | Fetches content from a specified URL | - -Examples: - -```markdown -> #buffer:init.lua -> #buffers:visible -> #diagnostics:current -> #file:path/to/file.js -> #git:staged -> #glob:`**/*.lua` -> #grep:`function setup` -> #quickfix -> #register:+ -> #url:https://example.com -> @glob -> @grep -> @file -``` - Define your own functions in the configuration with input handling and schema: ```lua @@ -356,47 +316,46 @@ Define your own functions in the configuration with input handling and schema: } ``` -### External Functions - -For external functions implementations, see the [discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions). - ## Selections -Selections determine the source content for chat interactions. - -Available selections are located in `local select = require("CopilotChat.select")`: - -| Selection | Description | -| --------- | ------------------------------------------------------ | -| `visual` | Current visual selection | -| `buffer` | Current buffer content | -| `line` | Current line content | -| `unnamed` | Unnamed register (last deleted/changed/yanked content) | - -You can set a default selection in the configuration: +Control what content is automatically included: ```lua { - -- Uses visual selection or falls back to buffer + -- Use visual selection, fallback to current line selection = function(source) - return select.visual(source) or select.buffer(source) - end + return require('CopilotChat.select').visual(source) or + require('CopilotChat.select').line(source) + end, } ``` -## Providers +**Available selections:** -Providers are modules that implement integration with different AI providers. +- `require('CopilotChat.select').visual` - Current visual selection +- `require('CopilotChat.select').buffer` - Entire buffer content +- `require('CopilotChat.select').line` - Current line content +- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked) -### Built-in Providers +## Providers -- `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers +Add custom AI providers: -### Provider Interface +```lua +{ + providers = { + my_provider = { + get_url = function(opts) return "https://api.example.com/chat" end, + get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end, + get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end, + prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, + prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, + } + } +} +``` -Custom providers can implement these methods: +**Provider Interface:** ```lua { @@ -426,134 +385,11 @@ Custom providers can implement these methods: } ``` -### External Providers - -For external providers (Ollama, LM Studio, Mistral.ai), see the [providers discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/providers). - -# Configuration - -## Default Configuration - -Below are all available configuration options with their default values: - -```lua -{ - - -- Shared config starts here (can be passed to functions at runtime and configured via setup function) - - system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - - model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - language = 'English', -- Default language to use for answers - - resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) +**Built-in providers:** - temperature = 0.1, -- Result temperature - headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember config as sticky prompts when asking questions - - -- default selection - -- see select.lua for implementation - selection = require('CopilotChat.select').visual, - - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout - 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 - blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent - }, - - 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 - 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) - - log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - - headers = { - user = '## User ', -- Header to use for user questions - assistant = '## Copilot ', -- Header to use for AI answers - tool = '## Tool ', -- Header to use for tool calls - }, - - separator = '───', -- Separator to use in chat - - -- default providers - -- see config/providers.lua for implementation - providers = require('CopilotChat.config.providers'), - - -- default functions - -- see config/functions.lua for implementation - functions = require('CopilotChat.config.functions'), - - -- default prompts - -- see config/prompts.lua for implementation - prompts = require('CopilotChat.config.prompts'), - - -- default mappings - -- see config/mappings.lua for implementation - mappings = require('CopilotChat.config.mappings'), -} -``` - -## Customizing Buffers - -Types of copilot buffers: - -- `copilot-chat` - Main chat buffer -- `copilot-overlay` - Overlay buffers (e.g. help, info, diff) - -You can set local options for plugin buffers like this: - -```lua -vim.api.nvim_create_autocmd('BufEnter', { - pattern = 'copilot-*', - callback = function() - -- Set buffer-local options - vim.opt_local.relativenumber = false - vim.opt_local.number = false - vim.opt_local.conceallevel = 0 - end -}) -``` - -## Customizing Highlights - -Types of copilot highlights: - -- `CopilotChatHeader` - Header highlight in chat buffer -- `CopilotChatSeparator` - Separator highlight in chat buffer -- `CopilotChatStatus` - Status and spinner in chat buffer -- `CopilotChatHelp` - Help messages in chat buffer (help, references) -- `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) -- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) +- `copilot` - GitHub Copilot (default) +- `github_models` - GitHub Marketplace models (disabled by default) +- `copilot_embeddings` - Copilot embeddings provider # API Reference From 5ee40855485b5e1e661b74ecd57e37801594ae4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 13:36:23 +0000 Subject: [PATCH 455/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 571 +++++++++++++++----------------------------- 1 file changed, 198 insertions(+), 373 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9888219d..743276dd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -3,25 +3,28 @@ ============================================================================== Table of Contents *CopilotChat-table-of-contents* -1. Requirements |CopilotChat-requirements| +1. Installation |CopilotChat-installation| + - Requirements |CopilotChat-requirements| - Optional Dependencies |CopilotChat-optional-dependencies| - Integration with pickers |CopilotChat-integration-with-pickers| -2. Installation |CopilotChat-installation| - lazy.nvim |CopilotChat-lazy.nvim| - vim-plug |CopilotChat-vim-plug| - - Manual |CopilotChat-manual| -3. Features |CopilotChat-features| +2. Core Concepts |CopilotChat-core-concepts| + - Examples |CopilotChat-examples| +3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Key Mappings |CopilotChat-key-mappings| + - Built-in Functions |CopilotChat-built-in-functions| + - Predefined Prompts |CopilotChat-predefined-prompts| +4. Configuration |CopilotChat-configuration| + - Quick Setup |CopilotChat-quick-setup| + - Window & Appearance |CopilotChat-window-&-appearance| + - Buffer Behavior |CopilotChat-buffer-behavior| + - Highlights |CopilotChat-highlights| - Prompts |CopilotChat-prompts| - - Models |CopilotChat-models| - Functions |CopilotChat-functions| - Selections |CopilotChat-selections| - Providers |CopilotChat-providers| -4. Configuration |CopilotChat-configuration| - - Default Configuration |CopilotChat-default-configuration| - - Customizing Buffers |CopilotChat-customizing-buffers| - - Customizing Highlights |CopilotChat-customizing-highlights| 5. API Reference |CopilotChat-api-reference| - Core |CopilotChat-core| - Chat Window |CopilotChat-chat-window| @@ -33,26 +36,28 @@ Table of Contents *CopilotChat-table-of-contents* 8. Stargazers |CopilotChat-stargazers| 9. Links |CopilotChat-links| -CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat -capabilities directly into your editor. It provides: +CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim +with a focus on transparency and user control. -- 🤖 GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) -- 💻 Rich workspace context powered by smart embeddings system -- 🔒 Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) -- 🔌 Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) -- 📝 Interactive chat UI with completion, diffs and quickfix integration -- 🎯 Powerful prompt system with composable templates and sticky prompts -- 🔄 Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚡ Efficient token usage with tiktoken token counting and history management +- 🤖 **Multiple AI Models** - GitHub Copilot (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash) + custom providers (Ollama, Mistral.ai) +- 🔧 **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval +- 🔒 **Explicit Control** - Only shares what you specifically request - no background data collection +- 📝 **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration +- 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context +- ⚡ **Efficient** - Smart token usage with tiktoken counting and history management +- 🔌 **Extensible** - Custom functions and providers , plus integrations like mcphub.nvim ============================================================================== -1. Requirements *CopilotChat-requirements* +1. Installation *CopilotChat-installation* + -- Neovim 0.10.0+ - Older versions are not officially supported -- curl - Version 8.0.0+ recommended for best compatibility +REQUIREMENTS *CopilotChat-requirements* + +- Neovim 0.10.0+ +- curl 8.0.0+ - Copilot chat in the IDE enabled in GitHub settings -- plenary.nvim - Plugin dependency +- plenary.nvim [!WARNING] For Neovim < 0.11.0, add `noinsert` or `noselect` to your @@ -82,16 +87,6 @@ very basic). Here are some examples: - snacks.picker - enable `ui_select` config - mini.pick - set `vim.ui.select = require('mini.pick').ui_select` -Plugin features that use picker: - -- `:CopilotChatPrompts` - for selecting prompts -- `:CopilotChatModels` - for selecting models -- `#:` - for selecting function input - - -============================================================================== -2. Installation *CopilotChat-installation* - LAZY.NVIM *CopilotChat-lazy.nvim* @@ -100,25 +95,19 @@ LAZY.NVIM *CopilotChat-lazy.nvim* { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions + { "nvim-lua/plenary.nvim", branch = "master" }, }, build = "make tiktoken", opts = { -- See Configuration section for options }, - -- See Commands section for default commands if you want to lazy load on them }, } < -See @jellydn for configuration - - VIM-PLUG *CopilotChat-vim-plug* -Similar to the lazy setup, you can use the following configuration: - >vim call plug#begin() Plug 'nvim-lua/plenary.nvim' @@ -126,45 +115,49 @@ Similar to the lazy setup, you can use the following configuration: call plug#end() lua << EOF - require("CopilotChat").setup { - -- See Configuration section for options - } + require("CopilotChat").setup() EOF < -MANUAL *CopilotChat-manual* +============================================================================== +2. Core Concepts *CopilotChat-core-concepts* -1. Put the files in the right place +- **Resources** (`#`) - Add specific content (files, git diffs, URLs) to your prompt +- **Tools** (`@`) - Give LLM access to functions it can call with your approval +- **Sticky Prompts** (`> `) - Persist context across single chat session +- **Models** (`$`) - Specify which AI model to use for the chat +- **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -> - mkdir -p ~/.config/nvim/pack/copilotchat/start - cd ~/.config/nvim/pack/copilotchat/start - - git clone https://github.com/nvim-lua/plenary.nvim - git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim -< -1. Add to your configuration (e.g. `~/.config/nvim/init.lua`) +EXAMPLES *CopilotChat-examples* ->lua - require("CopilotChat").setup { - -- See Configuration section for options - } +>markdown + # Add specific file to context + + #file:src/main.lua + + # Give LLM access to workspace tools + + @copilot What files are in this project? + + # Sticky prompt that persists + + > #buffer:current + > You are a helpful coding assistant < -See @deathbeam for configuration - +When you use `@copilot`, the LLM can call functions like `glob`, `file`, +`gitdiff` etc. You’ll see the proposed function call and can approve/reject +it before execution. ============================================================================== -3. Features *CopilotChat-features* +3. Usage *CopilotChat-usage* COMMANDS *CopilotChat-commands* -Commands are used to control the chat interface: - Command Description -------------------------- ------------------------------- :CopilotChat ? Open chat with optional input @@ -181,8 +174,6 @@ Commands are used to control the chat interface: KEY MAPPINGS *CopilotChat-key-mappings* -Default mappings in the chat interface: - Insert Normal Action ----------- -------- -------------------------------------------- - Trigger/accept completion menu for tokens @@ -199,190 +190,162 @@ Default mappings in the chat interface: - gd Show diff between source and nearest diff - gc Show info about current chat - 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 +BUILT-IN FUNCTIONS *CopilotChat-built-in-functions* -For example, to change the complete mapping to Tab or show_diff full diff -option: + ------------------------------------------------------------------------------ + Function Description Example Usage + ------------- ----------------------------------------- ---------------------- + buffer Retrieves content from a specific buffer #buffer ->lua - { - mappings = { - complete = { - insert = '' - } - show_diff = { - full_diff = true - } - } - } -< + buffers Fetches content from multiple buffers #buffers:visible + diagnostics Collects code diagnostics (errors, #diagnostics:current + warnings) -PROMPTS *CopilotChat-prompts* + file Reads content from a specified file path #file:path/to/file + gitdiff Retrieves git diff information #gitdiff:staged -PREDEFINED PROMPTS ~ + gitstatus Retrieves git status information #gitstatus -Predefined prompt templates for common tasks. Reference them with `/PromptName` -in chat, use `:CopilotChat` or `:CopilotChatPrompts` to select -them: + glob Lists filenames matching a pattern in #glob:**/*.lua + workspace - Prompt Description - ---------- -------------------------------------------------- - Explain Write an explanation for the selected code - Review Review the selected code - Fix Rewrite the code with bug fixes - Optimize Optimize code for performance and readability - Docs Add documentation comments to the code - Tests Generate tests for the code - Commit Write commit message using commitizen convention -Define your own prompts in the configuration: + grep Searches for a pattern across files in #grep:TODO + workspace ->lua - { - prompts = { - MyCustomPrompt = { - prompt = 'Explain how it works.', - system_prompt = 'You are very good at explaining stuff', - mapping = 'ccmc', - description = 'My custom prompt description', - } - } - } -< + quickfix Includes content of files in quickfix #quickfix + list + register Provides access to specified Vim register #register:+ -SYSTEM PROMPTS ~ + url Fetches content from a specified URL #url:https://... + ------------------------------------------------------------------------------ -System prompts define the AI model’s behavior. Reference them with -`/PROMPT_NAME` in chat: +PREDEFINED PROMPTS *CopilotChat-predefined-prompts* - Prompt Description - ---------------------- -------------------------------------------- - COPILOT_BASE All prompts should be built on top of this - COPILOT_INSTRUCTIONS Base instructions - COPILOT_EXPLAIN Adds coding tutor behavior - COPILOT_REVIEW Adds code review behavior with diagnostics -Define your own system prompts in the configuration (similar to `prompts`): +Use with `:CopilotChat` or reference with `/PromptName`: ->lua - { - prompts = { - Yarrr = { - system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', - }, - NiceInstructions = { - system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.' .. require('CopilotChat.config.prompts').COPILOT_BASE.system_prompt, - } - } - } -< +- `/Explain` - Explain selected code +- `/Review` - Code review +- `/Fix` - Fix bugs +- `/Optimize` - Performance improvements +- `/Docs` - Add documentation +- `/Tests` - Generate tests +- `/Commit` - Commit message -STICKY PROMPTS ~ - -Sticky prompts persist across chat sessions. They’re useful for maintaining -model or resource selection. They work as follows: +============================================================================== +4. Configuration *CopilotChat-configuration* -1. Prefix text with `>` using markdown blockquote syntax -2. The prompt will be copied at the start of every new chat prompt -3. Edit sticky prompts freely while maintaining the `>` prefix +For all available configuration options, see `lua/CopilotChat/config.lua` +. -Examples: ->markdown - > #glob:`*.lua` - > List all files in the workspace - - > @models Using Mistral-small - > What is 1 + 11 -< +QUICK SETUP *CopilotChat-quick-setup* -You can also set default sticky prompts in the configuration: +Most users only need to configure a few options: >lua { - sticky = { - '#glob:*.lua', - } + model = 'gpt-4.1', -- AI model to use + temperature = 0.1, -- Lower = focused, higher = creative + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float' + width = 0.5, -- 50% of screen width + }, + auto_insert_mode = true, -- Enter insert mode when opening } < -MODELS *CopilotChat-models* +WINDOW & APPEARANCE *CopilotChat-window-&-appearance* -You can control which AI model to use in three ways: - -1. List available models with `:CopilotChatModels` -2. Set model in prompt with `$model_name` -3. Configure default model via `model` config key - -For supported models, see: - -- Copilot Chat Models -- GitHub Marketplace Models (experimental, limited usage) +>lua + { + window = { + layout = 'float', + width = 80, -- Fixed width in columns + height = 20, -- Fixed height in rows + border = 'rounded', -- 'single', 'double', 'rounded', 'solid' + title = '🤖 AI Assistant', + zindex = 100, -- Ensure window stays on top + }, + + headers = { + user = '👤 You: ', + assistant = '🤖 Copilot: ', + tool = '🔧 Tool: ', + }, + separator = '━━', + show_folds = false, -- Disable folding for cleaner look + } +< -FUNCTIONS *CopilotChat-functions* +BUFFER BEHAVIOR *CopilotChat-buffer-behavior* -Functions provide additional information and behaviour to the chat. Functions -can be organized into groups by setting the `group` property. Functions can be -made available to the LLM with `@group_name` or `@function_name` syntax. LLM -will then be able to use them in responses as tool calls. If function has URI, -they can also be used directly in prompt with `#function_name[:input]` syntax -for providing context as resources. +>lua + -- Auto-command to customize chat buffer behavior + vim.api.nvim_create_autocmd('BufEnter', { + pattern = 'copilot-*', + callback = function() + vim.opt_local.relativenumber = false + vim.opt_local.number = false + vim.opt_local.conceallevel = 0 + end, + }) +< - ------------------------------------------------------------------------------- - Function Input URI Description - Support - ------------- ------------ ----- ---------------------------------------------- - buffer ✓ (name) ✓ Retrieves content from a specific buffer - buffers ✓ (scope) ✓ Fetches content from multiple buffers - (listed/visible) +HIGHLIGHTS *CopilotChat-highlights* - diagnostics ✓ (scope) ✓ Collects code diagnostics (errors, warnings) +You can customize colors by setting highlight groups in your config: - file ✓ (path) ✓ Reads content from a specified file path +>lua + -- In your colorscheme or init.lua + vim.api.nvim_set_hl(0, 'CopilotChatHeader', { fg = '#7C3AED', bold = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { fg = '#374151' }) + vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { fg = '#10B981', italic = true }) +< - gitdiff ✓ (sha) ✓ Retrieves git diff information - (unstaged/staged/sha) +Types of copilot highlights: - gitstatus - ✓ Retrieves git status information +- `CopilotChatHeader` - Header highlight in chat buffer +- `CopilotChatSeparator` - Separator highlight in chat buffer +- `CopilotChatStatus` - Status and spinner in chat buffer +- `CopilotChatHelp` - Help messages in chat buffer (help, references) +- `CopilotChatSelection` - Selection highlight in source buffer +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) - glob ✓ (pattern) ✓ Lists filenames matching a pattern in - workspace - grep ✓ (pattern) ✓ Searches for a pattern across files in - workspace +PROMPTS *CopilotChat-prompts* - quickfix - ✓ Includes content of files in quickfix list +Define your own prompts in the configuration: - register ✓ (register) ✓ Provides access to specified Vim register +>lua + { + prompts = { + MyCustomPrompt = { + prompt = 'Explain how it works.', + system_prompt = 'You are very good at explaining stuff', + mapping = 'ccmc', + description = 'My custom prompt description', + }, + Yarrr = { + system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', + }, + NiceInstructions = { + system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.' .. require('CopilotChat.config.prompts').COPILOT_BASE.system_prompt, + } + } + } +< - url ✓ (url) ✓ Fetches content from a specified URL - ------------------------------------------------------------------------------- -Examples: ->markdown - > #buffer:init.lua - > #buffers:visible - > #diagnostics:current - > #file:path/to/file.js - > #git:staged - > #glob:`**/*.lua` - > #grep:`function setup` - > #quickfix - > #register:+ - > #url:https://example.com - > @glob - > @grep - > @file -< +FUNCTIONS *CopilotChat-functions* Define your own functions in the configuration with input handling and schema: @@ -418,52 +381,47 @@ Define your own functions in the configuration with input handling and schema: < -EXTERNAL FUNCTIONS ~ - -For external functions implementations, see the discussion page -. - - SELECTIONS *CopilotChat-selections* -Selections determine the source content for chat interactions. - -Available selections are located in `local select = -require("CopilotChat.select")`: - - Selection Description - ----------- -------------------------------------------------------- - visual Current visual selection - buffer Current buffer content - line Current line content - unnamed Unnamed register (last deleted/changed/yanked content) -You can set a default selection in the configuration: +Control what content is automatically included: >lua { - -- Uses visual selection or falls back to buffer + -- Use visual selection, fallback to current line selection = function(source) - return select.visual(source) or select.buffer(source) - end + return require('CopilotChat.select').visual(source) or + require('CopilotChat.select').line(source) + end, } < +**Available selections:** -PROVIDERS *CopilotChat-providers* - -Providers are modules that implement integration with different AI providers. - +- `require('CopilotChat.select').visual` - Current visual selection +- `require('CopilotChat.select').buffer` - Entire buffer content +- `require('CopilotChat.select').line` - Current line content +- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked) -BUILT-IN PROVIDERS ~ -- `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers +PROVIDERS *CopilotChat-providers* +Add custom AI providers: -PROVIDER INTERFACE ~ +>lua + { + providers = { + my_provider = { + get_url = function(opts) return "https://api.example.com/chat" end, + get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end, + get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end, + prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, + prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, + } + } + } +< -Custom providers can implement these methods: +**Provider Interface:** >lua { @@ -493,142 +451,11 @@ Custom providers can implement these methods: } < +**Built-in providers:** -EXTERNAL PROVIDERS ~ - -For external providers (Ollama, LM Studio, Mistral.ai), see the providers -discussion page -. - - -============================================================================== -4. Configuration *CopilotChat-configuration* - - -DEFAULT CONFIGURATION *CopilotChat-default-configuration* - -Below are all available configuration options with their default values: - ->lua - { - - -- Shared config starts here (can be passed to functions at runtime and configured via setup function) - - system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - - model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - language = 'English', -- Default language to use for answers - - resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) - - temperature = 0.1, -- Result temperature - headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember config as sticky prompts when asking questions - - -- default selection - -- see select.lua for implementation - selection = require('CopilotChat.select').visual, - - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout - 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 - blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent - }, - - 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 - 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) - - log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - - headers = { - user = '## User ', -- Header to use for user questions - assistant = '## Copilot ', -- Header to use for AI answers - tool = '## Tool ', -- Header to use for tool calls - }, - - separator = '───', -- Separator to use in chat - - -- default providers - -- see config/providers.lua for implementation - providers = require('CopilotChat.config.providers'), - - -- default functions - -- see config/functions.lua for implementation - functions = require('CopilotChat.config.functions'), - - -- default prompts - -- see config/prompts.lua for implementation - prompts = require('CopilotChat.config.prompts'), - - -- default mappings - -- see config/mappings.lua for implementation - mappings = require('CopilotChat.config.mappings'), - } -< - - -CUSTOMIZING BUFFERS *CopilotChat-customizing-buffers* - -Types of copilot buffers: - -- `copilot-chat` - Main chat buffer -- `copilot-overlay` - Overlay buffers (e.g. help, info, diff) - -You can set local options for plugin buffers like this: - ->lua - vim.api.nvim_create_autocmd('BufEnter', { - pattern = 'copilot-*', - callback = function() - -- Set buffer-local options - vim.opt_local.relativenumber = false - vim.opt_local.number = false - vim.opt_local.conceallevel = 0 - end - }) -< - - -CUSTOMIZING HIGHLIGHTS *CopilotChat-customizing-highlights* - -Types of copilot highlights: - -- `CopilotChatHeader` - Header highlight in chat buffer -- `CopilotChatSeparator` - Separator highlight in chat buffer -- `CopilotChatStatus` - Status and spinner in chat buffer -- `CopilotChatHelp` - Help messages in chat buffer (help, references) -- `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) -- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) +- `copilot` - GitHub Copilot (default) +- `github_models` - GitHub Marketplace models (disabled by default) +- `copilot_embeddings` - Copilot embeddings provider ============================================================================== @@ -801,9 +628,7 @@ Contributions of any kind are welcome! ============================================================================== 9. Links *CopilotChat-links* -1. *@jellydn*: -2. *@deathbeam*: -3. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg?variant=adaptive +1. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg?variant=adaptive Generated by panvimdoc From 8116b2d79bb335b8a7d3f19853b4271cd8ca6b06 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 15:51:32 +0200 Subject: [PATCH 456/728] docs(README): use table for prompts as well (#1254) * docs(README): use table for prompts as well 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 | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b0ea7e5e..22de3b98 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `:CopilotChatModels` | View/select available models | | `:CopilotChat` | Use specific prompt template | -## Key Mappings +## Chat Key Mappings | Insert | Normal | Action | | ----------- | ------- | ------------------------------------------ | @@ -152,7 +152,7 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | - | `gc` | Show info about current chat | | - | `gh` | Show help message | -## Built-in Functions +## Functions | Function | Description | Example Usage | | ------------- | ------------------------------------------------ | ---------------------- | @@ -168,17 +168,17 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `register` | Provides access to specified Vim register | `#register:+` | | `url` | Fetches content from a specified URL | `#url:https://...` | -## Predefined Prompts - -Use with `:CopilotChat` or reference with `/PromptName`: +## Prompts -- `/Explain` - Explain selected code -- `/Review` - Code review -- `/Fix` - Fix bugs -- `/Optimize` - Performance improvements -- `/Docs` - Add documentation -- `/Tests` - Generate tests -- `/Commit` - Commit message +| Prompt | Description | +| ---------- | ---------------------------------------------------------------------- | +| `Explain` | Write detailed explanation of selected code as paragraphs | +| `Review` | Comprehensive code review with line-specific issue reporting | +| `Fix` | Identify problems and rewrite code with fixes and explanation | +| `Optimize` | Improve performance and readability with optimization strategy | +| `Docs` | Add documentation comments to selected code | +| `Tests` | Generate tests for selected code | +| `Commit` | Generate commit message with commitizen convention from staged changes | # Configuration From 4eb9076dbaffdbabd919c3719346047175997c79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 13:51:52 +0000 Subject: [PATCH 457/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 743276dd..e5596682 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -13,9 +13,9 @@ Table of Contents *CopilotChat-table-of-contents* - Examples |CopilotChat-examples| 3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - - Key Mappings |CopilotChat-key-mappings| - - Built-in Functions |CopilotChat-built-in-functions| - - Predefined Prompts |CopilotChat-predefined-prompts| + - Chat Key Mappings |CopilotChat-chat-key-mappings| + - Functions |CopilotChat-functions| + - Prompts |CopilotChat-prompts| 4. Configuration |CopilotChat-configuration| - Quick Setup |CopilotChat-quick-setup| - Window & Appearance |CopilotChat-window-&-appearance| @@ -172,7 +172,7 @@ COMMANDS *CopilotChat-commands* :CopilotChatModels View/select available models :CopilotChat Use specific prompt template -KEY MAPPINGS *CopilotChat-key-mappings* +CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* Insert Normal Action ----------- -------- -------------------------------------------- @@ -191,7 +191,7 @@ KEY MAPPINGS *CopilotChat-key-mappings* - gc Show info about current chat - gh Show help message -BUILT-IN FUNCTIONS *CopilotChat-built-in-functions* +FUNCTIONS *CopilotChat-functions* ------------------------------------------------------------------------------ Function Description Example Usage @@ -223,18 +223,26 @@ BUILT-IN FUNCTIONS *CopilotChat-built-in-functions* url Fetches content from a specified URL #url:https://... ------------------------------------------------------------------------------ -PREDEFINED PROMPTS *CopilotChat-predefined-prompts* +PROMPTS *CopilotChat-prompts* + + ------------------------------------------------------------------------- + Prompt Description + ---------- -------------------------------------------------------------- + Explain Write detailed explanation of selected code as paragraphs + + Review Comprehensive code review with line-specific issue reporting + + Fix Identify problems and rewrite code with fixes and explanation + + Optimize Improve performance and readability with optimization strategy -Use with `:CopilotChat` or reference with `/PromptName`: + Docs Add documentation comments to selected code -- `/Explain` - Explain selected code -- `/Review` - Code review -- `/Fix` - Fix bugs -- `/Optimize` - Performance improvements -- `/Docs` - Add documentation -- `/Tests` - Generate tests -- `/Commit` - Commit message + Tests Generate tests for selected code + Commit Generate commit message with commitizen convention from staged + changes + ------------------------------------------------------------------------- ============================================================================== 4. Configuration *CopilotChat-configuration* From e1dc20c2e8b85f2133e4629203444825e87f15ea Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 16:11:47 +0200 Subject: [PATCH 458/728] docs(README): Fix duplicate headers (#1256) Closes #1255 Signed-off-by: Tomas Slusny --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 22de3b98..d70e31a4 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,9 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | - | `gc` | Show info about current chat | | - | `gh` | Show help message | -## Functions +## Predefined Functions + +All predefined functions belong to the `copilot` group. | Function | Description | Example Usage | | ------------- | ------------------------------------------------ | ---------------------- | @@ -168,7 +170,7 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `register` | Provides access to specified Vim register | `#register:+` | | `url` | Fetches content from a specified URL | `#url:https://...` | -## Prompts +## Predefined Prompts | Prompt | Description | | ---------- | ---------------------------------------------------------------------- | From 560d61c4da98da53cf35bdb2359ff6f76b3c12b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 14:12:05 +0000 Subject: [PATCH 459/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e5596682..c23f2808 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -14,8 +14,8 @@ Table of Contents *CopilotChat-table-of-contents* 3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Chat Key Mappings |CopilotChat-chat-key-mappings| - - Functions |CopilotChat-functions| - - Prompts |CopilotChat-prompts| + - Predefined Functions |CopilotChat-predefined-functions| + - Predefined Prompts |CopilotChat-predefined-prompts| 4. Configuration |CopilotChat-configuration| - Quick Setup |CopilotChat-quick-setup| - Window & Appearance |CopilotChat-window-&-appearance| @@ -191,7 +191,9 @@ CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* - gc Show info about current chat - gh Show help message -FUNCTIONS *CopilotChat-functions* +PREDEFINED FUNCTIONS *CopilotChat-predefined-functions* + +All predefined functions belong to the `copilot` group. ------------------------------------------------------------------------------ Function Description Example Usage @@ -223,7 +225,7 @@ FUNCTIONS *CopilotChat-functions* url Fetches content from a specified URL #url:https://... ------------------------------------------------------------------------------ -PROMPTS *CopilotChat-prompts* +PREDEFINED PROMPTS *CopilotChat-predefined-prompts* ------------------------------------------------------------------------- Prompt Description From 4d2586be38a6dbb07fec5d5f3d3335e973ea0ae1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 18:19:02 +0200 Subject: [PATCH 460/728] fix(functions): properly allow skipping handling for tools (#1257) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 67848dd8..163287b5 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -973,23 +973,40 @@ function M.ask(prompt, config) if not config.headless then utils.schedule_main() + -- Remove any tool calls that we did not handle + local assistant_message = M.chat:get_message('assistant') + if assistant_message and assistant_message.tool_calls then + local handled_ids = {} + for _, tool in ipairs(resolved_tools) do + handled_ids[tool.id] = true + end + + assistant_message.tool_calls = vim + .iter(assistant_message.tool_calls) + :filter(function(tool_call) + return handled_ids[tool_call.id] + end) + :totable() + end + if not utils.empty(resolved_tools) then + -- If we are handling tools, replace user message with tool results M.chat:remove_message('user') + for _, tool in ipairs(resolved_tools) do + M.chat:add_message({ + id = tool.id, + role = 'tool', + tool_call_id = tool.id, + content = '\n' .. tool.result .. '\n', + }) + end else + -- Otherwise just replace the user message with resolved prompt M.chat:add_message({ role = 'user', content = '\n' .. prompt .. '\n', }, true) end - - for _, tool in ipairs(resolved_tools) do - M.chat:add_message({ - id = tool.id, - role = 'tool', - tool_call_id = tool.id, - content = '\n' .. tool.result .. '\n', - }) - end end if utils.empty(prompt) and utils.empty(resolved_tools) then From bad83db89bb3d813be62dd1b2767406ac3c96e4c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 18:30:10 +0200 Subject: [PATCH 461/728] fix(chat): handle empty prompt and tools before ask (#1258) Move the check for empty prompt and resolved tools before making the ask call. This ensures that unnecessary user messages are removed and the finish callback is called early, preventing redundant processing. Also remove assistant message if all tool calls are handled. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 163287b5..d2dbee0c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -969,10 +969,17 @@ function M.ask(prompt, config) end prompt = vim.trim(prompt) + utils.schedule_main() - if not config.headless then - utils.schedule_main() + if utils.empty(prompt) and utils.empty(resolved_tools) then + if not config.headless then + M.chat:remove_message('user') + finish() + end + return + end + if not config.headless then -- Remove any tool calls that we did not handle local assistant_message = M.chat:get_message('assistant') if assistant_message and assistant_message.tool_calls then @@ -987,6 +994,10 @@ function M.ask(prompt, config) return handled_ids[tool_call.id] end) :totable() + + if utils.empty(assistant_message.tool_calls) then + M.chat:remove_message('assistant') + end end if not utils.empty(resolved_tools) then @@ -1009,14 +1020,6 @@ function M.ask(prompt, config) end end - if utils.empty(prompt) and utils.empty(resolved_tools) then - if not config.headless then - M.chat:remove_message('user') - finish() - end - return - end - local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, history = M.chat.messages, From 936426a500d2f0da25f7d3f065e07450ac851c66 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 18:51:36 +0200 Subject: [PATCH 462/728] fix(chat): handle skipped tool calls with explicit error result (#1259) Previously, unhandled tool calls were simply removed from the assistant message, which could lead to confusion or lack of feedback. Now, when a tool call is skipped, an explicit error result is added to the resolved tools, indicating that the user skipped the function call. This improves transparency and makes the handling of skipped tool calls clearer. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d2dbee0c..4e81c934 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -971,16 +971,7 @@ function M.ask(prompt, config) prompt = vim.trim(prompt) utils.schedule_main() - if utils.empty(prompt) and utils.empty(resolved_tools) then - if not config.headless then - M.chat:remove_message('user') - finish() - end - return - end - if not config.headless then - -- Remove any tool calls that we did not handle local assistant_message = M.chat:get_message('assistant') if assistant_message and assistant_message.tool_calls then local handled_ids = {} @@ -988,15 +979,15 @@ function M.ask(prompt, config) handled_ids[tool.id] = true end - assistant_message.tool_calls = vim - .iter(assistant_message.tool_calls) - :filter(function(tool_call) - return handled_ids[tool_call.id] - end) - :totable() - - if utils.empty(assistant_message.tool_calls) then - M.chat:remove_message('assistant') + -- If we skipped any tool calls, send that as result + for _, tool_call in ipairs(assistant_message.tool_calls) do + if not handled_ids[tool_call.id] then + table.insert(resolved_tools, { + id = tool_call.id, + result = string.format(BLOCK_OUTPUT_FORMAT, 'error', 'User skipped this function call.'), + }) + handled_ids[tool_call.id] = true + end end end @@ -1020,6 +1011,14 @@ function M.ask(prompt, config) end end + if utils.empty(prompt) and utils.empty(resolved_tools) then + if not config.headless then + M.chat:remove_message('user') + finish() + end + return + end + local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, history = M.chat.messages, From 95d4d7ade6b0ae8acee77f291228109dc37602cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 06:34:55 +0200 Subject: [PATCH 463/728] chore(main): release 4.0.0 (#1191) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ version.txt | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10dde051..12e31352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # Changelog +## [4.0.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.2...v4.0.0) (2025-08-02) + + +### ⚠ BREAKING CHANGES + +* **mappings:** use C-Space as default completion trigger instead of Tab +* **providers:** github_models provider is now disabled by default, enable with `providers.github_models.disabled = false` +* **resources:** intelligent resource processing is now disabled by default, use config.resource_processing: true to reenable +* **context:** Multiple breaking changes due to big refactor: + - The context API has changed from callback-based input handling to schema-based definitions. + - config.contexts renamed to config.tools + - config.context removed, use config.sticky + - diagnostics moved to separate tool call, selection and buffer calls no longer include them by default + - gi renamed to gc, now also includes selection + - filenames renamed to glob + - files removed (use glob together with tool calling instead, or buffers/quickfix) + - copilot extension agents removed, tools + mcp servers can replace this feature and maintaining them was pain, they can still be implemented via custom providers anyway + - actions and integrations action removed as they were deprecated for a while + - config.questionHeader, config.answerHeader moved to config.headers.user/config.headers.assistant + +### Features + +* add Windows_NT support in Makefile and dynamic library loading ([#1190](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1190)) ([7559fd2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7559fd25928f8f3cf311ff25b95bdc5f9ec736d7)) +* **context:** switch from contexts to function calling ([057b8e4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/057b8e46d955748b1426e7b174d7af3e58f5191b)), closes [#1045](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1045) [#1090](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1090) [#1096](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1096) [#526](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/526) +* display group as kind when listing resources ([#1215](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1215)) ([450fcec](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/450fcecf2f71d0469e9c98f5967252092714ed03)) +* **functions:** automatically parse schema from url templates ([#1220](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1220)) ([950fdb6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/950fdb6ab56754929d4db91c73139b33e645deec)) +* **health:** add temp dir writable check ([#1239](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1239)) ([02cf9e5](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/02cf9e52634b3e3d45beb2c4e5bbc17da28aef64)) +* **mappings:** use C-Space as default completion trigger instead of Tab ([ea41684](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/ea4168476a0fdbd5bf40a4a769d6c1dc998929eb)) +* **prompts:** add configurable response language ([#1246](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1246)) ([ced388c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/ced388c97b313ea235809824ed501970b155e59f)), closes [#1086](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1086) +* **providers:** add info output to panel for copilot with stats ([#1229](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1229)) ([1713ce6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1713ce6c8ec700a7833236a8dadfae8a0742b14d)) +* **providers:** new github models api, in-built authorization without copilot.vim dep ([#1218](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1218)) ([9c4501e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9c4501e7ae92020f2d9b828086016ee70e7fa52c)), closes [#1140](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1140) +* **providers:** prioritize gh clie auth if available for github models ([#1240](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1240)) ([01d38b2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/01d38b27ea2183302c743dac09b27611d09d7591)) +* **resources:** add option to enable resource processing ([#1202](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1202)) ([6ac77aa](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/6ac77aaa68a0ce7fe3c8c41622ab1986f8f6d2c7)) +* **ui:** add window.blend option for controllin float transparency ([#1227](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1227)) ([a01bbd6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a01bbd6779f4bee23c29ebcfe0d2f5fa5664b5bf)), closes [#1126](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1126) +* **ui:** highlight copilotchat keywords ([#1225](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1225)) ([8071a69](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/8071a6979b5569ce03f7f4d7192814da4c2d4e0b)) +* **ui:** improve chat responsiveness by starting spinner early ([#1205](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1205)) ([9d9b280](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9d9b2809e1240f9525752ae145799b88d22cd7af)) + + +### Bug Fixes + +* add back sticky loading on opening window ([#1210](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1210)) ([1d6911f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1d6911fef13952c9b56347485f090baeff77a7e4)) +* **chat:** do not allow sending empty prompt ([#1245](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1245)) ([c3d0048](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c3d00484c42065a883db0fb859c686e277012d6c)), closes [#1189](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1189) +* **chat:** handle empty prompt and tools before ask ([#1258](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1258)) ([bad83db](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/bad83db89bb3d813be62dd1b2767406ac3c96e4c)) +* **chat:** handle skipped tool calls with explicit error result ([#1259](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1259)) ([936426a](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/936426a500d2f0da25f7d3f065e07450ac851c66)) +* **chat:** highlight keywords only in user messages ([#1236](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1236)) ([425ff0c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/425ff0c48906a94ca522f6d2e98e4b39057e4fd4)) +* **chat:** improve how sticky prompts are stored and parsed ([#1233](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1233)) ([82be513](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/82be513c07a27f55860d55144c54040d1c93cf2a)) +* **chat:** properly replace all message data when replacing message ([#1244](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1244)) ([d1d155e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d1d155e50193e28a3ec00f8e21d6f11445f96ea1)) +* **chat:** properly reset modifiable after modifying it ([#1234](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1234)) ([fc93d1c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/fc93d1c535bf9538a0a036f118b1034930ee5eb9)) +* **chat:** show messages in overlay ([#1237](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1237)) ([1a17534](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1a17534c17e6ae9f5417df08b8c0eec434c47875)) +* check for explicit uri input properly ([#1214](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1214)) ([b738fb4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b738fb40de3a4bcbb835b8ff6ab2d171acc5d2dd)) +* **files:** use also plenary filetype on top of vim.filetype.match ([#1250](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1250)) ([9fd068f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9fd068f5d6a0ca00fc739a98f29125cb577b2dfa)), closes [#1249](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1249) +* **functions:** change neovim://buffer to just buffer:// to avoid conflicts ([#1252](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1252)) ([3509cf0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/3509cf0971c59ba79fbcd618d82910f8567a7929)) +* **functions:** if enum returns only 1 choice auto accept it ([#1209](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1209)) ([e632470](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/e632470171cd82a95c2675360120833c159e7ae0)) +* **functions:** if schema.properties is empty, do not send schema ([#1211](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1211)) ([8a5cda1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/8a5cda1d90c4d4756dda39cfd748e52cbcde5a99)) +* **functions:** properly allow skipping handling for tools ([#1257](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1257)) ([4d2586b](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/4d2586be38a6dbb07fec5d5f3d3335e973ea0ae1)) +* **functions:** properly escape percent signs in uri inputs ([#1212](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1212)) ([d905917](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d905917a025e4c056db28b3082dd474475bad8cd)) +* **functions:** properly filter tool schema from functions ([#1243](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1243)) ([f7a3228](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f7a3228f155d0533197ac79b0e08582e504d0399)) +* **functions:** properly handle multiple tool calls at once ([#1198](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1198)) ([dd06166](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/dd0616661505a3c4892ddcdb9517b720a74e59b8)) +* **functions:** properly resolve defaults for diagnostics ([#1201](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1201)) ([946069a](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/946069a03946ce35619cbacc3a6757819d096ac5)), closes [#1200](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1200) +* **functions:** properly send prompt as 3rd function resolve param ([#1221](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1221)) ([c03bd1d](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c03bd1df78b276aa5be2f173c2a31ad273164f15)) +* **functions:** use vim.filetype.match for non bulk file reads ([#1226](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1226)) ([b124b94](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b124b94264140a5d352512b38b7a46d85ee59b24)), closes [#1181](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1181) +* **healthcheck:** chance copilot.vim dependency to optional ([#1219](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1219)) ([d9f4e29](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d9f4e29c3b46b827443b1832209d22d05c1a69af)) +* **prompt:** be more specific when definining what is resource ([#1238](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1238)) ([7c82936](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7c82936f2126b106af1b1bf0f9ae4d42dd45fcad)) +* properly validate source window when retrieving cwd ([#1231](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1231)) ([f53069c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f53069c595a3b12bbe8b9b711917f9ef33c22a0a)), closes [#1230](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1230) +* **providers:** do not save copilot.vim token ([#1223](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1223)) ([294bcb6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/294bcb620ff66183e142cd8a43a7c77d5bc77a16)) +* **quickfix:** use new chat messages instead of old chat sections for populating qf ([#1199](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1199)) ([e0df6d1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/e0df6d1242af29b6262b0eb3e4248568c57c4b3e)) +* **ui:** do not allow empty separator ([#1224](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1224)) ([67ed258](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/67ed258c6ccc0a9bfbb6dfcbe3d5e19e22888e73)) +* **ui:** fix check for auto follow cursor ([#1222](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1222)) ([1f96d53](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1f96d53c3f10f176ca25065a23e610d7b4a72b99)) +* update sticky reference for commit messages ([#1207](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1207)) ([dab5089](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/dab50896c7e1e80142dd297e6fc75590735b3e9c)) +* update to latest lua actions and update README ([#1196](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1196)) ([b4b7f9c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b4b7f9c2bb34d43b18dbbe0a889881630e217bc3)) +* **utils:** remove temp file after curl request is done ([#1235](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1235)) ([dec3127](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/dec3127e4f373875d7fd50854e221ed8dc0e061f)), closes [#1194](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1194) + ## [3.12.2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.1...v3.12.2) (2025-07-09) diff --git a/version.txt b/version.txt index 8531a3b7..fcdb2e10 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.12.2 +4.0.0 From 4fdf89a30366da3189ae20be67b5b315773e6d45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 3 Aug 2025 04:35:14 +0000 Subject: [PATCH 464/728] 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 c23f2808..d45bb55d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 02 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 03 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 0d64e267a5aef3bd7d580a2c488bcc8b66d374a4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 3 Aug 2025 08:19:48 +0200 Subject: [PATCH 465/728] feat(ui): improve keyword highlights accuracy and performance (#1260) Signed-off-by: Tomas Slusny --- README.md | 8 ++++++-- lua/CopilotChat/ui/chat.lua | 19 +++---------------- plugin/CopilotChat.lua | 20 ++++++++++++++++++-- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d70e31a4..c35320a4 100644 --- a/README.md +++ b/README.md @@ -255,9 +255,13 @@ Types of copilot highlights: - `CopilotChatHeader` - Header highlight in chat buffer - `CopilotChatSeparator` - Separator highlight in chat buffer - `CopilotChatStatus` - Status and spinner in chat buffer -- `CopilotChatHelp` - Help messages in chat buffer (help, references) +- `CopilotChatHelp` - Help text in chat buffer +- `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) +- `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`) +- `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) +- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) +- `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) ## Prompts diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index fe70feff..8b22c32e 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -340,6 +340,9 @@ function Chat:open(config) vim.wo[self.winnr].foldcolumn = '0' end + local ns = vim.api.nvim_create_namespace('copilot-chat-local-hl') + vim.api.nvim_set_hl(ns, '@markup.quote.markdown', {}) + vim.api.nvim_win_set_hl_ns(self.winnr, ns) vim.api.nvim_win_set_buf(self.winnr, self.bufnr) self:render() end @@ -678,22 +681,6 @@ function Chat:render() end end - -- Keywords - if current_message and current_message.role == 'user' then - -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it - local patterns = { - '()#?#[^ ]+()', - '()@[^ ]+()', - '()%$[^ ]+()', - '()/[^ ]+()', - } - for _, pattern in ipairs(patterns) do - for s, e in line:gmatch(pattern) do - vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) - end - end - end - -- If last line, finish last message if l == #lines and current_message then current_message.section.end_line = l diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index de5e0158..e59ee49e 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -16,10 +16,14 @@ local function setup_highlights() vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) - vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { link = 'Keyword', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatResource', { link = 'Constant', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatTool', { link = 'Function', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatPrompt', { link = 'Statement', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatModel', { link = 'Type', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatUri', { link = 'Underlined', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) - vim.api.nvim_set_hl(0, 'CopilotChatAnnotation', { link = 'ColorColumn', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatAnnotation', { link = 'ColorColumn', default = true }) local fg = vim.api.nvim_get_hl(0, { name = 'CopilotChatStatus', link = false }).fg local bg = vim.api.nvim_get_hl(0, { name = 'CopilotChatAnnotation', link = false }).bg vim.api.nvim_set_hl(0, 'CopilotChatAnnotationHeader', { fg = fg, bg = bg }) @@ -75,6 +79,18 @@ vim.api.nvim_create_user_command('CopilotChatReset', function() chat.reset() end, { force = true }) +vim.api.nvim_create_autocmd('FileType', { + pattern = 'copilot-chat', + group = group, + callback = vim.schedule_wrap(function() + vim.cmd.syntax('match CopilotChatResource "#\\S\\+"') + vim.cmd.syntax('match CopilotChatTool "@\\S\\+"') + vim.cmd.syntax('match CopilotChatPrompt "/\\S\\+"') + vim.cmd.syntax('match CopilotChatModel "\\$\\S\\+"') + vim.cmd.syntax('match CopilotChatUri "##\\S\\+"') + end), +}) + local function complete_load() local chat = require('CopilotChat') local options = vim.tbl_map(function(file) From 831db05de8ba7a9211ae40ccffb2c8a978202c3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 3 Aug 2025 06:20:07 +0000 Subject: [PATCH 466/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index d45bb55d..f49db7f1 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -325,9 +325,13 @@ Types of copilot highlights: - `CopilotChatHeader` - Header highlight in chat buffer - `CopilotChatSeparator` - Separator highlight in chat buffer - `CopilotChatStatus` - Status and spinner in chat buffer -- `CopilotChatHelp` - Help messages in chat buffer (help, references) +- `CopilotChatHelp` - Help text in chat buffer +- `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) +- `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`) +- `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) +- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) +- `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) From 8510f30ff8c338482e7c8a2a7d102519cc57315f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 3 Aug 2025 17:34:17 +0200 Subject: [PATCH 467/728] fix(functions): do not filter schema enum when entering input (#1264) It still need to be filtered when preparing tool use as we cant send json functions to API. Closes #1263 Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 6e936a3d..e63b63a3 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -114,15 +114,16 @@ function M.match_uri(uri, pattern) return result end ----@param tool CopilotChat.config.functions.Function -function M.parse_schema(tool) - local schema = tool.schema +--- Parse function schema and return a JSON schema object +---@param fn CopilotChat.config.functions.Function +function M.parse_schema(fn) + local schema = fn.schema -- If schema is missing but uri is present, generate a default schema from uri - if not schema and tool.uri then + if not schema and fn.uri then -- Extract parameter names from the uri pattern, e.g. file://{path} local param_names = {} - for param in tool.uri:gmatch(URI_PARAM_PATTERN) do + for param in fn.uri:gmatch(URI_PARAM_PATTERN) do table.insert(param_names, param) end if #param_names > 0 then @@ -138,26 +139,22 @@ function M.parse_schema(tool) end end - if schema then - schema = filter_schema(schema, true) - end - return schema end ---- Prepare the schema for use ----@param tools table +--- Prepare functions for tool use +---@param functions table ---@return table -function M.parse_tools(tools) - local tool_names = vim.tbl_keys(tools) +function M.parse_tools(functions) + local tool_names = vim.tbl_keys(functions) table.sort(tool_names) return vim.tbl_map(function(name) - local tool = tools[name] + local tool = functions[name] return { name = name, description = tool.description, - schema = M.parse_schema(tool), + schema = filter_schema(M.parse_schema(tool), true), } end, tool_names) end From 1923ad3d6fd88f756fba46b5c04df4a4addab23f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 17:36:34 +0200 Subject: [PATCH 468/728] chore(main): release 4.1.0 (#1261) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ version.txt | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e31352..d3a05ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [4.1.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.0.0...v4.1.0) (2025-08-03) + + +### Features + +* **ui:** improve keyword highlights accuracy and performance ([#1260](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1260)) ([0d64e26](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/0d64e267a5aef3bd7d580a2c488bcc8b66d374a4)) + + +### Bug Fixes + +* **functions:** do not filter schema enum when entering input ([#1264](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1264)) ([8510f30](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/8510f30ff8c338482e7c8a2a7d102519cc57315f)), closes [#1263](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1263) + ## [4.0.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.2...v4.0.0) (2025-08-02) diff --git a/version.txt b/version.txt index fcdb2e10..ee74734a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.0.0 +4.1.0 From 5c8b457d617dd1e533b826ff9f9b76ddf988756d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 3 Aug 2025 22:17:25 +0200 Subject: [PATCH 469/728] feat(chat): improve error handling (#1265) Gracefully handle errors as they come instead of manually calling show_error method and then just catch all the errors at once place --- lua/CopilotChat/init.lua | 70 ++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 4e81c934..e72eed5e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -204,17 +204,32 @@ local function finish(start_of_chat) end --- Show an error in the chat window. ----@param err string|table|nil -local function show_error(err) - err = err or 'Unknown error' - err = utils.make_string(err) +---@param config CopilotChat.config.Shared +---@param cb function +---@return any +local function handle_error(config, cb) + return function() + local ok, out = pcall(cb) + if ok then + return out + end - M.chat:add_message({ - role = 'assistant', - content = '\n' .. string.format(BLOCK_OUTPUT_FORMAT, 'error', err) .. '\n', - }) + log.error(out) + if config.headless then + return + end + + utils.schedule_main() + out = out or 'Unknown error' + out = utils.make_string(out) + + M.chat:add_message({ + role = 'assistant', + content = '\n' .. string.format(BLOCK_OUTPUT_FORMAT, 'error', out) .. '\n', + }) - finish() + finish() + end end --- Map a key to a function. @@ -954,7 +969,7 @@ function M.ask(prompt, config) -- Retrieve the selection local selection = M.get_selection() - local ok, err = pcall(async.run, function() + async.run(handle_error(config, function() local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) @@ -969,9 +984,9 @@ function M.ask(prompt, config) end prompt = vim.trim(prompt) - utils.schedule_main() if not config.headless then + utils.schedule_main() local assistant_message = M.chat:get_message('assistant') if assistant_message and assistant_message.tool_calls then local handled_ids = {} @@ -1019,7 +1034,7 @@ function M.ask(prompt, config) return end - local ask_ok, ask_response = pcall(client.ask, client, prompt, { + local ask_response = client.ask(client, prompt, { headless = config.headless, history = M.chat.messages, selection = selection, @@ -1038,16 +1053,6 @@ function M.ask(prompt, config) end), }) - utils.schedule_main() - - if not ask_ok then - log.error(ask_response) - if not config.headless then - show_error(ask_response) - end - return - end - -- If there was no error and no response, it means job was cancelled if ask_response == nil then return @@ -1059,14 +1064,8 @@ function M.ask(prompt, config) -- Call the callback function if config.callback then - local callback_ok, callback_response = pcall(config.callback, response.content, state.source) - if not callback_ok then - log.error('Callback error: ' .. callback_response) - if not config.headless then - show_error(callback_response) - end - return - end + utils.schedule_main() + config.callback(response.content, state.source) end if not config.headless then @@ -1076,19 +1075,14 @@ function M.ask(prompt, config) else response.content = '\n' .. response.content .. '\n' end + + utils.schedule_main() M.chat:add_message(response, true) M.chat.token_count = token_count M.chat.token_max_count = token_max_count finish() end - end) - - if not ok then - log.error(err) - if not config.headless then - show_error(err) - end - end + end)) end --- Stop current copilot output and optionally reset the chat ten show the help message. From 20b493ea8dee0589703e7e9fe24019f7d2649be1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:08:33 +0200 Subject: [PATCH 470/728] chore(main): release 4.2.0 (#1266) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3a05ec4..19bf9a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [4.2.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.1.0...v4.2.0) (2025-08-03) + + +### Features + +* **chat:** improve error handling ([#1265](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1265)) ([5c8b457](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/5c8b457d617dd1e533b826ff9f9b76ddf988756d)) + ## [4.1.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.0.0...v4.1.0) (2025-08-03) diff --git a/version.txt b/version.txt index ee74734a..6aba2b24 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.1.0 +4.2.0 From 405728b708cce7359a333b059b744ca176e6c42f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 Aug 2025 00:08:50 +0000 Subject: [PATCH 471/728] 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 f49db7f1..8cfb114b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 03 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 04 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 80f797c3a12e188908e25a3182d1e0a285179abd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 03:02:47 +0200 Subject: [PATCH 472/728] docs: mention selection in core concepts (#1268) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c35320a4..3b54fa3b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ EOF - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks +- **Selection** - Automatically includes current user selection in prompts ## Examples From ffbd2344c99d986b4f415b25e9a60d09f17872a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 Aug 2025 01:03:08 +0000 Subject: [PATCH 473/728] 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 8cfb114b..37524639 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -128,6 +128,7 @@ VIM-PLUG *CopilotChat-vim-plug* - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks +- **Selection** - Automatically includes current user selection in prompts EXAMPLES *CopilotChat-examples* From f38319fd8f3a7aaa1f75b78027032f9c07abc425 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 14:21:33 +0200 Subject: [PATCH 474/728] refactor(core): remove resource processing and embeddings (#1203) This change removes the resource processing and embeddings logic from the plugin, simplifying the codebase and reducing complexity. All related functions, configuration options, and provider logic for embeddings have been deleted. The plugin now relies solely on token count for resource management. This should make maintenance easier and improve user experience by reducing unnecessary processing. BREAKING CHANGE: Resource processing and embeddings support have been removed. Any configuration or usage relying on these features will no longer work. Signed-off-by: Tomas Slusny --- README.md | 4 - lua/CopilotChat/client.lua | 136 -------- lua/CopilotChat/config.lua | 3 - lua/CopilotChat/config/providers.lua | 27 -- lua/CopilotChat/init.lua | 10 - lua/CopilotChat/resources.lua | 460 --------------------------- lua/CopilotChat/utils.lua | 47 --- 7 files changed, 687 deletions(-) diff --git a/README.md b/README.md index 3b54fa3b..d7e7cfb2 100644 --- a/README.md +++ b/README.md @@ -369,9 +369,6 @@ Add custom AI providers: -- Optional: Disable provider disabled?: boolean, - -- Optional: Embeddings provider name or function - embed?: string|function, - -- Optional: Extra info about the provider displayed in info panel get_info?(): string[] @@ -396,7 +393,6 @@ Add custom AI providers: - `copilot` - GitHub Copilot (default) - `github_models` - GitHub Marketplace models (disabled by default) -- `copilot_embeddings` - Copilot embeddings provider # API Reference diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 11c353b6..4fd30197 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -31,17 +31,11 @@ ---@field description string description of the tool ---@field schema table? schema of the tool ----@class CopilotChat.client.Embed ----@field index number ----@field embedding table - ---@class CopilotChat.client.Resource ---@field name string ---@field type string ---@field data string ----@class CopilotChat.client.EmbeddedResource : CopilotChat.client.Resource, CopilotChat.client.Embed - ---@class CopilotChat.client.Model ---@field provider string? ---@field id string @@ -60,44 +54,6 @@ local class = utils.class --- Constants local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' -local LINE_CHARACTERS = 100 -local BIG_EMBED_THRESHOLD = 200 * LINE_CHARACTERS - ---- Resolve provider function ----@param model string ----@param models table ----@param providers table ----@return string, function -local function resolve_provider_function(name, model, models, providers) - local model_config = models[model] - if not model_config then - error('Model not found: ' .. model) - end - - local provider_name = model_config.provider - if not provider_name then - error('Provider not found for model: ' .. model) - end - local provider = providers[provider_name] - if not provider then - error('Provider not found: ' .. provider_name) - end - - local func = provider[name] - if type(func) == 'string' then - provider_name = func - provider = providers[provider_name] - if not provider then - error('Provider not found: ' .. provider_name) - end - func = provider[name] - end - if not func then - error('Function not found: ' .. name) - end - - return provider_name, func -end --- Generate content block with line numbers, truncating if necessary ---@param content string @@ -202,17 +158,6 @@ local function generate_ask_request(prompt, system_prompt, history, generated_me return messages end ---- Generate embedding request ---- @param inputs table ---- @param threshold number ---- @return table -local function generate_embedding_request(inputs, threshold) - return vim.tbl_map(function(embedding) - local content = generate_content_block(embedding.data, threshold) - return string.format(RESOURCE_FORMAT, embedding.name, embedding.type, content) - end, inputs) -end - ---@class CopilotChat.client.Client : Class ---@field private providers table ---@field private provider_cache table @@ -602,87 +547,6 @@ function Client:ask(prompt, opts) } end ---- Generate embeddings for the given inputs ----@param inputs table: The inputs to embed ----@param model string ----@return table -function Client:embed(inputs, model) - if not inputs or #inputs == 0 then - ---@diagnostic disable-next-line: return-type-mismatch - return inputs - end - - local models = self:models() - local ok, provider_name, embed = pcall(resolve_provider_function, 'embed', model, models, self.providers) - if not ok then - ---@diagnostic disable-next-line: return-type-mismatch - return inputs - end - - notify.publish(notify.STATUS, 'Generating embeddings for ' .. #inputs .. ' inputs') - - -- Initialize essentials - local to_process = inputs - local results = {} - local initial_chunk_size = 10 - - -- Process inputs in batches with adaptive chunk size - while #to_process > 0 do - local chunk_size = initial_chunk_size -- Reset chunk size for each new batch - local threshold = BIG_EMBED_THRESHOLD -- Reset threshold for each new batch - local last_error = nil - - -- Take next chunk - local batch = {} - for _ = 1, math.min(chunk_size, #to_process) do - table.insert(batch, table.remove(to_process, 1)) - end - - -- 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 ok, data = pcall(embed, generate_embedding_request(batch, threshold), self:authenticate(provider_name)) - - if not ok then - log.debug('Failed to get embeddings: ', data) - last_error = data - attempts = attempts + 1 - -- If we have few items and the request failed, try reducing threshold first - if #batch <= 5 then - 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 - 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 - for _, embedding in ipairs(data) do - local result = vim.tbl_extend('force', batch[embedding.index + 1], embedding) - table.insert(results, result) - end - end - end - - if not success then - error(last_error) - end - end - - return results -end - --- Stop the running job ---@return boolean function Client:stop() diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 30ad2bd8..48f5e96a 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -19,7 +19,6 @@ ---@field tools string|table|nil ---@field sticky string|table|nil ---@field language string? ----@field resource_processing boolean? ---@field temperature number? ---@field headless boolean? ---@field callback nil|fun(response: string, source: CopilotChat.source) @@ -61,8 +60,6 @@ return { sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). language = 'English', -- Default language to use for answers - resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) - temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index d2ac7976..5356925a 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -200,7 +200,6 @@ end ---@field get_headers nil|fun():table,number? ---@field get_info nil|fun(headers:table):string[] ---@field get_models nil|fun(headers:table):table ----@field embed nil|string|fun(inputs:table, headers:table):table ---@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table ---@field prepare_output nil|fun(output:table, opts:CopilotChat.config.providers.Options):CopilotChat.config.providers.Output ---@field get_url nil|fun(opts:CopilotChat.config.providers.Options):string @@ -209,8 +208,6 @@ end local M = {} M.copilot = { - embed = 'copilot_embeddings', - get_headers = function() local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, @@ -453,7 +450,6 @@ M.copilot = { M.github_models = { disabled = true, - embed = 'copilot_embeddings', get_headers = function() return { @@ -498,27 +494,4 @@ M.github_models = { end, } -M.copilot_embeddings = { - get_headers = M.copilot.get_headers, - - embed = function(inputs, headers) - local response, err = utils.curl_post('https://api.githubcopilot.com/embeddings', { - headers = headers, - json_request = true, - json_response = true, - body = { - dimensions = 512, - input = inputs, - model = 'text-embedding-3-small', - }, - }) - - if err then - error(err) - end - - return response.body.data - end, -} - return M diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e72eed5e..10b8bd10 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -973,16 +973,6 @@ function M.ask(prompt, config) local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) - if config.resource_processing then - local query_ok, processed_resources = - pcall(resources.process_resources, prompt, selected_model, resolved_resources) - if query_ok then - resolved_resources = processed_resources - else - log.warn('Failed to process resources', processed_resources) - end - end - prompt = vim.trim(prompt) if not config.headless then diff --git a/lua/CopilotChat/resources.lua b/lua/CopilotChat/resources.lua index da79d6ec..e3ec443a 100644 --- a/lua/CopilotChat/resources.lua +++ b/lua/CopilotChat/resources.lua @@ -8,385 +8,12 @@ ---@field end_col number local async = require('plenary.async') -local log = require('plenary.log') -local client = require('CopilotChat.client') -local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local file_cache = {} local url_cache = {} -local embedding_cache = {} -local outline_cache = {} local M = {} -local OUTLINE_TYPES = { - 'local_function', - 'function_item', - 'arrow_function', - 'function_definition', - 'function_declaration', - 'method_definition', - 'method_declaration', - 'proc_declaration', - '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', - 'atx_heading', - 'list_item', -} - -local NAME_TYPES = { - 'name', - 'identifier', - 'heading_content', -} - -local OFF_SIDE_RULE_LANGUAGES = { - 'python', - 'coffeescript', - 'nim', - 'elm', - 'curry', - 'fsharp', -} - -local MULTI_FILE_THRESHOLD = 5 - ---- Compute the cosine similarity between two vectors ----@param a table ----@param b table ----@return number -local function spatial_distance_cosine(a, b) - if not a or not b then - return 0 - end - - local dot_product = 0 - local magnitude_a = 0 - local magnitude_b = 0 - for i = 1, #a do - dot_product = dot_product + a[i] * b[i] - magnitude_a = magnitude_a + a[i] * a[i] - magnitude_b = magnitude_b + b[i] * b[i] - end - magnitude_a = math.sqrt(magnitude_a) - magnitude_b = math.sqrt(magnitude_b) - return dot_product / (magnitude_a * magnitude_b) -end - ---- Rank data by relatedness to the query ----@param query CopilotChat.client.EmbeddedResource ----@param data table ----@return table -local function data_ranked_by_relatedness(query, data) - for _, item in ipairs(data) do - local score = spatial_distance_cosine(item.embedding, query.embedding) - item.score = score or item.score or 0 - end - - table.sort(data, function(a, b) - return a.score > b.score - end) - - -- Apply dynamic filtering for embedding-based ranking - local filtered = {} - - if #data > 0 then - -- Calculate statistics for score distribution - local sum = 0 - local max_score = data[1].score - - for _, item in ipairs(data) do - sum = sum + item.score - end - - local mean = sum / #data - - -- Calculate standard deviation - local sum_squared_diff = 0 - for _, item in ipairs(data) do - sum_squared_diff = sum_squared_diff + ((item.score - mean) * (item.score - mean)) - end - local std_dev = math.sqrt(sum_squared_diff / #data) - - -- Calculate z-scores and use them to determine significance - -- Include items with z-score > -0.5 (meaning within 0.5 std dev below mean) - -- This is a statistical approach to find "significantly" related items - for _, result in ipairs(data) do - local z_score = (result.score - mean) / std_dev - if z_score > -0.5 then - table.insert(filtered, result) - end - end - - -- If we didn't get enough results or the distribution is very tight, - -- use a percentage of max score as fallback - if #filtered < MULTI_FILE_THRESHOLD then - filtered = {} - local adaptive_threshold = max_score * 0.6 -- 60% of max score - - for i, result in ipairs(data) do - if i <= MULTI_FILE_THRESHOLD or result.score >= adaptive_threshold then - table.insert(filtered, result) - end - end - end - end - - return filtered -end - --- Create trigrams from text (e.g., "hello" -> {"hel", "ell", "llo"}) -local function get_trigrams(text) - local trigrams = {} - text = text:lower() - for i = 1, #text - 2 do - trigrams[text:sub(i, i + 2)] = true - end - return trigrams -end - --- Calculate Jaccard similarity between two trigram sets -local function trigram_similarity(set1, set2) - local intersection = 0 - local union = 0 - - -- Count intersection and union - for trigram in pairs(set1) do - if set2[trigram] then - intersection = intersection + 1 - end - union = union + 1 - end - - for trigram in pairs(set2) do - if not set1[trigram] then - union = union + 1 - end - end - - return intersection / union -end - ---- Rank data by symbols and filenames ----@param query string ----@param data table ----@return table -local function data_ranked_by_symbols(query, data) - -- Get query trigrams including compound versions - local query_trigrams = {} - - -- Add trigrams for each word - for term in query:gmatch('%w+') do - for trigram in pairs(get_trigrams(term)) do - query_trigrams[trigram] = true - end - end - - -- Add trigrams for compound query - local compound_query = query:gsub('[^%w]', '') - for trigram in pairs(get_trigrams(compound_query)) do - query_trigrams[trigram] = true - end - - local max_score = 0 - - for _, entry in ipairs(data) do - local basename = utils.filename(entry.name):gsub('%..*$', '') - - -- Get trigrams for basename and compound version - local file_trigrams = get_trigrams(basename) - local compound_trigrams = get_trigrams(basename:gsub('[^%w]', '')) - - -- Calculate similarities - local name_sim = trigram_similarity(query_trigrams, file_trigrams) - local compound_sim = trigram_similarity(query_trigrams, compound_trigrams) - - -- Take best match - local score = (entry.score or 0) + math.max(name_sim, compound_sim) - - -- Add symbol matches - if entry.symbols then - local symbol_score = 0 - for _, symbol in ipairs(entry.symbols) do - if symbol.name then - local symbol_trigrams = get_trigrams(symbol.name) - local sym_sim = trigram_similarity(query_trigrams, symbol_trigrams) - symbol_score = math.max(symbol_score, sym_sim) - end - end - score = score + (symbol_score * 0.5) -- Weight symbol matches less - end - - max_score = math.max(max_score, score) - entry.score = score - end - - -- Normalize scores - for _, entry in ipairs(data) do - entry.score = entry.score / max_score - end - - -- Sort by score first - table.sort(data, function(a, b) - return a.score > b.score - end) - - -- Use elbow method to find natural cutoff point for symbol-based ranking - local filtered_results = {} - - if #data > 0 then - -- Always include at least the top result - table.insert(filtered_results, data[1]) - - -- Find the point of maximum drop-off (the "elbow") - local max_drop = 0 - local cutoff_index = math.min(MULTI_FILE_THRESHOLD, #data) - - for i = 2, math.min(20, #data) do - local drop = data[i - 1].score - data[i].score - if drop > max_drop then - max_drop = drop - cutoff_index = i - end - end - - -- Include everything up to the cutoff point - for i = 2, cutoff_index do - table.insert(filtered_results, data[i]) - end - - -- Also include any remaining items that have scores close to the cutoff - local cutoff_score = data[cutoff_index].score - local threshold = cutoff_score * 0.8 -- Within 80% of the cutoff score - - for i = cutoff_index + 1, #data do - if data[i].score >= threshold then - table.insert(filtered_results, data[i]) - end - end - end - - return filtered_results -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 ----@param content string ----@param ft string ----@return string?, table? -local function get_outline(content, ft) - if not ft or ft == '' then - return nil - end - - local lang = vim.treesitter.language.get_lang(ft) - local ok, parser = false, nil - if lang then - 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_string_parser, content, ft) - if not ok or not parser then - return nil - end - end - - local root = utils.ts_parse(parser) - local lines = vim.split(content, '\n') - local symbols = {} - local outline_lines = {} - local depth = 0 - - local function parse_node(node) - local type = node:type() - local is_outline = vim.tbl_contains(OUTLINE_TYPES, type) - local start_row, start_col, end_row, end_col = node:range() - - if is_outline then - depth = depth + 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(symbols, { - name = name, - signature = signature_start, - type = type, - start_row = start_row + 1, - start_col = start_col + 1, - end_row = end_row, - end_col = end_col, - }) - end - - for child in node:iter_children() do - parse_node(child) - end - - if is_outline 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) - end - depth = depth - 1 - end - end - - parse_node(root) - - if #outline_lines == 0 then - return nil - end - return table.concat(outline_lines, '\n'), symbols -end - --- Get data for a file ---@param filename string ---@return string?, string? @@ -492,91 +119,4 @@ function M.to_resource(resource) } end ---- Process resources based on the query ----@param prompt string ----@param model string ----@param resources table ----@return table -function M.process_resources(prompt, model, resources) - -- If we dont need to embed anything, just return directly - if #resources < MULTI_FILE_THRESHOLD then - return resources - end - - notify.publish(notify.STATUS, 'Preparing embedding outline') - - -- Get the outlines for each resource - for _, input in ipairs(resources) do - local hash = input.name .. utils.quick_hash(input.data) - input._hash = hash - - local outline = outline_cache[hash] - if not outline then - local outline_text, symbols = get_outline(input.data, input.type) - if outline_text then - outline = { - outline = outline_text, - symbols = symbols, - } - - outline_cache[hash] = outline - end - end - - if outline then - input.outline = outline.outline - input.symbols = outline.symbols - end - end - - notify.publish(notify.STATUS, 'Ranking embeddings') - - -- Build query from history and prompt - local query = prompt - - -- Rank embeddings by symbols - resources = data_ranked_by_symbols(query, resources) - log.debug('Ranked data:', #resources) - for i, item in ipairs(resources) do - log.debug(string.format('%s: %s - %s', i, item.score, item.name)) - end - - -- Prepare embeddings for processing - local to_process = {} - local results = {} - for _, input in ipairs(resources) do - local hash = input._hash - local embed = embedding_cache[hash] - if embed then - input.embedding = embed - table.insert(results, input) - else - table.insert(to_process, input) - end - end - table.insert(to_process, { - type = 'text', - data = query, - }) - - -- Embed the data and process the results - for _, input in ipairs(client:embed(to_process, model)) do - if input._hash then - embedding_cache[input._hash] = input.embedding - end - table.insert(results, input) - end - - -- Rate embeddings by relatedness to the query - local embedded_query = table.remove(results, #results) - log.debug('Embedded query:', embedded_query.content) - results = data_ranked_by_relatedness(embedded_query, results) - log.debug('Ranked embeddings:', #results) - for i, item in ipairs(results) do - log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) - end - - return results -end - return M diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 251238ca..34e6ab23 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -293,13 +293,6 @@ function M.uuid() ) end ---- Generate a quick hash ----@param str string The string to hash ----@return string -function M.quick_hash(str) - return #str .. str:sub(1, 64) .. str:sub(-64) -end - --- Make a string from arguments ---@vararg any The arguments ---@return string @@ -685,46 +678,6 @@ M.schedule_main = async.wrap(function(callback) end end, 1) ---- Run parse on a treesitter parser asynchronously if possible ----@param parser vim.treesitter.LanguageTree The parser -M.ts_parse = async.wrap(function(parser, callback) - ---@diagnostic disable-next-line: invisible - if not parser._async_parse then - local fn = function() - local trees = parser:parse(false) - if not trees or #trees == 0 then - callback(nil) - return - end - callback(trees[1]:root()) - end - - if vim.in_fast_event() then - vim.schedule(fn) - else - fn() - end - - return - end - - local fn = function() - parser:parse(false, function(err, trees) - if err or not trees or #trees == 0 then - callback(nil) - return - end - callback(trees[1]:root()) - end) - end - - if vim.in_fast_event() then - vim.schedule(fn) - else - fn() - end -end, 2) - --- Wait for a user input M.input = async.wrap(function(opts, callback) local fn = function() From 091bf5455bc5b371bd469e6be1dfcc656524321a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 Aug 2025 12:21:52 +0000 Subject: [PATCH 475/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 37524639..76753cd1 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -443,9 +443,6 @@ Add custom AI providers: -- Optional: Disable provider disabled?: boolean, - -- Optional: Embeddings provider name or function - embed?: string|function, - -- Optional: Extra info about the provider displayed in info panel get_info?(): string[] @@ -470,7 +467,6 @@ Add custom AI providers: - `copilot` - GitHub Copilot (default) - `github_models` - GitHub Marketplace models (disabled by default) -- `copilot_embeddings` - Copilot embeddings provider ============================================================================== From 874c7c56000e2b9d226b26f91929524cd5da1a25 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 16:34:21 +0200 Subject: [PATCH 476/728] refactor(tiktoken): use class-based API and method calls (#1271) Refactored the tiktoken module to use a class-based API instead of a module table. All tiktoken functions are now methods, and the client calls them using the colon syntax. This improves encapsulation and maintainability. Also replaced manual table insertion with vim.list_extend for generated messages and history in the client. No functional changes to token counting logic. --- lua/CopilotChat/client.lua | 30 ++++++++---------- lua/CopilotChat/tiktoken.lua | 60 +++++++++++++++++------------------- 2 files changed, 41 insertions(+), 49 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 4fd30197..c6e02334 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -138,17 +138,11 @@ local function generate_ask_request(prompt, system_prompt, history, generated_me end -- Include generated messages and history - for _, message in ipairs(generated_messages) do - table.insert(messages, { - content = message.content, - role = message.role, - }) - end - for _, message in ipairs(history) do - table.insert(messages, message) - end + vim.list_extend(messages, generated_messages) + vim.list_extend(messages, history) + + -- Include user prompt if we have no history if not utils.empty(prompt) and utils.empty(history) then - -- Include user prompt if we have no history table.insert(messages, { content = prompt, role = 'user', @@ -301,7 +295,7 @@ function Client:ask(prompt, opts) log.debug('Tokenizer:', tokenizer) if max_tokens and tokenizer then - tiktoken.load(tokenizer) + tiktoken:load(tokenizer) end if not opts.headless then @@ -320,29 +314,29 @@ function Client:ask(prompt, opts) if max_tokens then -- Count required tokens that we cannot reduce - local selection_tokens = selection_message and tiktoken.count(selection_message.content) or 0 - local prompt_tokens = tiktoken.count(prompt) - local system_tokens = tiktoken.count(opts.system_prompt) - local resource_tokens = #resource_messages > 0 and tiktoken.count(resource_messages[1].content) or 0 + local selection_tokens = selection_message and tiktoken:count(selection_message.content) or 0 + local prompt_tokens = tiktoken:count(prompt) + local system_tokens = tiktoken:count(opts.system_prompt) + local resource_tokens = #resource_messages > 0 and tiktoken:count(resource_messages[1].content) or 0 local required_tokens = prompt_tokens + system_tokens + selection_tokens + resource_tokens -- Calculate how many tokens we can use for history local history_limit = max_tokens - required_tokens local history_tokens = 0 for _, msg in ipairs(history) do - history_tokens = history_tokens + tiktoken.count(msg.content) + history_tokens = history_tokens + tiktoken:count(msg.content) end -- Remove history messages until we are under the limit while history_tokens > history_limit and #history > 0 do local entry = table.remove(history, 1) - history_tokens = history_tokens - tiktoken.count(entry.content) + history_tokens = history_tokens - tiktoken:count(entry.content) end -- Now add as many files as possible with remaining token budget local remaining_tokens = max_tokens - required_tokens - history_tokens for _, message in ipairs(resource_messages) do - local tokens = tiktoken.count(message.content) + local tokens = tiktoken:count(message.content) if remaining_tokens - tokens >= 0 then remaining_tokens = remaining_tokens - tokens table.insert(generated_messages, message) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index dde3d2b5..5a6f346b 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,5 +1,6 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') +local class = utils.class local current_tokenizer = nil --- @return string @@ -13,23 +14,10 @@ local function get_lib_extension() return '.so' end -package.cpath = package.cpath - .. ';' - .. debug.getinfo(1).source:match('@?(.*/)') - .. '../../build/?' - .. get_lib_extension() - -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 ---@async local function load_tiktoken_data(tokenizer) - utils.schedule_main() - local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' .. tokenizer .. '.tiktoken' local cache_dir = vim.fn.stdpath('cache') @@ -49,20 +37,34 @@ local function load_tiktoken_data(tokenizer) return cache_path end -local M = {} +---@class CopilotChat.tiktoken.Tiktoken : Class +---@field private tiktoken_core table? +---@field private tokenizer string? +local Tiktoken = class(function(self) + package.cpath = package.cpath + .. ';' + .. debug.getinfo(1).source:match('@?(.*/)') + .. '../../build/?' + .. get_lib_extension() + + local tiktoken_ok, tiktoken_core = pcall(require, 'tiktoken_core') + self.tiktoken_core = tiktoken_ok and tiktoken_core or nil + self.tokenizer = nil +end) --- Load the tiktoken module ---@param tokenizer string The tokenizer to load ---@async -M.load = function(tokenizer) - if not tiktoken_core then +function Tiktoken:load(tokenizer) + if not self.tiktoken_core then return end - if tokenizer == current_tokenizer then + if tokenizer == self.tokenizer then return end + utils.schedule_main() local path = load_tiktoken_data(tokenizer) local special_tokens = {} special_tokens['<|endoftext|>'] = 100257 @@ -74,26 +76,22 @@ M.load = function(tokenizer) "(?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+" utils.schedule_main() - tiktoken_core.new(path, special_tokens, pat_str) - current_tokenizer = tokenizer + self.tiktoken_core.new(path, special_tokens, pat_str) + self.tokenizer = tokenizer end --- Encode a prompt ---@param prompt string The prompt to encode ---@return table? -function M.encode(prompt) - if not tiktoken_core then +function Tiktoken:encode(prompt) + if not self.tiktoken_core then return nil end - if not prompt or prompt == '' then + if not prompt or prompt == '' or type(prompt) ~= 'string' then return nil end - -- Check if prompt is a string - if type(prompt) ~= 'string' then - error('Prompt must be a string') - end - local ok, result = pcall(tiktoken_core.encode, prompt) + local ok, result = pcall(self.tiktoken_core.encode, prompt) if not ok then return nil end @@ -104,16 +102,16 @@ 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 +function Tiktoken:count(prompt) + if not self.tiktoken_core then return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count end - local tokens = M.encode(prompt) + local tokens = self:encode(prompt) if not tokens then return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count end return #tokens end -return M +return Tiktoken() From 4d11c49b7a1afb573a3b09be5e10a78a3d41649d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 17:31:01 +0200 Subject: [PATCH 477/728] fix(functions): do not require tool reference in tool prompt, just tool id (#1273) Before it was necessary to include tool @ reference again in tool prompt or make it sticky, but we already know that the user asked for the tool so we do not need to check it again Closes #1269 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 10b8bd10..0f6e6671 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -178,8 +178,8 @@ local function finish(start_of_chat) end local prompt_content = '' - local last_message = M.chat.messages[#M.chat.messages] - local tool_calls = last_message and last_message.tool_calls or {} + local assistant_message = M.chat:get_message('assistant') + local tool_calls = assistant_message and assistant_message.tool_calls or {} if not utils.empty(state.sticky) then for _, sticky in ipairs(state.sticky) do @@ -316,7 +316,7 @@ function M.resolve_functions(prompt, config) for _, match in ipairs(matches) do for name, tool in pairs(M.config.functions) do if name == match or tool.group == match then - enabled_tools[name] = true + table.insert(enabled_tools, tools[name]) end end end @@ -382,7 +382,7 @@ function M.resolve_functions(prompt, config) if not tool then return nil end - if tool_id and not enabled_tools[name] and not tool.uri then + if not tool_id and not tool.uri then return nil end @@ -435,12 +435,7 @@ function M.resolve_functions(prompt, config) end end - return vim.tbl_map(function(name) - return tools[name] - end, vim.tbl_keys(enabled_tools)), - resolved_resources, - resolved_tools, - prompt + return enabled_tools, resolved_resources, resolved_tools, prompt end --- Resolve the final prompt and config from prompt template. From 93110a5f289aaed20adbbc13ec803f94dc6c63c6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 17:36:36 +0200 Subject: [PATCH 478/728] fix(ui): prevent italics from breaking glob pattern highlights (#1274) Disables the '@markup.italic.markdown_inline' highlight in the chat UI. This prevents markdown italics from interfering with glob pattern highlighting in chat messages. Signed-off-by: Tomas Slusny --- README.md | 1 - lua/CopilotChat/ui/chat.lua | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7e7cfb2..a4457147 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,6 @@ You can customize colors by setting highlight groups in your config: -- In your colorscheme or init.lua vim.api.nvim_set_hl(0, 'CopilotChatHeader', { fg = '#7C3AED', bold = true }) vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { fg = '#374151' }) -vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { fg = '#10B981', italic = true }) ``` Types of copilot highlights: diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 8b22c32e..e1d1156e 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -341,7 +341,8 @@ function Chat:open(config) end local ns = vim.api.nvim_create_namespace('copilot-chat-local-hl') - vim.api.nvim_set_hl(ns, '@markup.quote.markdown', {}) + vim.api.nvim_set_hl(ns, '@markup.quote.markdown', {}) -- disable quote block overriding chat keywords + vim.api.nvim_set_hl(ns, '@markup.italic.markdown_inline', {}) -- disable italic messing up glob patterns vim.api.nvim_win_set_hl_ns(self.winnr, ns) vim.api.nvim_win_set_buf(self.winnr, self.bufnr) self:render() From a92ed8157ecfb47054c121d406fd10a588254149 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 Aug 2025 15:36:56 +0000 Subject: [PATCH 479/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 76753cd1..04a95613 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -318,7 +318,6 @@ You can customize colors by setting highlight groups in your config: -- In your colorscheme or init.lua vim.api.nvim_set_hl(0, 'CopilotChatHeader', { fg = '#7C3AED', bold = true }) vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { fg = '#374151' }) - vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { fg = '#10B981', italic = true }) < Types of copilot highlights: From 7576afad950d4258cc7d455d8d42f7dccac4d19b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 17:47:29 +0200 Subject: [PATCH 480/728] chore: mark next release as 4.3.0 (#1275) Release-As: 4.3.0 From 06e54538c24cbeb341301418dabaf598ae8e7e60 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Aug 2025 18:11:52 +0200 Subject: [PATCH 481/728] refactor(client): use provider resolver instead of static table (#1276) Refactors the client to use a provider resolver function instead of a static providers table. This allows providers to be dynamically resolved when needed, improving flexibility and reducing the need to reload providers manually. Updates all internal references and replaces load_providers with add_providers. Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 47 ++++++++++++++++++++++++-------------- lua/CopilotChat/init.lua | 4 +++- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index c6e02334..f39953ef 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -54,6 +54,7 @@ local class = utils.class --- Constants local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' +local CACHE_TTL = 300 -- 5 minutes --- Generate content block with line numbers, truncating if necessary ---@param content string @@ -153,22 +154,40 @@ local function generate_ask_request(prompt, system_prompt, history, generated_me end ---@class CopilotChat.client.Client : Class ----@field private providers table +---@field private provider_resolver function():table ---@field private provider_cache table ---@field private model_cache table? ---@field private current_job string? local Client = class(function(self) - self.providers = {} - self.provider_cache = {} + self.provider_resolver = nil + self.provider_cache = vim.defaulttable(function() + return {} + end) self.model_cache = nil self.current_job = nil end) +--- Get all providers from the client +---@return table +function Client:get_providers() + if self.provider_resolver then + return self.provider_resolver() + end + return {} +end + +--- Set a provider resolver on the client +---@param resolver function: A function that returns a table of providers +function Client:add_providers(resolver) + self.provider_resolver = resolver +end + --- Authenticate with GitHub and get the required headers ---@param provider_name string: The provider to authenticate with ---@return table function Client:authenticate(provider_name) - local provider = self.providers[provider_name] + local providers = self:get_providers() + local provider = providers[provider_name] local headers = self.provider_cache[provider_name].headers local expires_at = self.provider_cache[provider_name].expires_at @@ -189,10 +208,11 @@ function Client:models() end local models = {} - local provider_order = vim.tbl_keys(self.providers) + local providers = self:get_providers() + local provider_order = vim.tbl_keys(providers) table.sort(provider_order) for _, provider_name in ipairs(provider_order) do - local provider = self.providers[provider_name] + local provider = providers[provider_name] if not provider.disabled and provider.get_models then notify.publish(notify.STATUS, 'Fetching models from ' .. provider_name) local ok, headers = pcall(self.authenticate, self, provider_name) @@ -228,9 +248,9 @@ end function Client:info() local infos = {} local now = math.floor(os.time()) - local CACHE_TTL = 300 -- 5 minutes + local providers = self:get_providers() - for provider_name, provider in pairs(self.providers) do + for provider_name, provider in pairs(providers) do if not provider.disabled and provider.get_info then local cache = self.provider_cache[provider_name] if cache and cache.info and cache.info_expires_at and cache.info_expires_at > now then @@ -267,6 +287,7 @@ function Client:ask(prompt, opts) log.debug('Resources:', #opts.resources) log.debug('History:', #opts.history) + local providers = self:get_providers() local models = self:models() local model_config = models[opts.model] if not model_config then @@ -277,7 +298,7 @@ function Client:ask(prompt, opts) if not provider_name then error('Provider not found for model: ' .. opts.model) end - local provider = self.providers[provider_name] + local provider = providers[provider_name] if not provider then error('Provider not found: ' .. provider_name) end @@ -558,13 +579,5 @@ function Client:running() return self.current_job ~= nil end ---- Load providers to client -function Client:load_providers(providers) - self.providers = providers - for provider_name, _ in pairs(providers) do - self.provider_cache[provider_name] = {} - end -end - --- @type CopilotChat.client.Client return Client() diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0f6e6671..d90b39ff 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1200,7 +1200,9 @@ function M.setup(config) -- Load the providers client:stop() - client:load_providers(M.config.providers) + client:add_providers(function() + return M.config.providers + end) if M.config.debug then M.log_level('debug') From 2eb4dd98ed934bce31b486ab117ea5b7ba875b0f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 6 Aug 2025 20:14:28 +0200 Subject: [PATCH 482/728] refactor(resources): unify resource format and handling (#1279) Simplifies resource representation by removing redundant conversion logic and standardizing resource fields. Updates resource block formatting and selection message generation to use the new structure. Cleans up unused types and improves consistency across resource-related functions. --- lua/CopilotChat/client.lua | 64 ++++++++++++++++------------ lua/CopilotChat/config/functions.lua | 11 +++-- lua/CopilotChat/config/mappings.lua | 10 ++--- lua/CopilotChat/init.lua | 2 +- lua/CopilotChat/resources.lua | 20 --------- 5 files changed, 47 insertions(+), 60 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index f39953ef..95db5bb8 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -32,9 +32,10 @@ ---@field schema table? schema of the tool ---@class CopilotChat.client.Resource ----@field name string ----@field type string ---@field data string +---@field name string? +---@field mimetype string? +---@field uri string? ---@class CopilotChat.client.Model ---@field provider string? @@ -53,49 +54,58 @@ local utils = require('CopilotChat.utils') local class = utils.class --- Constants -local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' +local RESOURCE_SHORT_FORMAT = '# %s\n```%s start_line=% end_line=%s\n%s\n```' +local RESOURCE_LONG_FORMAT = '# %s\n```%s path=%s start_line=%s end_line=%s\n%s\n```' local CACHE_TTL = 300 -- 5 minutes ---- Generate content block with line numbers, truncating if necessary +--- Generate resource block with line numbers, truncating if necessary ---@param content string ----@param start_line number?: The starting line number +---@param start_line number: The starting line number ---@return string -local function generate_content_block(content, start_line) - if start_line ~= nil 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_line or 1)) - lines[i] = formatted_line_number .. ': ' .. line - end +local function generate_resource_block(content, mimetype, name, path, start_line, end_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_line or 1)) + lines[i] = formatted_line_number .. ': ' .. line + end - return table.concat(lines, '\n') + local updated_content = table.concat(lines, '\n') + local filetype = utils.mimetype_to_filetype(mimetype or 'text') + if not start_line then + start_line = 1 + end + if not end_line then + end_line = start_line and (start_line + total_lines - 1) or 1 end - return content + if path then + return string.format(RESOURCE_LONG_FORMAT, name, filetype, path, start_line, end_line, updated_content) + else + return string.format(RESOURCE_SHORT_FORMAT, name, filetype, start_line, end_line, updated_content) + end end --- Generate messages for the given selection --- @param selection CopilotChat.select.Selection --- @return CopilotChat.client.Message? local function generate_selection_message(selection) - local filename = selection.filename or 'unknown' - local filetype = selection.filetype or 'text' local content = selection.content if not content or content == '' then return nil end - local out = "User's active selection:\n" - if selection.start_line and selection.end_line then - out = out .. string.format('Excerpt from %s, lines %s to %s:\n', filename, selection.start_line, selection.end_line) - end - out = out .. string.format('```%s\n%s\n```', filetype, generate_content_block(content, selection.start_line)) - return { - content = out, + content = generate_resource_block( + content, + selection.filetype, + "User's active selection", + selection.filename, + selection.start_line, + selection.end_line + ), role = 'user', } end @@ -110,10 +120,8 @@ local function generate_resource_messages(resources) return resource.data and resource.data ~= '' end) :map(function(resource) - local content = generate_content_block(resource.data, 1) - return { - content = string.format(RESOURCE_FORMAT, resource.name, resource.type, content), + content = generate_resource_block(resource.data, resource.mimetype, resource.uri, resource.name, 1, nil), role = 'user', } end) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 25c3f6c5..9050d267 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -1,17 +1,12 @@ local resources = require('CopilotChat.resources') local utils = require('CopilotChat.utils') ----@class CopilotChat.config.functions.Result ----@field data string ----@field mimetype string? ----@field uri string? - ---@class CopilotChat.config.functions.Function ---@field description string? ---@field schema table? ---@field group string? ---@field uri string? ----@field resolve fun(input: table, source: CopilotChat.source, prompt: string):table +---@field resolve fun(input: table, source: CopilotChat.source, prompt: string):table ---@type table return { @@ -46,6 +41,7 @@ return { return { { uri = 'file://' .. input.path, + name = input.path, mimetype = mimetype, data = data, }, @@ -163,6 +159,7 @@ return { return { { uri = 'buffer://' .. name, + name = name, mimetype = mimetype, data = data, }, @@ -205,6 +202,7 @@ return { end return { uri = 'buffer://' .. name, + name = name, mimetype = mimetype, data = data, } @@ -258,6 +256,7 @@ return { end return { uri = uri, + name = file, mimetype = mimetype, data = data, } diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index aaaa0ea6..04152721 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -33,6 +33,11 @@ local function get_diff(block) -- If we have header info, use it as source of truth if header.start_line and header.end_line then + filename = utils.uri_to_filename(header.filename) + filetype = header.filetype or utils.filetype(filename) + start_line = header.start_line + end_line = header.end_line + -- Try to find matching buffer and window bufnr = nil for _, win in ipairs(vim.api.nvim_list_wins()) do @@ -43,11 +48,6 @@ local function get_diff(block) end end - filename = header.filename - filetype = header.filetype or utils.filetype(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') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d90b39ff..8b343a75 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -397,7 +397,7 @@ function M.resolve_functions(prompt, config) local content_out = nil if content.uri then content_out = '##' .. content.uri - table.insert(resolved_resources, resources.to_resource(content)) + table.insert(resolved_resources, content) if tool_id then table.insert(state.sticky, content_out) end diff --git a/lua/CopilotChat/resources.lua b/lua/CopilotChat/resources.lua index e3ec443a..a8c1fd78 100644 --- a/lua/CopilotChat/resources.lua +++ b/lua/CopilotChat/resources.lua @@ -1,12 +1,3 @@ ----@class CopilotChat.resources.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 - local async = require('plenary.async') local utils = require('CopilotChat.utils') local file_cache = {} @@ -108,15 +99,4 @@ function M.get_url(url) return content, utils.filetype_to_mimetype(ft) end ---- Transform a resource into a format suitable for the client ----@param resource CopilotChat.config.functions.Result ----@return CopilotChat.client.Resource -function M.to_resource(resource) - return { - name = utils.uri_to_filename(resource.uri), - type = utils.mimetype_to_filetype(resource.mimetype), - data = resource.data, - } -end - return M From 468871cf690d5a9a13ef1b12af394ef988c82489 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 Aug 2025 18:14:51 +0000 Subject: [PATCH 483/728] 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 04a95613..ffacf3e0 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 04 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 06 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 59f5b43cdd3d27ab4e033882179d5cf028cf1302 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 7 Aug 2025 02:33:03 +0200 Subject: [PATCH 484/728] feat(keymap): switch back to for completion, add Copilot conflict note (#1280) * feat(keymap): switch back to for completion, add Copilot conflict note Switch completion keymap from to in CopilotChat. Update README to reflect this change and add a warning about potential keymap conflicts with copilot.vim. Provide instructions for disabling Copilot's default mapping and customizing CopilotChat keymaps. * [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 | 43 ++++++++++++++++++----------- lua/CopilotChat/config/mappings.lua | 2 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a4457147..4198bb70 100644 --- a/README.md +++ b/README.md @@ -136,22 +136,33 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif ## Chat Key Mappings -| Insert | Normal | Action | -| ----------- | ------- | ------------------------------------------ | -| `` | - | Trigger/accept completion menu for tokens | -| `` | `q` | Close the chat window | -| `` | `` | Reset and clear the chat window | -| `` | `` | Submit the current prompt | -| - | `grr` | Toggle sticky prompt for line under cursor | -| - | `grx` | Clear all sticky prompts in prompt | -| `` | `` | Accept nearest diff | -| - | `gj` | Jump to section of nearest diff | -| - | `gqa` | Add all answers from chat to quickfix list | -| - | `gqd` | Add all diffs from chat to quickfix list | -| - | `gy` | Yank nearest diff to register | -| - | `gd` | Show diff between source and nearest diff | -| - | `gc` | Show info about current chat | -| - | `gh` | Show help message | +| Insert | Normal | Action | +| ------- | ------- | ------------------------------------------ | +| `` | - | Trigger/accept completion menu for tokens | +| `` | `q` | Close the chat window | +| `` | `` | Reset and clear the chat window | +| `` | `` | Submit the current prompt | +| - | `grr` | Toggle sticky prompt for line under cursor | +| - | `grx` | Clear all sticky prompts in prompt | +| `` | `` | Accept nearest diff | +| - | `gj` | Jump to section of nearest diff | +| - | `gqa` | Add all answers from chat to quickfix list | +| - | `gqd` | Add all diffs from chat to quickfix list | +| - | `gy` | Yank nearest diff to register | +| - | `gd` | Show diff between source and nearest diff | +| - | `gc` | Show info about current chat | +| - | `gh` | Show help message | + +> [!WARNING] +> Some plugins (e.g. `copilot.vim`) may also map common keys like `` in insert mode. +> To avoid conflicts, disable Copilot's default `` mapping with: +> +> ```lua +> vim.g.copilot_no_tab_map = true +> vim.keymap.set('i', '', 'copilot#Accept("\\")', { expr = true, replace_keycodes = false }) +> ``` +> +> You can also customize CopilotChat keymaps in your config. ## Predefined Functions diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 04152721..552e6c6b 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -137,7 +137,7 @@ end ---@field show_help CopilotChat.config.mapping|false|nil return { complete = { - insert = '', + insert = '', callback = function() copilot.trigger_complete() end, From ba213d39734d026e02af3117a33eba342b13e7e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 7 Aug 2025 00:33:23 +0000 Subject: [PATCH 485/728] 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 ffacf3e0..391a7766 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 06 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 07 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -175,22 +175,31 @@ COMMANDS *CopilotChat-commands* CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* - Insert Normal Action - ----------- -------- -------------------------------------------- - - Trigger/accept completion menu for tokens - q Close the chat window - Reset and clear the chat window - Submit the current prompt - - grr Toggle sticky prompt for line under cursor - - grx Clear all sticky prompts in prompt - Accept nearest diff - - gj Jump to section of nearest diff - - gqa Add all answers from chat to quickfix list - - gqd Add all diffs from chat to quickfix list - - gy Yank nearest diff to register - - gd Show diff between source and nearest diff - - gc Show info about current chat - - gh Show help message + Insert Normal Action + -------- -------- -------------------------------------------- + - Trigger/accept completion menu for tokens + q Close the chat window + Reset and clear the chat window + Submit the current prompt + - grr Toggle sticky prompt for line under cursor + - grx Clear all sticky prompts in prompt + Accept nearest diff + - gj Jump to section of nearest diff + - gqa Add all answers from chat to quickfix list + - gqd Add all diffs from chat to quickfix list + - gy Yank nearest diff to register + - gd Show diff between source and nearest diff + - gc Show info about current chat + - gh Show help message + + [!WARNING] Some plugins (e.g. `copilot.vim`) may also map common keys like + `` in insert mode. To avoid conflicts, disable Copilot’s default `` + mapping with: + >lua + vim.g.copilot_no_tab_map = true + vim.keymap.set('i', '', 'copilot#Accept("\\")', { expr = true, replace_keycodes = false }) + < + You can also customize CopilotChat keymaps in your config. PREDEFINED FUNCTIONS *CopilotChat-predefined-functions* From 807c4a179d48aedaf9c5b5dd667486766411d81b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 7 Aug 2025 04:59:41 +0200 Subject: [PATCH 486/728] refactor: clean up annotations and unused code (#1281) Remove unused resources import from init.lua and unnecessary variable from tiktoken.lua. Add and improve function annotations in chat.lua for better documentation and maintainability. --- lua/CopilotChat/init.lua | 1 - lua/CopilotChat/tiktoken.lua | 2 +- lua/CopilotChat/ui/chat.lua | 5 +++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 8b343a75..7791e43a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,7 +1,6 @@ local async = require('plenary.async') local log = require('plenary.log') local functions = require('CopilotChat.functions') -local resources = require('CopilotChat.resources') local client = require('CopilotChat.client') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 5a6f346b..3f631428 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,8 +1,8 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = utils.class -local current_tokenizer = nil +--- Get the library extension based on the operating system --- @return string local function get_lib_extension() if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index e1d1156e..2752e6d0 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -424,6 +424,9 @@ function Chat:finish() end end +--- Add a message to the chat window. +---@param message CopilotChat.client.Message +---@param replace boolean? If true, replaces the last message if it has same role function Chat:add_message(message, replace) local current_message = self.messages[#self.messages] local is_new = not current_message @@ -471,6 +474,8 @@ function Chat:add_message(message, replace) end end +--- Remove a message from the chat window by role. +---@param role string function Chat:remove_message(role) if not self:visible() then return From 8e4dc67d822d649280e82561725462e86923695f Mon Sep 17 00:00:00 2001 From: Mihamina Rakotomandimby Date: Thu, 7 Aug 2025 18:49:59 +0300 Subject: [PATCH 487/728] docs: update info about models * #1283 Update the list of models supported * #1283 Update the list of models supported --------- Co-authored-by: Mihamina RKTMB --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4198bb70..52a8d56b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. -- 🤖 **Multiple AI Models** - GitHub Copilot (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash) + custom providers (Ollama, Mistral.ai) +- 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your [GitHub Copilot settings](https://github.com/settings/copilot/features) and the models provided by GitHub's API. - 🔧 **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval - 🔒 **Explicit Control** - Only shares what you specifically request - no background data collection - 📝 **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration From e81da15603a47117cd6852be1e0a2806ca5795c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 7 Aug 2025 15:50:18 +0000 Subject: [PATCH 488/728] 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 391a7766..e9b4a113 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -39,7 +39,7 @@ Table of Contents *CopilotChat-table-of-contents* CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. -- 🤖 **Multiple AI Models** - GitHub Copilot (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash) + custom providers (Ollama, Mistral.ai) +- 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your GitHub Copilot settings and the models provided by GitHub’s API. - 🔧 **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval - 🔒 **Explicit Control** - Only shares what you specifically request - no background data collection - 📝 **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration From d4ed3fb40f301fc1ff32e0ff3b437b619112ce14 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:53:04 +0200 Subject: [PATCH 489/728] docs: add rakotomandimby as a contributor for doc (#1287) * 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 3e77a246..72ae3b71 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -431,6 +431,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/214497460?v=4", "profile": "https://github.com/danilohorta", "contributions": ["code"] + }, + { + "login": "rakotomandimby", + "name": "Mihamina Rakotomandimby", + "avatar_url": "https://avatars.githubusercontent.com/u/488088?v=4", + "profile": "https://mihamina.rktmb.org", + "contributions": ["doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 52a8d56b..5bdfac2d 100644 --- a/README.md +++ b/README.md @@ -632,6 +632,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Aaron D Borden
Aaron D Borden

💻 Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

💻 📖 Danilo Horta
Danilo Horta

💻 + Mihamina Rakotomandimby
Mihamina Rakotomandimby

📖 From 1189e376fcad629edf6ffd186aa659f114df0271 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 12:55:57 +0200 Subject: [PATCH 490/728] feat(setup): trigger CopilotChatLoaded user autocommand (#1288) Adds a call to vim.api.nvim_exec_autocmds for the 'User' event with the 'CopilotChatLoaded' pattern at the end of the setup function. This allows external plugins or user configs to react when CopilotChat has finished loading. Also removes unused highlights_loaded state assignment. --- lua/CopilotChat/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7791e43a..0cd052e9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1189,7 +1189,6 @@ end function M.setup(config) local default_config = require('CopilotChat.config') M.config = vim.tbl_deep_extend('force', default_config, config or {}) - state.highlights_loaded = false -- Save proxy and insecure settings utils.curl_store_args({ @@ -1311,6 +1310,8 @@ function M.setup(config) end end end + + vim.api.nvim_exec_autocmds('User', { pattern = 'CopilotChatLoaded' }) end return M From 6eb4d810bc9f2163ed85922df9f78fea49cd4989 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 8 Aug 2025 10:56:14 +0000 Subject: [PATCH 491/728] 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 e9b4a113..c3b52a5c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 07 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 08 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -634,7 +634,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖This project follows the all-contributors specification. Contributions of any kind are welcome! From 536c5c66e69b113204af1d776b29b86e870471e0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 14:45:55 +0200 Subject: [PATCH 492/728] refactor(core): improve config handling and setup logic (#1290) - Use metatable for lazy config loading in main module - Update setup to merge config values instead of replacing table - Remove unused autocmd trigger for CopilotChatLoaded - Set default separator directly when empty --- lua/CopilotChat/init.lua | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0cd052e9..64558190 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -15,7 +15,14 @@ local BLOCK_OUTPUT_FORMAT = '```%s\n%s\n```' ---@class CopilotChat ---@field config CopilotChat.config.Config ---@field chat CopilotChat.ui.chat.Chat -local M = {} +local M = setmetatable({}, { + __index = function(t, key) + if key == 'config' then + return require('CopilotChat.config') + end + return rawget(t, key) + end, +}) --- @class CopilotChat.source --- @field bufnr number @@ -1187,8 +1194,10 @@ end --- Set up the plugin ---@param config CopilotChat.config.Config? function M.setup(config) - local default_config = require('CopilotChat.config') - M.config = vim.tbl_deep_extend('force', default_config, config or {}) + -- Little bit of update magic + for k, v in pairs(vim.tbl_deep_extend('force', M.config, config or {})) do + M.config[k] = v + end -- Save proxy and insecure settings utils.curl_store_args({ @@ -1212,7 +1221,7 @@ function M.setup(config) log.warn( 'Empty separator is not allowed, using default separator instead. Set `separator` in config to change this.' ) - M.config.separator = default_config.separator + M.config.separator = '---' end if M.chat then @@ -1310,8 +1319,6 @@ function M.setup(config) end end end - - vim.api.nvim_exec_autocmds('User', { pattern = 'CopilotChatLoaded' }) end return M From 93d3bb93fd6478b25c35acce99815583a79c8079 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:52:53 +0200 Subject: [PATCH 493/728] chore(main): release 4.3.0 (#1270) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ version.txt | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19bf9a6b..82fab37b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [4.3.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.2.0...v4.3.0) (2025-08-08) + + +### ⚠ BREAKING CHANGES + +* **core:** Resource processing and embeddings support have been removed. Any configuration or usage relying on these features will no longer work. + +### Features + +* **keymap:** switch back to <Tab> for completion, add Copilot conflict note ([#1280](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1280)) ([59f5b43](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/59f5b43cdd3d27ab4e033882179d5cf028cf1302)) +* **setup:** trigger CopilotChatLoaded user autocommand ([#1288](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1288)) ([1189e37](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1189e376fcad629edf6ffd186aa659f114df0271)) + + +### Bug Fixes + +* **functions:** do not require tool reference in tool prompt, just tool id ([#1273](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1273)) ([4d11c49](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/4d11c49b7a1afb573a3b09be5e10a78a3d41649d)), closes [#1269](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1269) +* **ui:** prevent italics from breaking glob pattern highlights ([#1274](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1274)) ([93110a5](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/93110a5f289aaed20adbbc13ec803f94dc6c63c6)) + + +### Miscellaneous Chores + +* mark next release as 4.3.0 ([#1275](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1275)) ([7576afa](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7576afad950d4258cc7d455d8d42f7dccac4d19b)) + + +### Code Refactoring + +* **core:** remove resource processing and embeddings ([#1203](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1203)) ([f38319f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f38319fd8f3a7aaa1f75b78027032f9c07abc425)) + ## [4.2.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.1.0...v4.2.0) (2025-08-03) diff --git a/version.txt b/version.txt index 6aba2b24..80895903 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.2.0 +4.3.0 From ffb665919fdafecbfb8dceaf63243d614b50c497 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 15:06:15 +0200 Subject: [PATCH 494/728] fix(client): store models cache per provider (#1291) Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 49 +++++++++++++++-------------- lua/CopilotChat/config/mappings.lua | 2 +- lua/CopilotChat/utils.lua | 3 ++ 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 95db5bb8..7955691c 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -164,14 +164,12 @@ end ---@class CopilotChat.client.Client : Class ---@field private provider_resolver function():table ---@field private provider_cache table ----@field private model_cache table? ---@field private current_job string? local Client = class(function(self) self.provider_resolver = nil self.provider_cache = vim.defaulttable(function() return {} end) - self.model_cache = nil self.current_job = nil end) @@ -211,10 +209,6 @@ end --- Fetch models from the Copilot API ---@return table function Client:models() - if self.model_cache then - return self.model_cache - end - local models = {} local providers = self:get_providers() local provider_order = vim.tbl_keys(providers) @@ -222,24 +216,34 @@ function Client:models() for _, provider_name in ipairs(provider_order) do local provider = providers[provider_name] if not provider.disabled and provider.get_models then - notify.publish(notify.STATUS, 'Fetching models from ' .. provider_name) - local ok, headers = pcall(self.authenticate, self, provider_name) - if not ok then - log.warn('Failed to authenticate with ' .. provider_name .. ': ' .. headers) - goto continue - end - local ok, provider_models = pcall(provider.get_models, headers) - if not ok then - log.warn('Failed to fetch models from ' .. provider_name .. ': ' .. provider_models) - goto continue + local cache = self.provider_cache[provider_name] + local resolved_models = nil + if cache and cache.models then + resolved_models = cache.models + else + notify.publish(notify.STATUS, 'Fetching models from ' .. provider_name) + local ok, headers = pcall(self.authenticate, self, provider_name) + if not ok then + log.warn('Failed to authenticate with ' .. provider_name .. ': ' .. headers) + goto continue + end + local ok, provider_models = pcall(provider.get_models, headers) + if not ok then + log.warn('Failed to fetch models from ' .. provider_name .. ': ' .. provider_models) + goto continue + end + resolved_models = provider_models + cache.models = resolved_models end - for _, model in ipairs(provider_models) do - model.provider = provider_name - if models[model.id] then - model.id = model.id .. ':' .. provider_name + if resolved_models then + for _, model in ipairs(resolved_models) do + model.provider = provider_name + if models[model.id] then + model.id = model.id .. ':' .. provider_name + end + models[model.id] = model end - models[model.id] = model end ::continue:: @@ -247,8 +251,7 @@ function Client:models() end log.debug('Fetched models:', #vim.tbl_keys(models)) - self.model_cache = models - return self.model_cache + return models end --- Get information about all providers diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 552e6c6b..2421c264 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -519,7 +519,7 @@ return { end table.insert(lines, header) - table.insert(lines, '```' .. resource.type) + table.insert(lines, '```' .. utils.mimetype_to_filetype(resource.mimetype)) for _, line in ipairs(preview) do table.insert(lines, line) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 34e6ab23..54f4c6ae 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -240,6 +240,9 @@ function M.filetype_to_mimetype(filetype) if filetype == 'html' or filetype == 'css' then return 'text/' .. filetype end + if filetype:find('/') then + return filetype + end return 'text/x-' .. filetype end From b1d2b4f3c7cd3fc1e89605ecd070c2670be35a68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:07:07 +0200 Subject: [PATCH 495/728] chore(main): release 4.3.1 (#1292) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fab37b..b0585f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [4.3.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.3.0...v4.3.1) (2025-08-08) + + +### Bug Fixes + +* **client:** store models cache per provider ([#1291](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1291)) ([ffb6659](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/ffb665919fdafecbfb8dceaf63243d614b50c497)) + ## [4.3.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.2.0...v4.3.0) (2025-08-08) diff --git a/version.txt b/version.txt index 80895903..f77856a6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.3.0 +4.3.1 From aac1a2684b6bb6e6daac3d83bc6fa4eff6ff5cae Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 21:15:42 +0200 Subject: [PATCH 496/728] refactor(client): use OrderedMap for provider management (#1295) Refactored provider handling in Client to use OrderedMap, improving consistency and sorting. Added generic type annotations for OrderedMap and introduced a get_cached helper for cache management. This change simplifies provider filtering and caching logic for models and info. --- lua/CopilotChat/client.lua | 143 +++++++++++++++++++++---------------- lua/CopilotChat/utils.lua | 5 +- 2 files changed, 84 insertions(+), 64 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 7955691c..146169fb 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -58,6 +58,22 @@ local RESOURCE_SHORT_FORMAT = '# %s\n```%s start_line=% end_line=%s\n%s\n```' local RESOURCE_LONG_FORMAT = '# %s\n```%s path=%s start_line=%s end_line=%s\n%s\n```' local CACHE_TTL = 300 -- 5 minutes +--- Get a cached value or fill it if not present +--- @param cache table: The cache table to use +--- @param key string: The key to look up in the cache +--- @param filler function: A function that returns the value to cache if not present +local function get_cached(cache, key, filler) + local now = math.floor(os.time()) + if cache and cache[key] and cache[key .. '_expires_at'] > now then + return cache[key] + end + + local value = filler() + cache[key] = value + cache[key .. '_expires_at'] = now + CACHE_TTL + return value +end + --- Generate resource block with line numbers, truncating if necessary ---@param content string ---@param start_line number: The starting line number @@ -174,12 +190,26 @@ local Client = class(function(self) end) --- Get all providers from the client ----@return table -function Client:get_providers() - if self.provider_resolver then - return self.provider_resolver() +---@param supported_method? string: The method to filter providers by (optional) +---@return OrderedMap +function Client:get_providers(supported_method) + local out = utils.ordered_map() + + if not self.provider_resolver then + return out + end + + local providers = self.provider_resolver() + local provider_names = vim.tbl_keys(providers) + table.sort(provider_names) + + for _, provider_name in ipairs(provider_names) do + local provider = providers[provider_name] + if provider and not provider.disabled and (not supported_method or provider[supported_method]) then + out:set(provider_name, provider) + end end - return {} + return out end --- Set a provider resolver on the client @@ -192,8 +222,7 @@ end ---@param provider_name string: The provider to authenticate with ---@return table function Client:authenticate(provider_name) - local providers = self:get_providers() - local provider = providers[provider_name] + local provider = self:get_providers():get(provider_name) local headers = self.provider_cache[provider_name].headers local expires_at = self.provider_cache[provider_name].expires_at @@ -209,80 +238,71 @@ end --- Fetch models from the Copilot API ---@return table function Client:models() - local models = {} - local providers = self:get_providers() - local provider_order = vim.tbl_keys(providers) - table.sort(provider_order) - for _, provider_name in ipairs(provider_order) do - local provider = providers[provider_name] - if not provider.disabled and provider.get_models then - local cache = self.provider_cache[provider_name] - local resolved_models = nil - if cache and cache.models then - resolved_models = cache.models - else + local out = {} + local providers = self:get_providers('get_models') + + for _, provider_name in ipairs(providers:keys()) do + local provider = providers:get(provider_name) + for _, model in + ipairs(get_cached(self.provider_cache[provider_name], 'models', function() notify.publish(notify.STATUS, 'Fetching models from ' .. provider_name) + local ok, headers = pcall(self.authenticate, self, provider_name) if not ok then log.warn('Failed to authenticate with ' .. provider_name .. ': ' .. headers) - goto continue + return {} end - local ok, provider_models = pcall(provider.get_models, headers) + + local ok, models = pcall(provider.get_models, headers) if not ok then - log.warn('Failed to fetch models from ' .. provider_name .. ': ' .. provider_models) - goto continue + log.warn('Failed to fetch models from ' .. provider_name .. ': ' .. models) + return {} end - resolved_models = provider_models - cache.models = resolved_models - end - if resolved_models then - for _, model in ipairs(resolved_models) do - model.provider = provider_name - if models[model.id] then - model.id = model.id .. ':' .. provider_name - end - models[model.id] = model - end + return models or {} + end)) + do + model.provider = provider_name + if out[model.id] then + model.id = model.id .. ':' .. provider_name end - - ::continue:: + out[model.id] = model end end - log.debug('Fetched models:', #vim.tbl_keys(models)) - return models + log.debug('Fetched models:', #vim.tbl_keys(out)) + return out end --- Get information about all providers ---@return table function Client:info() - local infos = {} - local now = math.floor(os.time()) - local providers = self:get_providers() + local out = {} + local providers = self:get_providers('get_info') + + for _, provider_name in ipairs(providers:keys()) do + local provider = providers:get(provider_name) + out[provider_name] = get_cached(self.provider_cache[provider_name], 'infos', function() + notify.publish(notify.STATUS, 'Fetching info from ' .. provider_name) + + local ok, headers = pcall(self.authenticate, self, provider_name) + if not ok then + log.warn('Failed to authenticate with ' .. provider_name .. ': ' .. headers) + return {} + end - for provider_name, provider in pairs(providers) do - if not provider.disabled and provider.get_info then - local cache = self.provider_cache[provider_name] - if cache and cache.info and cache.info_expires_at and cache.info_expires_at > now then - infos[provider_name] = cache.info - else - local ok, info = pcall(provider.get_info, self:authenticate(provider_name)) - if ok then - infos[provider_name] = info - if cache then - cache.info = info - cache.info_expires_at = now + CACHE_TTL - end - else - log.warn('Failed to get info for provider ' .. provider_name .. ': ' .. info) - end + local ok, infos = pcall(provider.get_info, headers) + if not ok then + log.warn('Failed to fetch info from ' .. provider_name .. ': ' .. infos) + return {} end - end + + return infos or {} + end) end - log.debug('Fetched provider infos:', #vim.tbl_keys(infos)) - return infos + log.debug('Fetched provider infos:', #vim.tbl_keys(out)) + return out end --- Ask a question to Copilot @@ -298,7 +318,6 @@ function Client:ask(prompt, opts) log.debug('Resources:', #opts.resources) log.debug('History:', #opts.history) - local providers = self:get_providers() local models = self:models() local model_config = models[opts.model] if not model_config then @@ -309,7 +328,7 @@ function Client:ask(prompt, opts) if not provider_name then error('Provider not found for model: ' .. opts.model) end - local provider = providers[provider_name] + local provider = self:get_providers():get(provider_name) if not provider then error('Provider not found: ' .. provider_name) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 54f4c6ae..fe2f4773 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -66,14 +66,15 @@ function M.class(fn, parent) return out end ----@class OrderedMap +---@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 +---@generic K, V +---@return OrderedMap function M.ordered_map() return { _keys = {}, From 1b04ddcfe2d04363a3898998a1005ab2f493dff4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 22:18:59 +0200 Subject: [PATCH 497/728] refactor(chat): move completion logic to separate module (#1267) This change refactors the chat completion logic by moving it from init.lua into a new completion.lua module. It also updates mappings and setup to use the new module, improving code organization and maintainability. Autocomplete setup is now handled in the completion module, and prompt listing is extracted to a helper function. Signed-off-by: Tomas Slusny --- README.md | 8 +- lua/CopilotChat/completion.lua | 235 +++++++++++++++++++++++++ lua/CopilotChat/config/mappings.lua | 2 +- lua/CopilotChat/init.lua | 256 +++------------------------- 4 files changed, 265 insertions(+), 236 deletions(-) create mode 100644 lua/CopilotChat/completion.lua diff --git a/README.md b/README.md index 5bdfac2d..4177c1e9 100644 --- a/README.md +++ b/README.md @@ -436,16 +436,10 @@ chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector -chat.prompts() -- Get all available prompts - --- Completion -chat.trigger_complete() -- Trigger completion in chat window -chat.complete_info() -- Get completion info for custom providers -chat.complete_items() -- Get completion items (WARN: async, requires plenary.async.run) -- History Management -chat.save(name, history_path) -- Save chat history chat.load(name, history_path) -- Load chat history +chat.save(name, history_path) -- Save chat history -- Configuration chat.setup(config) -- Update configuration diff --git a/lua/CopilotChat/completion.lua b/lua/CopilotChat/completion.lua new file mode 100644 index 00000000..61e8bde1 --- /dev/null +++ b/lua/CopilotChat/completion.lua @@ -0,0 +1,235 @@ +local async = require('plenary.async') +local client = require('CopilotChat.client') +local config = require('CopilotChat.config') +local functions = require('CopilotChat.functions') +local utils = require('CopilotChat.utils') + +local M = {} + +--- Get the completion info for the chat window, for use with custom completion providers +---@return table +function M.info() + return { + triggers = { '@', '/', '#', '$' }, + pattern = [[\%(@\|/\|#\|\$\)\S*]], + } +end + +--- Get the completion items for the chat window, for use with custom completion providers +---@return table +---@async +function M.items() + local models = client:models() + local prompts = config.prompts or {} + local items = {} + + for name, prompt in pairs(prompts) do + if type(prompt) == 'string' then + prompt = { + prompt = prompt, + } + end + + 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, + abbr = name, + kind = kind, + info = info, + menu = prompt.description or '', + icase = 1, + dup = 0, + empty = 0, + } + end + + for id, model in pairs(models) do + items[#items + 1] = { + word = '$' .. id, + abbr = id, + kind = model.provider, + menu = model.name, + icase = 1, + dup = 0, + empty = 0, + } + end + + local groups = {} + for name, tool in pairs(config.functions) do + if tool.group then + groups[tool.group] = groups[tool.group] or {} + groups[tool.group][name] = tool + end + end + for name, group in pairs(groups) do + local group_tools = vim.tbl_keys(group) + items[#items + 1] = { + word = '@' .. name, + abbr = name, + kind = 'group', + info = table.concat(group_tools, '\n'), + menu = string.format('%s tools', #group_tools), + icase = 1, + dup = 0, + empty = 0, + } + end + for name, tool in pairs(config.functions) do + items[#items + 1] = { + word = '@' .. name, + abbr = name, + kind = 'tool', + info = tool.description, + menu = tool.group or '', + icase = 1, + dup = 0, + empty = 0, + } + end + + local tools_to_use = functions.parse_tools(config.functions) + for _, tool in pairs(tools_to_use) do + local uri = config.functions[tool.name].uri + if uri then + local info = + string.format('%s\n\n%s', tool.description, tool.schema and vim.inspect(tool.schema, { indent = ' ' }) or '') + + items[#items + 1] = { + word = '#' .. tool.name, + abbr = tool.name, + kind = config.functions[tool.name].group or 'resource', + info = info, + menu = uri, + icase = 1, + dup = 0, + empty = 0, + } + end + 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) + + return items +end + +--- Trigger the completion for the chat window. +---@param without_input boolean? +function M.complete(without_input) + local source = require('CopilotChat').get_source() + local info = M.info() + local bufnr = vim.api.nvim_get_current_buf() + local line = vim.api.nvim_get_current_line() + local win = vim.api.nvim_get_current_win() + local row, col = unpack(vim.api.nvim_win_get_cursor(win)) + + local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern)) + if not prefix then + return + end + + if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then + local found_tool = config.functions[prefix:sub(2, -2)] + local found_schema = found_tool and functions.parse_schema(found_tool) + if found_tool and found_schema then + async.run(function() + local value = functions.enter_input(found_schema, source) + if not value then + return + end + + utils.schedule_main() + vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value }) + vim.api.nvim_win_set_cursor(0, { row, col + #value }) + end) + end + + return + end + + utils.debounce('copilot_chat_complete', function() + async.run(function() + local items = M.items() + utils.schedule_main() + + local row_changed = vim.api.nvim_win_get_cursor(win)[1] ~= row + local mode = vim.api.nvim_get_mode().mode + if row_changed or not (mode == 'i' or mode == 'ic') then + return + end + + vim.fn.complete( + cmp_start + 1, + vim.tbl_filter(function(item) + return vim.startswith(item.word:lower(), prefix:lower()) + end, items) + ) + end) + end, 100) +end + +--- Omnifunc for the chat window completion. +---@param findstart integer 0 or 1, decides behavior +---@param base integer findstart=0, text to match against +---@param _ any +---@return number|table +function M.omnifunc(findstart, base) + assert(base) + local bufnr = vim.api.nvim_get_current_buf() + local ft = vim.bo[bufnr].filetype + + if ft ~= 'copilot-chat' then + return findstart == 1 and -1 or {} + end + + M.complete(true) + return -2 -- Return -2 to indicate that we are handling the completion asynchronously +end + +--- Enable the completion for specific buffer. +---@param bufnr number: the buffer number to enable completion for +---@param autocomplete boolean: whether to enable autocomplete +function M.enable(bufnr, autocomplete) + if autocomplete then + vim.api.nvim_create_autocmd('TextChangedI', { + buffer = bufnr, + callback = function() + local completeopt = vim.opt.completeopt:get() + if not vim.tbl_contains(completeopt, 'noinsert') and not vim.tbl_contains(completeopt, 'noselect') then + -- Don't trigger completion if completeopt is not set to noinsert or noselect + return + end + + M.complete(true) + end, + }) + + -- Add noinsert completeopt if not present + if vim.fn.has('nvim-0.11.0') == 1 then + local completeopt = vim.opt.completeopt:get() + if not vim.tbl_contains(completeopt, 'noinsert') then + table.insert(completeopt, 'noinsert') + vim.bo[bufnr].completeopt = table.concat(completeopt, ',') + end + end + else + -- Just set the omnifunc for the buffer + vim.bo[bufnr].omnifunc = [[v:lua.require'CopilotChat.completion'.omnifunc]] + end +end + +return M diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 2421c264..af14f004 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -139,7 +139,7 @@ return { complete = { insert = '', callback = function() - copilot.trigger_complete() + require('CopilotChat.completion').complete() end, }, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 64558190..0886b412 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -170,6 +170,29 @@ local function list_models() end, result) end +--- List available prompts. +---@return table +local function list_prompts() + local prompts_to_use = {} + + for name, prompt in pairs(M.config.prompts) do + local val = prompt + if type(prompt) == 'string' then + val = { + prompt = prompt, + } + end + + if val.system_prompt and M.config.prompts[val.system_prompt] then + val.system_prompt = M.config.prompts[val.system_prompt].system_prompt + end + + prompts_to_use[name] = val + end + + return prompts_to_use +end + --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) @@ -456,7 +479,7 @@ function M.resolve_prompt(prompt, config) end end - local prompts_to_use = M.prompts() + local prompts_to_use = list_prompts() local depth = 0 local MAX_DEPTH = 10 @@ -604,198 +627,6 @@ function M.set_selection(bufnr, start_line, end_line, clear) update_highlights() end ---- Trigger the completion for the chat window. ----@param without_input boolean? -function M.trigger_complete(without_input) - 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 not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then - local found_tool = M.config.functions[prefix:sub(2, -2)] - local found_schema = found_tool and functions.parse_schema(found_tool) - if found_tool and found_schema then - async.run(function() - local value = functions.enter_input(found_schema, state.source) - if not value then - return - end - - utils.schedule_main() - vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value }) - vim.api.nvim_win_set_cursor(0, { row, col + #value }) - end) - end - - return - end - - async.run(function() - local items = M.complete_items() - utils.schedule_main() - - if vim.fn.mode() ~= 'i' then - return - end - - 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() - return { - triggers = { '@', '/', '#', '$' }, - pattern = [[\%(@\|/\|#\|\$\)\S*]], - } -end - ---- Get the completion items for the chat window, for use with custom completion providers ----@return table ----@async -function M.complete_items() - local models = list_models() - local prompts_to_use = M.prompts() - 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, - abbr = name, - kind = kind, - info = info, - menu = prompt.description or '', - icase = 1, - dup = 0, - empty = 0, - } - end - - for _, model in ipairs(models) do - items[#items + 1] = { - word = '$' .. model.id, - abbr = model.id, - kind = model.provider, - menu = model.name, - icase = 1, - dup = 0, - empty = 0, - } - end - - local groups = {} - for name, tool in pairs(M.config.functions) do - if tool.group then - groups[tool.group] = groups[tool.group] or {} - groups[tool.group][name] = tool - end - end - for name, group in pairs(groups) do - local group_tools = vim.tbl_keys(group) - items[#items + 1] = { - word = '@' .. name, - abbr = name, - kind = 'group', - info = table.concat(group_tools, '\n'), - menu = string.format('%s tools', #group_tools), - icase = 1, - dup = 0, - empty = 0, - } - end - for name, tool in pairs(M.config.functions) do - items[#items + 1] = { - word = '@' .. name, - abbr = name, - kind = 'tool', - info = tool.description, - menu = tool.group or '', - icase = 1, - dup = 0, - empty = 0, - } - end - - local tools_to_use = functions.parse_tools(M.config.functions) - for _, tool in pairs(tools_to_use) do - local uri = M.config.functions[tool.name].uri - if uri then - local info = - string.format('%s\n\n%s', tool.description, tool.schema and vim.inspect(tool.schema, { indent = ' ' }) or '') - - items[#items + 1] = { - word = '#' .. tool.name, - abbr = tool.name, - kind = M.config.functions[tool.name].group or 'resource', - info = info, - menu = uri, - icase = 1, - dup = 0, - empty = 0, - } - end - 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) - - return items -end - ---- Get the prompts to use. ----@return table -function M.prompts() - local prompts_to_use = {} - - for name, prompt in pairs(M.config.prompts) do - local val = prompt - if type(prompt) == 'string' then - val = { - prompt = prompt, - } - end - - if val.system_prompt and M.config.prompts[val.system_prompt] then - val.system_prompt = M.config.prompts[val.system_prompt].system_prompt - end - - prompts_to_use[name] = val - end - - return prompts_to_use -end - --- Open the chat window. ---@param config CopilotChat.config.Shared? function M.open(config) @@ -888,7 +719,7 @@ end --- Select a prompt template to use. ---@param config CopilotChat.config.Shared? function M.select_prompt(config) - local prompts = M.prompts() + local prompts = list_prompts() local keys = vim.tbl_keys(prompts) table.sort(keys) @@ -1236,6 +1067,8 @@ function M.setup(config) map_key(name, bufnr) end + require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { buffer = bufnr, callback = function(ev) @@ -1258,44 +1091,11 @@ function M.setup(config) }) end - if M.config.chat_autocomplete then - vim.api.nvim_create_autocmd('TextChangedI', { - buffer = bufnr, - callback = function() - local completeopt = vim.opt.completeopt:get() - if not vim.tbl_contains(completeopt, 'noinsert') and not vim.tbl_contains(completeopt, 'noselect') then - -- Don't trigger completion if completeopt is not set to noinsert or noselect - return - end - - 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('complete', function() - M.trigger_complete(true) - end, 100) - end - end, - }) - - -- Add noinsert completeopt if not present - if vim.fn.has('nvim-0.11.0') == 1 then - local completeopt = vim.opt.completeopt:get() - if not vim.tbl_contains(completeopt, 'noinsert') then - table.insert(completeopt, 'noinsert') - vim.bo[bufnr].completeopt = table.concat(completeopt, ',') - end - end - end - finish(true) end ) - for name, prompt in pairs(M.prompts()) do + for name, prompt in pairs(list_prompts()) do if prompt.prompt then vim.api.nvim_create_user_command('CopilotChat' .. name, function(args) local input = prompt.prompt From c0580d1d043f8222a975f9c049c9bad94dee98a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 8 Aug 2025 20:19:16 +0000 Subject: [PATCH 498/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index c3b52a5c..4b3b0700 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -511,16 +511,10 @@ CORE *CopilotChat-core* -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector - chat.prompts() -- Get all available prompts - - -- Completion - chat.trigger_complete() -- Trigger completion in chat window - chat.complete_info() -- Get completion info for custom providers - chat.complete_items() -- Get completion items (WARN: async, requires plenary.async.run) -- History Management - chat.save(name, history_path) -- Save chat history chat.load(name, history_path) -- Load chat history + chat.save(name, history_path) -- Save chat history -- Configuration chat.setup(config) -- Update configuration From 90c324177b33aec6d4c2bd5043c26bfc9fbc081f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 23:09:45 +0200 Subject: [PATCH 499/728] fix(info): show resource uri instead of name in preview (#1296) Previously, the resource preview header displayed the resource name, which could be missing. This change updates the header to show the resource URI, providing clearer context. Also, removes unused type annotations for improved code clarity. --- lua/CopilotChat/completion.lua | 1 - lua/CopilotChat/config/mappings.lua | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/CopilotChat/completion.lua b/lua/CopilotChat/completion.lua index 61e8bde1..9874999b 100644 --- a/lua/CopilotChat/completion.lua +++ b/lua/CopilotChat/completion.lua @@ -185,7 +185,6 @@ end --- Omnifunc for the chat window completion. ---@param findstart integer 0 or 1, decides behavior ---@param base integer findstart=0, text to match against ----@param _ any ---@return number|table function M.omnifunc(findstart, base) assert(base) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index af14f004..4fcf03c7 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -133,7 +133,6 @@ end ---@field yank_diff CopilotChat.config.mapping.yank_diff|false|nil ---@field show_diff CopilotChat.config.mapping.show_diff|false|nil ---@field show_info CopilotChat.config.mapping|false|nil ----@field show_context CopilotChat.config.mapping|false|nil ---@field show_help CopilotChat.config.mapping|false|nil return { complete = { @@ -513,7 +512,7 @@ return { for _, resource in ipairs(resolved_resources) do local resource_lines = vim.split(resource.data, '\n') local preview = vim.list_slice(resource_lines, 1, math.min(10, #resource_lines)) - local header = string.format('**%s** (%s lines)', resource.name, #resource_lines) + local header = string.format('**%s** (%s lines)', resource.uri, #resource_lines) if #resource_lines > 10 then header = header .. ' (truncated)' end From 27c24c4590ec33d9fa849a96dd95bc1774191be5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 8 Aug 2025 23:34:32 +0200 Subject: [PATCH 500/728] refactor(chat): unify message/block API for cursor access (#1298) Refactored chat window API to use `get_message(role, cursor)` and `get_block(role, cursor)` for both last and cursor-closest access. Removed legacy `get_closest_message` and `get_closest_block` methods. Updated mappings and documentation to reflect unified API. This simplifies usage and improves consistency for message/block retrieval and removal actions. --- README.md | 8 ++-- lua/CopilotChat/config/mappings.lua | 14 +++---- lua/CopilotChat/ui/chat.lua | 64 ++++++++++++++--------------- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4177c1e9..f4111a0f 100644 --- a/README.md +++ b/README.md @@ -458,8 +458,10 @@ window:visible() -- Check if chat window is visible window:focused() -- Check if chat window is focused -- Message Management -window:get_message(role) -- Get last chat message by role (user, assistant, tool) +window:get_message(role, cursor) -- Get chat message by role, either last or closest to cursor window:add_message({ role, content }, replace) -- Add or replace a message in chat +window:remove_message(role, cursor) -- Remove chat message by role, either last or closest to cursor +window:get_block(role, cursor) -- Get code block by role, either last or closest to cursor window:add_sticky(sticky) -- Add sticky prompt to chat message -- Content Management @@ -473,9 +475,7 @@ window:follow() -- Move cursor to end of chat content window:focus() -- Focus the chat window -- Advanced Features -window:get_closest_message(role) -- Get message closest to cursor -window:get_closest_block(role) -- Get code block closest to cursor -window:overlay(opts) -- Show overlay with specified options +window:overlay(opts) -- Show overlay with specified options ``` ## Example Usage diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 4fcf03c7..5f411473 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -162,7 +162,7 @@ return { normal = '', insert = '', callback = function() - local message = copilot.chat:get_closest_message('user') + local message = copilot.chat:get_message('user', true) if not message then return end @@ -234,7 +234,7 @@ return { normal = '', insert = '', callback = function(source) - local diff = get_diff(copilot.chat:get_closest_block()) + local diff = get_diff(copilot.chat:get_block('assistant', true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -249,7 +249,7 @@ return { jump_to_diff = { normal = 'gj', callback = function(source) - local diff = get_diff(copilot.chat:get_closest_block()) + local diff = get_diff(copilot.chat:get_block('assistant', true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -326,7 +326,7 @@ return { normal = 'gy', register = '"', -- Default register to use for yanking callback = function() - local block = copilot.chat:get_closest_block() + local block = copilot.chat:get_block('assistant', true) if not block then return end @@ -339,7 +339,7 @@ return { normal = 'gd', full_diff = false, -- Show full diff instead of unified diff when showing diff window callback = function(source) - local diff = get_diff(copilot.chat:get_closest_block()) + local diff = get_diff(copilot.chat:get_block('assistant', true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -356,7 +356,7 @@ return { -- Apply all diffs from same file if #modified > 0 then -- Find all diffs from the same file in this section - local message = copilot.chat:get_closest_message('assistant') + local message = copilot.chat:get_message('assistant', true) local section = message and message.section local same_file_diffs = {} if section then @@ -429,7 +429,7 @@ return { show_info = { normal = 'gc', callback = function(source) - local message = copilot.chat:get_closest_message('user') + local message = copilot.chat:get_message('user', true) if not message then return end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 2752e6d0..bb4492b1 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -119,35 +119,11 @@ function Chat:focused() return self:visible() and vim.api.nvim_get_current_win() == self.winnr end ---- Get the closest message to the cursor. ----@param role string? If specified, only considers sections of the given role ----@return CopilotChat.ui.chat.Message? -function Chat:get_closest_message(role) - if not self:visible() then - return nil - end - - self:render() - local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) - local cursor_line = cursor_pos[1] - local closest_message = nil - local max_line_below_cursor = -1 - - for _, message in ipairs(self.messages) do - local section = message.section - local matches_role = not role or message.role == role - if matches_role and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then - max_line_below_cursor = section.start_line - closest_message = message - end - end - - return closest_message -end - --- Get the closest code block to the cursor. +---@param role string? If specified, only considers sections of the given role +---@param cursor boolean? If true, returns the block closest to the cursor position ---@return CopilotChat.ui.chat.Block? -function Chat:get_closest_block() +function Chat:get_block(role, cursor) if not self:visible() then return nil end @@ -172,10 +148,31 @@ function Chat:get_closest_block() end --- Get last message by role in the chat window. +---@param role string? If specified, only considers sections of the given role +---@param cursor boolean? If true, returns the message closest to the cursor position ---@return CopilotChat.ui.chat.Message? -function Chat:get_message(role) - if not self:visible() then - return +function Chat:get_message(role, cursor) + if cursor then + if not self:visible() then + return nil + end + + self:render() + local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) + local cursor_line = cursor_pos[1] + local closest_message = nil + local max_line_below_cursor = -1 + + for _, message in ipairs(self.messages) do + local section = message.section + local matches_role = not role or message.role == role + if matches_role and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then + max_line_below_cursor = section.start_line + closest_message = message + end + end + + return closest_message end for i = #self.messages, 1, -1 do @@ -475,14 +472,15 @@ function Chat:add_message(message, replace) end --- Remove a message from the chat window by role. ----@param role string -function Chat:remove_message(role) +---@param role string? If specified, only considers sections of the given role +---@param cursor boolean? If true, removes the message closest to the cursor position +function Chat:remove_message(role, cursor) if not self:visible() then return end self:render() - local message = self:get_closest_message(role) + local message = self:get_message(role, cursor) if not message then return end From 77971bb00f057ca15630a96bcdb5f25c5ec61287 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 8 Aug 2025 21:34:53 +0000 Subject: [PATCH 501/728] 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 4b3b0700..ed8df417 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -534,8 +534,10 @@ You can also access the chat window UI methods through the `chat.chat` object: window:focused() -- Check if chat window is focused -- Message Management - window:get_message(role) -- Get last chat message by role (user, assistant, tool) + window:get_message(role, cursor) -- Get chat message by role, either last or closest to cursor window:add_message({ role, content }, replace) -- Add or replace a message in chat + window:remove_message(role, cursor) -- Remove chat message by role, either last or closest to cursor + window:get_block(role, cursor) -- Get code block by role, either last or closest to cursor window:add_sticky(sticky) -- Add sticky prompt to chat message -- Content Management @@ -549,9 +551,7 @@ You can also access the chat window UI methods through the `chat.chat` object: window:focus() -- Focus the chat window -- Advanced Features - window:get_closest_message(role) -- Get message closest to cursor - window:get_closest_block(role) -- Get code block closest to cursor - window:overlay(opts) -- Show overlay with specified options + window:overlay(opts) -- Show overlay with specified options < From 92777fb98ad4de7496188f1e9de336d16871ac43 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 9 Aug 2025 00:01:52 +0200 Subject: [PATCH 502/728] feat(ui): show assistant reasoning as virtual text (#1299) Adds support for displaying the assistant's reasoning above messages in the chat UI as virtual text. Reasoning is now streamed and stored separately from content, and models with reasoning capability are indicated in the model selector. This improves transparency of model responses and debugging. --- lua/CopilotChat/client.lua | 39 +++++++++++++++++++--------- lua/CopilotChat/config/providers.lua | 4 +++ lua/CopilotChat/init.lua | 11 ++++---- lua/CopilotChat/ui/chat.lua | 21 ++++++++++++++- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 146169fb..abd41872 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -7,11 +7,12 @@ ---@field system_prompt string ---@field model string ---@field temperature number ----@field on_progress? fun(response: string):nil +---@field on_progress fun(response: CopilotChat.client.Message)? ---@class CopilotChat.client.Message ---@field role string ---@field content string +---@field reasoning string? ---@field tool_call_id string? ---@field tool_calls table? @@ -46,6 +47,7 @@ ---@field max_output_tokens number? ---@field streaming boolean? ---@field tools boolean? +---@field reasoning boolean? local log = require('plenary.log') local tiktoken = require('CopilotChat.tiktoken') @@ -402,15 +404,15 @@ function Client:ask(prompt, opts) end end - local errored = false + local errored = nil local finished = false local token_count = 0 - local response_buffer = utils.string_buffer() + local response_content_buffer = utils.string_buffer() + local response_reasoning_buffer = utils.string_buffer() local function finish_stream(err, job) if err then - errored = true - response_buffer:set(err) + errored = err end log.debug('Finishing stream', err) @@ -460,10 +462,19 @@ function Client:ask(prompt, opts) end if out.content then - response_buffer:add(out.content) - if opts.on_progress then - opts.on_progress(out.content) - end + response_content_buffer:add(out.content) + end + + if out.reasoning then + response_reasoning_buffer:add(out.reasoning) + end + + if opts.on_progress then + opts.on_progress({ + role = 'assistant', + content = out.content or '', + reasoning = out.reasoning or '', + }) end if out.finish_reason then @@ -562,12 +573,14 @@ function Client:ask(prompt, opts) return end - local response_text = response_buffer:tostring() if errored then - error(response_text) + error(errored) return end + local response_text = response_content_buffer:tostring() + local response_reasoning = response_reasoning_buffer:tostring() + if response then if is_stream then if utils.empty(response_text) and not finished then @@ -578,13 +591,15 @@ function Client:ask(prompt, opts) else parse_line(response.body) end - response_text = response_buffer:tostring() + response_text = response_content_buffer:tostring() + response_reasoning = response_reasoning_buffer:tostring() end return { message = { role = 'assistant', content = response_text, + reasoning = response_reasoning, tool_calls = #tool_calls:values() > 0 and tool_calls:values() or nil, }, token_count = token_count, diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5356925a..e08f5fdf 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -191,6 +191,7 @@ end ---@class CopilotChat.config.providers.Output ---@field content string +---@field reasoning string? ---@field finish_reason string? ---@field total_tokens number? ---@field tool_calls table @@ -429,6 +430,7 @@ M.copilot = { local message = choice.message or choice.delta local content = message and message.content + local reasoning = message and (message.reasoning or message.reasoning_content) local usage = choice.usage and choice.usage.total_tokens if not usage then usage = output.usage and output.usage.total_tokens @@ -437,6 +439,7 @@ M.copilot = { return { content = content, + reasoning = reasoning, finish_reason = finish_reason, total_tokens = usage, tool_calls = tool_calls, @@ -480,6 +483,7 @@ M.github_models = { max_output_tokens = max_output_tokens, streaming = vim.tbl_contains(model.capabilities, 'streaming'), tools = vim.tbl_contains(model.capabilities, 'tool-calling'), + reasoning = vim.tbl_contains(model.capabilities, 'reasoning'), version = model.version, } end) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0886b412..c08667af 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -677,6 +677,7 @@ function M.select_model() provider = model.provider, streaming = model.streaming, tools = model.tools, + reasoning = model.reasoning, selected = model.id == M.config.model, } end, models) @@ -701,6 +702,9 @@ function M.select_model() if item.tools then table.insert(indicators, 'tools') end + if item.reasoning then + table.insert(indicators, 'reasoning') + end if #indicators > 0 then out = out .. ' [' .. table.concat(indicators, ', ') .. ']' @@ -865,12 +869,9 @@ function M.ask(prompt, config) system_prompt = system_prompt, model = selected_model, temperature = config.temperature, - on_progress = vim.schedule_wrap(function(token) + on_progress = vim.schedule_wrap(function(message) if not config.headless then - M.chat:add_message({ - content = token, - role = 'assistant', - }) + M.chat:add_message(message) end end), }) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index bb4492b1..34137ec1 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -716,8 +716,8 @@ function Chat:render() -- Replace self.messages with new_messages (preserving tool_calls, etc.) self.messages = new_messages - -- Show tool call details as virt lines for _, message in ipairs(self.messages) do + -- Show tool call details as virt lines if message.tool_calls and #message.tool_calls > 0 then local section = message.section if section and section.end_line then @@ -751,6 +751,25 @@ function Chat:render() }) end end + + -- Show reasoning as virtual text above assistant messages + if + message.role == 'assistant' + and not utils.empty(message.reasoning) + and message.section + and message.section.start_line + then + local virt_lines = {} + for _, line in ipairs(vim.split(message.reasoning, '\n')) do + table.insert(virt_lines, { { 'Reasoning: ' .. line, 'CopilotChatAnnotation' } }) + end + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, message.section.start_line - 1, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + priority = 100, + strict = false, + }) + end end -- Show help as before, using last user message From 7e027df6e95b622da25282285e84a9fc3806dcf1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 9 Aug 2025 03:52:20 +0200 Subject: [PATCH 503/728] fix(chat): correct block selection logic by cursor (#1301) Refactored Chat:get_block to properly select the closest block to the cursor, considering the role filter and ensuring correct fallback to the last matching block. This improves accuracy when interacting with chat blocks in the UI. --- lua/CopilotChat/ui/chat.lua | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 34137ec1..4a549c6c 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -124,27 +124,40 @@ end ---@param cursor boolean? If true, returns the block closest to the cursor position ---@return CopilotChat.ui.chat.Block? function Chat:get_block(role, cursor) - if not self:visible() then - return nil - end + if cursor then + if not self:visible() then + 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 - local max_line_below_cursor = -1 - - for _, message in pairs(self.messages) do - local section = message.section - 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 + self:render() + 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 _, message in ipairs(self.messages) do + local section = message.section + local matches_role = not role or message.role == role + if matches_role and section and section.blocks then + 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 end + + return closest_block end - return closest_block + for i = #self.messages, 1, -1 do + local message = self.messages[i] + local matches_role = not role or message.role == role + if matches_role and message.section and message.section.blocks and #message.section.blocks > 0 then + return message.section.blocks[#message.section.blocks] + end + end end --- Get last message by role in the chat window. From 45c923d172af6ce32dc7e5f2ed1e328f0875c8e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 9 Aug 2025 01:52:39 +0000 Subject: [PATCH 504/728] 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 ed8df417..349cdeeb 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 08 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 6d49da8e211dcb254a10ecd036b1bfdc8838f970 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 9 Aug 2025 04:49:10 +0200 Subject: [PATCH 505/728] refactor(constants): centralize role and plugin name constants (#1302) Move role strings and plugin name to a dedicated constants module. Refactor all usages to reference the new constants. This improves maintainability and reduces duplication across the codebase. --- lua/CopilotChat/client.lua | 15 ++++++------ lua/CopilotChat/completion.lua | 7 +++--- lua/CopilotChat/config/mappings.lua | 21 ++++++++-------- lua/CopilotChat/config/providers.lua | 5 ++-- lua/CopilotChat/constants.lua | 10 ++++++++ lua/CopilotChat/init.lua | 36 ++++++++++++++-------------- lua/CopilotChat/ui/chat.lua | 9 +++---- 7 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 lua/CopilotChat/constants.lua diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index abd41872..f2dba5b0 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -50,8 +50,9 @@ ---@field reasoning boolean? local log = require('plenary.log') -local tiktoken = require('CopilotChat.tiktoken') +local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') +local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local class = utils.class @@ -124,7 +125,7 @@ local function generate_selection_message(selection) selection.start_line, selection.end_line ), - role = 'user', + role = constants.ROLE.USER, } end @@ -140,7 +141,7 @@ local function generate_resource_messages(resources) :map(function(resource) return { content = generate_resource_block(resource.data, resource.mimetype, resource.uri, resource.name, 1, nil), - role = 'user', + role = constants.ROLE.USER, } end) :totable() @@ -160,7 +161,7 @@ local function generate_ask_request(prompt, system_prompt, history, generated_me if not utils.empty(system_prompt) then table.insert(messages, { content = system_prompt, - role = 'system', + role = constants.ROLE.SYSTEM, }) end @@ -172,7 +173,7 @@ local function generate_ask_request(prompt, system_prompt, history, generated_me if not utils.empty(prompt) and utils.empty(history) then table.insert(messages, { content = prompt, - role = 'user', + role = constants.ROLE.USER, }) end @@ -471,7 +472,7 @@ function Client:ask(prompt, opts) if opts.on_progress then opts.on_progress({ - role = 'assistant', + role = constants.ROLE.ASSISTANT, content = out.content or '', reasoning = out.reasoning or '', }) @@ -597,7 +598,7 @@ function Client:ask(prompt, opts) return { message = { - role = 'assistant', + role = constants.ROLE.ASSISTANT, content = response_text, reasoning = response_reasoning, tool_calls = #tool_calls:values() > 0 and tool_calls:values() or nil, diff --git a/lua/CopilotChat/completion.lua b/lua/CopilotChat/completion.lua index 9874999b..a32141eb 100644 --- a/lua/CopilotChat/completion.lua +++ b/lua/CopilotChat/completion.lua @@ -1,5 +1,6 @@ local async = require('plenary.async') local client = require('CopilotChat.client') +local constants = require('CopilotChat.constants') local config = require('CopilotChat.config') local functions = require('CopilotChat.functions') local utils = require('CopilotChat.utils') @@ -33,10 +34,10 @@ function M.items() local kind = '' local info = '' if prompt.prompt then - kind = 'user' + kind = constants.ROLE.USER info = prompt.prompt elseif prompt.system_prompt then - kind = 'system' + kind = constants.ROLE.SYSTEM info = prompt.system_prompt end @@ -88,7 +89,7 @@ function M.items() items[#items + 1] = { word = '@' .. name, abbr = name, - kind = 'tool', + kind = constants.ROLE.TOOL, info = tool.description, menu = tool.group or '', icase = 1, diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 5f411473..4928a876 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,6 +1,7 @@ local async = require('plenary.async') local copilot = require('CopilotChat') local client = require('CopilotChat.client') +local constants = require('CopilotChat.constants') local utils = require('CopilotChat.utils') ---@class CopilotChat.config.mappings.Diff @@ -162,7 +163,7 @@ return { normal = '', insert = '', callback = function() - local message = copilot.chat:get_message('user', true) + local message = copilot.chat:get_message(constants.ROLE.USER, true) if not message then return end @@ -174,7 +175,7 @@ return { toggle_sticky = { normal = 'grr', callback = function() - local message = copilot.chat:get_message('user') + local message = copilot.chat:get_message(constants.ROLE.USER) local section = message and message.section if not section then return @@ -205,7 +206,7 @@ return { clear_stickies = { normal = 'grx', callback = function() - local message = copilot.chat:get_message('user') + local message = copilot.chat:get_message(constants.ROLE.USER) local section = message and message.section if not section then return @@ -234,7 +235,7 @@ return { normal = '', insert = '', callback = function(source) - local diff = get_diff(copilot.chat:get_block('assistant', true)) + local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -249,7 +250,7 @@ return { jump_to_diff = { normal = 'gj', callback = function(source) - local diff = get_diff(copilot.chat:get_block('assistant', true)) + local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -264,7 +265,7 @@ return { callback = function() local items = {} for i, message in ipairs(copilot.chat.messages) do - if message.section and message.role == 'assistant' then + if message.section and message.role == constants.ROLE.ASSISTANT then local prev_message = copilot.chat.messages[i - 1] local text = '' if prev_message then @@ -326,7 +327,7 @@ return { normal = 'gy', register = '"', -- Default register to use for yanking callback = function() - local block = copilot.chat:get_block('assistant', true) + local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) if not block then return end @@ -339,7 +340,7 @@ return { normal = 'gd', full_diff = false, -- Show full diff instead of unified diff when showing diff window callback = function(source) - local diff = get_diff(copilot.chat:get_block('assistant', true)) + local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -356,7 +357,7 @@ return { -- Apply all diffs from same file if #modified > 0 then -- Find all diffs from the same file in this section - local message = copilot.chat:get_message('assistant', true) + local message = copilot.chat:get_message(constants.ROLE.ASSISTANT, true) local section = message and message.section local same_file_diffs = {} if section then @@ -429,7 +430,7 @@ return { show_info = { normal = 'gc', callback = function(source) - local message = copilot.chat:get_message('user', true) + local message = copilot.chat:get_message(constants.ROLE.USER, true) if not message then return end diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index e08f5fdf..b2a03eea 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,3 +1,4 @@ +local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local plenary_utils = require('plenary.async.util') @@ -342,8 +343,8 @@ M.copilot = { } if is_o1 then - if input.role == 'system' then - output.role = 'user' + if input.role == constants.ROLE.SYSTEM then + output.role = constants.ROLE.USER end end diff --git a/lua/CopilotChat/constants.lua b/lua/CopilotChat/constants.lua new file mode 100644 index 00000000..7c6f7561 --- /dev/null +++ b/lua/CopilotChat/constants.lua @@ -0,0 +1,10 @@ +return { + PLUGIN_NAME = 'CopilotChat', + + ROLE = { + USER = 'user', + ASSISTANT = 'assistant', + SYSTEM = 'system', + TOOL = 'tool', + }, +} diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c08667af..90a4209d 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -2,10 +2,10 @@ local async = require('plenary.async') local log = require('plenary.log') local functions = require('CopilotChat.functions') local client = require('CopilotChat.client') +local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') -local PLUGIN_NAME = 'CopilotChat' local WORD = '([^%s:]+)' local WORD_NO_INPUT = '([^%s]+)' local WORD_WITH_INPUT_QUOTED = WORD .. ':`([^`]+)`' @@ -44,7 +44,7 @@ local state = { ---@param prompt string ---@param config CopilotChat.config.Shared local function insert_sticky(prompt, config) - local existing_prompt = M.chat:get_message('user') + local existing_prompt = M.chat:get_message(constants.ROLE.USER) local combined_prompt = (existing_prompt and existing_prompt.content or '') .. '\n' .. (prompt or '') local lines = vim.split(prompt or '', '\n') local stickies = utils.ordered_map() @@ -207,7 +207,7 @@ local function finish(start_of_chat) end local prompt_content = '' - local assistant_message = M.chat:get_message('assistant') + local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) local tool_calls = assistant_message and assistant_message.tool_calls or {} if not utils.empty(state.sticky) then @@ -225,7 +225,7 @@ local function finish(start_of_chat) end M.chat:add_message({ - role = 'user', + role = constants.ROLE.USER, content = prompt_content, }) @@ -253,7 +253,7 @@ local function handle_error(config, cb) out = utils.make_string(out) M.chat:add_message({ - role = 'assistant', + role = constants.ROLE.ASSISTANT, content = '\n' .. string.format(BLOCK_OUTPUT_FORMAT, 'error', out) .. '\n', }) @@ -282,7 +282,7 @@ local function map_key(name, bufnr, fn) 'n', key.normal, fn, - { buffer = bufnr, nowait = true, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') } + { buffer = bufnr, nowait = true, desc = constants.PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') } ) end if key.insert and key.insert ~= '' then @@ -296,7 +296,7 @@ local function map_key(name, bufnr, fn) else fn() end - end, { buffer = bufnr, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') }) + end, { buffer = bufnr, desc = constants.PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') }) end end @@ -473,7 +473,7 @@ end ---@return CopilotChat.config.prompts.Prompt, string function M.resolve_prompt(prompt, config) if not prompt then - local message = M.chat:get_message('user') + local message = M.chat:get_message(constants.ROLE.USER) if message then prompt = message.content end @@ -636,12 +636,12 @@ function M.open(config) M.chat:open(config) -- Add sticky values from provided config when opening the chat - local message = M.chat:get_message('user') + local message = M.chat:get_message(constants.ROLE.USER) if message then local prompt = insert_sticky(message.content, config) if prompt then M.chat:add_message({ - role = 'user', + role = constants.ROLE.USER, content = '\n' .. prompt, }, true) end @@ -813,7 +813,7 @@ function M.ask(prompt, config) if not config.headless then utils.schedule_main() - local assistant_message = M.chat:get_message('assistant') + local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) if assistant_message and assistant_message.tool_calls then local handled_ids = {} for _, tool in ipairs(resolved_tools) do @@ -834,11 +834,11 @@ function M.ask(prompt, config) if not utils.empty(resolved_tools) then -- If we are handling tools, replace user message with tool results - M.chat:remove_message('user') + M.chat:remove_message(constants.ROLE.USER) for _, tool in ipairs(resolved_tools) do M.chat:add_message({ id = tool.id, - role = 'tool', + role = constants.ROLE.TOOL, tool_call_id = tool.id, content = '\n' .. tool.result .. '\n', }) @@ -846,7 +846,7 @@ function M.ask(prompt, config) else -- Otherwise just replace the user message with resolved prompt M.chat:add_message({ - role = 'user', + role = constants.ROLE.USER, content = '\n' .. prompt .. '\n', }, true) end @@ -854,7 +854,7 @@ function M.ask(prompt, config) if utils.empty(prompt) and utils.empty(resolved_tools) then if not config.headless then - M.chat:remove_message('user') + M.chat:remove_message(constants.ROLE.USER) finish() end return @@ -1008,7 +1008,7 @@ function M.log_level(level) M.config.debug = level == 'debug' log.new({ - plugin = PLUGIN_NAME, + plugin = constants.PLUGIN_NAME, level = level, outfile = M.config.log_path, fmt_msg = function(is_console, mode_name, src_path, src_line, msg) @@ -1110,13 +1110,13 @@ function M.setup(config) nargs = '*', force = true, range = true, - desc = prompt.description or (PLUGIN_NAME .. ' ' .. name), + desc = prompt.description or (constants.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 (constants.PLUGIN_NAME .. ' ' .. name) }) end end end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 4a549c6c..2e784d33 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -1,5 +1,6 @@ local Overlay = require('CopilotChat.ui.overlay') local Spinner = require('CopilotChat.ui.spinner') +local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = utils.class @@ -204,7 +205,7 @@ function Chat:add_sticky(sticky) return end - local prompt = self:get_message('user') + local prompt = self:get_message(constants.ROLE.USER) if not prompt or not prompt.section then return end @@ -667,7 +668,7 @@ function Chat:render() end -- Code blocks - if current_message and current_message.role == 'assistant' then + if current_message and current_message.role == constants.ROLE.ASSISTANT then local filetype, filename, start_line, end_line = match_header(line) if filetype and filename and not current_block then current_block = { @@ -767,7 +768,7 @@ function Chat:render() -- Show reasoning as virtual text above assistant messages if - message.role == 'assistant' + message.role == constants.ROLE.ASSISTANT and not utils.empty(message.reasoning) and message.section and message.section.start_line @@ -787,7 +788,7 @@ function Chat:render() -- Show help as before, using last user message local last_message = self.messages[#self.messages] - if last_message and last_message.role == 'user' then + if last_message and last_message.role == constants.ROLE.USER then local msg = self.config.show_help and self.help or '' if self.token_count and self.token_max_count then if msg ~= '' then From f1d6bb5aa7219cfb426c2f585362b866f1dbb7d9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 9 Aug 2025 16:45:09 +0200 Subject: [PATCH 506/728] refactor(ui): simplify chat and overlay initialization (#1303) Refactored chat and overlay constructors to remove redundant help argument and use key_to_info for help and close mappings directly. This improves clarity and maintainability by centralizing mapping logic and reducing parameter complexity. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 54 +++++++++++++++++-------------------- lua/CopilotChat/ui/chat.lua | 32 +++++++++++++--------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 90a4209d..83a3d0e5 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1060,41 +1060,37 @@ function M.setup(config) M.chat:close(state.source and state.source.bufnr or nil) M.chat:delete() end - M.chat = require('CopilotChat.ui.chat')( - M.config, - utils.key_to_info('show_help', M.config.mappings.show_help), - function(bufnr) - for name, _ in pairs(M.config.mappings) do - map_key(name, bufnr) - end + M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) + for name, _ in pairs(M.config.mappings) do + map_key(name, bufnr) + end - require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) + require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) - vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { - buffer = bufnr, - callback = function(ev) - if ev.event == 'BufEnter' then - update_source() - end + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { + buffer = bufnr, + callback = function(ev) + if ev.event == 'BufEnter' then + update_source() + end + + vim.schedule(update_highlights) + end, + }) - vim.schedule(update_highlights) + if M.config.insert_at_end then + vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + buffer = bufnr, + callback = function() + vim.cmd('normal! 0') + vim.cmd('normal! G$') + vim.v.char = 'x' end, }) - - if M.config.insert_at_end then - vim.api.nvim_create_autocmd({ 'InsertEnter' }, { - buffer = bufnr, - callback = function() - vim.cmd('normal! 0') - vim.cmd('normal! G$') - vim.v.char = 'x' - end, - }) - end - - finish(true) end - ) + + finish(true) + end) for name, prompt in pairs(list_prompts()) do if prompt.prompt then diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 2e784d33..e7f0c75c 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -69,8 +69,8 @@ end ---@field private separator string ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay -local Chat = class(function(self, config, help, on_buf_create) - Overlay.init(self, 'copilot-chat', help, on_buf_create) +local Chat = class(function(self, config, on_buf_create) + Overlay.init(self, 'copilot-chat', utils.key_to_info('show_help', config.mappings.show_help), on_buf_create) self.winnr = nil self.config = config @@ -83,18 +83,24 @@ local Chat = class(function(self, config, help, on_buf_create) self.separator = config.separator self.spinner = Spinner() - self.chat_overlay = Overlay('copilot-overlay', 'q to close', function(bufnr) - vim.keymap.set('n', 'q', function() - self.chat_overlay:restore(self.winnr, self.bufnr) - end) - - vim.api.nvim_create_autocmd({ 'BufHidden', 'BufDelete' }, { - buffer = bufnr, - callback = function() + self.chat_overlay = Overlay( + 'copilot-overlay', + utils.key_to_info('close', { + normal = config.mappings.close.normal, + }), + function(bufnr) + vim.keymap.set('n', config.mappings.close.normal, function() self.chat_overlay:restore(self.winnr, self.bufnr) - end, - }) - end) + end) + + vim.api.nvim_create_autocmd({ 'BufHidden', 'BufDelete' }, { + buffer = bufnr, + callback = function() + self.chat_overlay:restore(self.winnr, self.bufnr) + end, + }) + end + ) notify.listen(notify.MESSAGE, function(msg) utils.schedule_main() From 0553496607717e539f08780a75c292a81619dcb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:40:52 +0200 Subject: [PATCH 507/728] chore(main): release 4.4.0 (#1297) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 14 ++++++++++++++ version.txt | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0585f66..15f1f334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [4.4.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.3.1...v4.4.0) (2025-08-09) + + +### Features + +* **completion:** add support for omnifunc and move completion logic to separate module ([1b04ddc](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1b04ddcfe2d04363a3898998a1005ab2f493dff4)) +* **ui:** show assistant reasoning as virtual text ([#1299](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1299)) ([92777fb](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/92777fb98ad4de7496188f1e9de336d16871ac43)) + + +### Bug Fixes + +* **chat:** correct block selection logic by cursor ([#1301](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1301)) ([7e027df](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7e027df6e95b622da25282285e84a9fc3806dcf1)) +* **info:** show resource uri instead of name in preview ([#1296](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1296)) ([90c3241](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/90c324177b33aec6d4c2bd5043c26bfc9fbc081f)) + ## [4.3.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.3.0...v4.3.1) (2025-08-08) diff --git a/version.txt b/version.txt index f77856a6..fdc66988 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.3.1 +4.4.0 From f5fd1a7ead5ccdd240fc3ef6e740fb49f74a1294 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 10 Aug 2025 16:41:17 +0000 Subject: [PATCH 508/728] 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 349cdeeb..6d02c33f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 10 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 5e091bf1bf11827bec5130edc8d4f87fdd243716 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 11 Aug 2025 20:21:15 +0200 Subject: [PATCH 509/728] fix(prompts): update tool instructions for system prompt (#1304) Change references from "system context" to "system prompt" in tool use instructions for clarity and consistency. This improves documentation accuracy and helps prevent confusion when defining tool usage. --- lua/CopilotChat/config/prompts.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 8764f914..44b2f8bd 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -24,7 +24,7 @@ Think creatively to provide complete solutions based on the information availabl Never fabricate or hallucinate file contents you haven't actually seen. -If tools are explicitly defined in your system context: +If tools are explicitly defined in your system prompt: - Follow JSON schema precisely when using tools, including all required properties and outputting valid JSON. - Use appropriate tools for tasks rather than asking for manual actions. - Execute actions directly when you indicate you'll do so, without asking for permission. @@ -33,7 +33,7 @@ If tools are explicitly defined in your system context: 1. Resources shared via "# " headers and referenced via "##" links 2. Code blocks with file path labels 3. Other contextual sharing like selected text or conversation history -- If you don't have explicit tool definitions in your system context, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. +- If you don't have explicit tool definitions in your system prompt, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. You will receive code snippets that include line number prefixes - use these to maintain correct position references but remove them when generating output. From fa8bb09decb927a092788e327b0d3ce4bbcd35e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 11 Aug 2025 18:21:33 +0000 Subject: [PATCH 510/728] 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 6d02c33f..6bafa23b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 10 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 11 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 15eebed57156c3ae6a6bb6f73692dbf0547ba9e4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 12 Aug 2025 02:30:04 +0200 Subject: [PATCH 511/728] fix(chat): schedule chat initialization after window opens (#1308) Ensure chat initialization and prompt resolution are scheduled after opening the chat window to avoid race conditions and ensure proper state. This fixes issues where chat state was not fully resolved before processing user prompts or tool calls. Closes #1307 --- lua/CopilotChat/init.lua | 209 ++++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 101 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 83a3d0e5..c97359dd 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -764,6 +764,9 @@ function M.ask(prompt, config) vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) config = vim.tbl_deep_extend('force', M.config, config or {}) + local schedule = function(cb) + return cb() + end -- Stop previous conversation and open window if not config.headless then @@ -774,6 +777,7 @@ function M.ask(prompt, config) end if not M.chat:focused() then M.open(config) + schedule = vim.schedule end else update_source() @@ -783,129 +787,132 @@ function M.ask(prompt, config) prompt = insert_sticky(prompt, config) prompt = vim.trim(prompt) - -- Prepare chat - if not config.headless then - store_sticky(prompt) - M.chat:start() - M.chat:append('\n') - end + -- After opening window we need to schedule to next cycle so everything properly resolves + schedule(function() + -- Prepare chat + if not config.headless then + store_sticky(prompt) + M.chat:start() + M.chat:append('\n') + end - -- Resolve prompt references - config, prompt = M.resolve_prompt(prompt, config) - local system_prompt = config.system_prompt or '' + -- Resolve prompt references + config, prompt = M.resolve_prompt(prompt, config) + local system_prompt = config.system_prompt or '' - -- Remove sticky prefix - prompt = table.concat( - vim.tbl_map(function(l) - return l:gsub('^>%s+', '') - end, vim.split(prompt, '\n')), - '\n' - ) + -- Remove sticky prefix + prompt = table.concat( + vim.tbl_map(function(l) + return l:gsub('^>%s+', '') + end, vim.split(prompt, '\n')), + '\n' + ) - -- Retrieve the selection - local selection = M.get_selection() + -- Retrieve the selection + local selection = M.get_selection() - async.run(handle_error(config, function() - local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) - local selected_model, prompt = M.resolve_model(prompt, config) + async.run(handle_error(config, function() + local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) + local selected_model, prompt = M.resolve_model(prompt, config) - prompt = vim.trim(prompt) + prompt = vim.trim(prompt) - if not config.headless then - utils.schedule_main() - local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) - if assistant_message and assistant_message.tool_calls then - local handled_ids = {} - for _, tool in ipairs(resolved_tools) do - handled_ids[tool.id] = true - end + if not config.headless then + utils.schedule_main() + local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) + if assistant_message and assistant_message.tool_calls then + local handled_ids = {} + for _, tool in ipairs(resolved_tools) do + handled_ids[tool.id] = true + end - -- If we skipped any tool calls, send that as result - for _, tool_call in ipairs(assistant_message.tool_calls) do - if not handled_ids[tool_call.id] then - table.insert(resolved_tools, { - id = tool_call.id, - result = string.format(BLOCK_OUTPUT_FORMAT, 'error', 'User skipped this function call.'), - }) - handled_ids[tool_call.id] = true + -- If we skipped any tool calls, send that as result + for _, tool_call in ipairs(assistant_message.tool_calls) do + if not handled_ids[tool_call.id] then + table.insert(resolved_tools, { + id = tool_call.id, + result = string.format(BLOCK_OUTPUT_FORMAT, 'error', 'User skipped this function call.'), + }) + handled_ids[tool_call.id] = true + end end end - end - if not utils.empty(resolved_tools) then - -- If we are handling tools, replace user message with tool results - M.chat:remove_message(constants.ROLE.USER) - for _, tool in ipairs(resolved_tools) do + if not utils.empty(resolved_tools) then + -- If we are handling tools, replace user message with tool results + M.chat:remove_message(constants.ROLE.USER) + for _, tool in ipairs(resolved_tools) do + M.chat:add_message({ + id = tool.id, + role = constants.ROLE.TOOL, + tool_call_id = tool.id, + content = '\n' .. tool.result .. '\n', + }) + end + else + -- Otherwise just replace the user message with resolved prompt M.chat:add_message({ - id = tool.id, - role = constants.ROLE.TOOL, - tool_call_id = tool.id, - content = '\n' .. tool.result .. '\n', - }) + role = constants.ROLE.USER, + content = '\n' .. prompt .. '\n', + }, true) end - else - -- Otherwise just replace the user message with resolved prompt - M.chat:add_message({ - role = constants.ROLE.USER, - content = '\n' .. prompt .. '\n', - }, true) end - end - if utils.empty(prompt) and utils.empty(resolved_tools) then - if not config.headless then - M.chat:remove_message(constants.ROLE.USER) - finish() - end - return - end - - local ask_response = client.ask(client, prompt, { - headless = config.headless, - history = M.chat.messages, - selection = selection, - resources = resolved_resources, - tools = selected_tools, - system_prompt = system_prompt, - model = selected_model, - temperature = config.temperature, - on_progress = vim.schedule_wrap(function(message) + if utils.empty(prompt) and utils.empty(resolved_tools) then if not config.headless then - M.chat:add_message(message) + M.chat:remove_message(constants.ROLE.USER) + finish() end - end), - }) + return + end - -- If there was no error and no response, it means job was cancelled - if ask_response == nil then - return - end + local ask_response = client.ask(client, prompt, { + headless = config.headless, + history = M.chat.messages, + selection = selection, + resources = resolved_resources, + tools = selected_tools, + system_prompt = system_prompt, + model = selected_model, + temperature = config.temperature, + on_progress = vim.schedule_wrap(function(message) + if not config.headless then + M.chat:add_message(message) + end + end), + }) - local response = ask_response.message - local token_count = ask_response.token_count - local token_max_count = ask_response.token_max_count + -- If there was no error and no response, it means job was cancelled + if ask_response == nil then + return + end - -- Call the callback function - if config.callback then - utils.schedule_main() - config.callback(response.content, state.source) - end + local response = ask_response.message + local token_count = ask_response.token_count + local token_max_count = ask_response.token_max_count - if not config.headless then - response.content = vim.trim(response.content) - if utils.empty(response.content) then - response.content = '' - else - response.content = '\n' .. response.content .. '\n' + -- Call the callback function + if config.callback then + utils.schedule_main() + config.callback(response.content, state.source) end - utils.schedule_main() - M.chat:add_message(response, true) - M.chat.token_count = token_count - M.chat.token_max_count = token_max_count - finish() - end - end)) + if not config.headless then + response.content = vim.trim(response.content) + if utils.empty(response.content) then + response.content = '' + else + response.content = '\n' .. response.content .. '\n' + end + + utils.schedule_main() + M.chat:add_message(response, true) + M.chat.token_count = token_count + M.chat.token_max_count = token_max_count + finish() + end + end)) + end) end --- Stop current copilot output and optionally reset the chat ten show the help message. From 925342b5964bfd6f5b0e7333f62333f3b0590bfb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 Aug 2025 00:30:50 +0000 Subject: [PATCH 512/728] 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 6bafa23b..2c3086ef 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 11 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 12 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From f22747ae5a4c88b4aa519e4638fe89b274ea28a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 02:31:38 +0200 Subject: [PATCH 513/728] chore(main): release 4.4.1 (#1305) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ version.txt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f1f334..1df5c92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [4.4.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.4.0...v4.4.1) (2025-08-12) + + +### Bug Fixes + +* **chat:** schedule chat initialization after window opens ([#1308](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1308)) ([15eebed](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/15eebed57156c3ae6a6bb6f73692dbf0547ba9e4)), closes [#1307](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1307) +* **prompts:** update tool instructions for system prompt ([#1304](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1304)) ([5e091bf](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/5e091bf1bf11827bec5130edc8d4f87fdd243716)) + ## [4.4.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.3.1...v4.4.0) (2025-08-09) diff --git a/version.txt b/version.txt index fdc66988..cca25a93 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.4.0 +4.4.1 From 081d4c20242140bb185ebee142a65454ad375f7d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 12 Aug 2025 16:04:22 +0200 Subject: [PATCH 514/728] refactor(prompts)!: support template substitution in system_prompt (#1312) Refactor prompt configuration to allow template substitution in system_prompt strings using curly braces (e.g. {COPILOT_BASE}). This enables more flexible and composable prompt definitions. Also update callback signature to use the full response object. Update README to reflect new prompt usage. BREAKING CHANGE: callback receives the full response object instead of just content. --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/config/prompts.lua | 64 ++++++++++++++---------------- lua/CopilotChat/init.lua | 15 ++++--- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index f4111a0f..c766fbd4 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Define your own prompts in the configuration: system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', }, NiceInstructions = { - system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.' .. require('CopilotChat.config.prompts').COPILOT_BASE.system_prompt, + system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner. {BASE_INSTRUCTIONS}', } } } diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 48f5e96a..f3a0d291 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -21,7 +21,7 @@ ---@field language string? ---@field temperature number? ---@field headless boolean? ----@field callback nil|fun(response: string, source: CopilotChat.source) +---@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.source) ---@field remember_as_sticky boolean? ---@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.Selection? ---@field window CopilotChat.config.Window? diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 44b2f8bd..559740e1 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -1,4 +1,12 @@ -local COPILOT_BASE = [[ +---@class CopilotChat.config.prompts.Prompt : CopilotChat.config.Shared +---@field prompt string? +---@field description string? +---@field mapping string? + +---@type table +return { + COPILOT_BASE = { + system_prompt = [[ When asked for your name, you must respond with "GitHub Copilot". Follow the user's requirements carefully & to the letter. Keep your answers short and impersonal. @@ -72,15 +80,20 @@ When presenting code changes: 4. Address any diagnostics issues when fixing code. 5. If multiple changes are needed, present them as separate code blocks. -]] +]], + }, -local COPILOT_INSTRUCTIONS = [[ + COPILOT_INSTRUCTIONS = { + system_prompt = [[ You are a code-focused AI programming assistant that specializes in practical software engineering solutions. -]] .. COPILOT_BASE -local COPILOT_EXPLAIN = [[ +{COPILOT_BASE} +]], + }, + + COPILOT_EXPLAIN = { + system_prompt = [[ You are a programming instructor focused on clear, practical explanations. -]] .. COPILOT_BASE .. [[ When explaining code: - Provide concise high-level overview first @@ -90,11 +103,16 @@ When explaining code: - Focus on complex parts rather than basic syntax - Use short paragraphs with clear structure - Mention performance considerations where relevant -]] -local COPILOT_REVIEW = [[ +{COPILOT_BASE} +]], + }, + + COPILOT_REVIEW = { + system_prompt = [[ You are a code reviewer focused on improving code quality and maintainability. -]] .. COPILOT_BASE .. [[ + +{COPILOT_BASE} Format each issue you find precisely as: line=: @@ -117,39 +135,17 @@ Multiple issues on one line should be separated by semicolons. End with: "**`To clear buffer highlights, please ask a different question.`**" If no issues found, confirm the code is well-written and explain why. -]] - ----@class CopilotChat.config.prompts.Prompt : CopilotChat.config.Shared ----@field prompt string? ----@field description string? ----@field mapping string? - ----@type table -return { - COPILOT_BASE = { - system_prompt = COPILOT_BASE, - }, - - COPILOT_INSTRUCTIONS = { - system_prompt = COPILOT_INSTRUCTIONS, - }, - - COPILOT_EXPLAIN = { - system_prompt = COPILOT_EXPLAIN, - }, - - COPILOT_REVIEW = { - system_prompt = COPILOT_REVIEW, +]], }, Explain = { prompt = 'Write an explanation for the selected code as paragraphs of text.', - system_prompt = 'COPILOT_EXPLAIN', + system_prompt = '{COPILOT_EXPLAIN}', }, Review = { prompt = 'Review the selected code.', - system_prompt = 'COPILOT_REVIEW', + system_prompt = '{COPILOT_REVIEW}', callback = function(response, source) local diagnostics = {} for line in response:gmatch('[^\r\n]+') do diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c97359dd..48703c74 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -183,10 +183,6 @@ local function list_prompts() } end - if val.system_prompt and M.config.prompts[val.system_prompt] then - val.system_prompt = M.config.prompts[val.system_prompt].system_prompt - end - prompts_to_use[name] = val end @@ -506,11 +502,14 @@ function M.resolve_prompt(prompt, config) config = vim.tbl_deep_extend('force', M.config, config or {}) config, prompt = resolve(config, prompt or '') - if prompts_to_use[config.system_prompt] then - config.system_prompt = prompts_to_use[config.system_prompt].system_prompt - end if config.system_prompt then + for name, prompt in pairs(prompts_to_use) do + if prompt.system_prompt then + config.system_prompt = config.system_prompt:gsub('{' .. name .. '}', prompt.system_prompt) + end + end + config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) if state.source then @@ -894,7 +893,7 @@ function M.ask(prompt, config) -- Call the callback function if config.callback then utils.schedule_main() - config.callback(response.content, state.source) + config.callback(response, state.source) end if not config.headless then From 918e4d1078655ab966a6b69010d77192e69a1eb7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 Aug 2025 14:04:44 +0000 Subject: [PATCH 515/728] 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 2c3086ef..90c7a034 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -361,7 +361,7 @@ Define your own prompts in the configuration: system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', }, NiceInstructions = { - system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.' .. require('CopilotChat.config.prompts').COPILOT_BASE.system_prompt, + system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner. {BASE_INSTRUCTIONS}', } } } From 957e0a88c7d7df706380e09412c0b3f24af534ad Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 12 Aug 2025 16:18:15 +0200 Subject: [PATCH 516/728] fix(utils): always exit insert mode in return_to_normal_mode (#1313) Previously, return_to_normal_mode only exited insert mode if not already in normal mode. This change ensures that 'stopinsert' is always called, making the function more reliable when switching modes, especially after visual selections. This improves consistency in mode transitions. Fixes #1307 --- 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 fe2f4773..a221b749 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -174,9 +174,8 @@ function M.return_to_normal_mode() local mode = vim.fn.mode():lower() if mode:find('v') then vim.cmd([[execute "normal! \"]]) - elseif mode ~= 'n' then - vim.cmd('stopinsert') end + vim.cmd('stopinsert') end --- Debounce a function From d12f6dff0e1641f933f9941b843d094bf505a82e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 12 Aug 2025 18:16:07 +0200 Subject: [PATCH 517/728] chore: mark next release as 4.5.0 (#1315) Release-As: 4.5.0 Signed-off-by: Tomas Slusny From 33e6ffc63b77b0340731f2b50bd962045adf9366 Mon Sep 17 00:00:00 2001 From: Mihamina Rakotomandimby Date: Wed, 13 Aug 2025 07:58:21 +0300 Subject: [PATCH 518/728] feat(prompts): add support for providing system prompt as function (#1318) * use a function to dynamically look for sytem instructions * remove unused parameter * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Mihamina RKTMB Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/init.lua | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f3a0d291..1f35f33f 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -14,7 +14,7 @@ ---@field blend number? ---@class CopilotChat.config.Shared ----@field system_prompt string? +---@field system_prompt string|fun(source: CopilotChat.source):string|nil ---@field model string? ---@field tools string|table|nil ---@field sticky string|table|nil diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 48703c74..8a660183 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -189,6 +189,26 @@ local function list_prompts() return prompts_to_use end +--- Resolve system prompt - handle both string and function types +---@param system_prompt string|function|nil +---@return string? +local function resolve_system_prompt(system_prompt) + if not system_prompt then + return nil + end + + if type(system_prompt) == 'function' then + local ok, result = pcall(system_prompt) + if not ok then + log.warn('Failed to resolve system prompt function: ' .. result) + return nil + end + return result + end + + return system_prompt +end + --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) @@ -503,6 +523,9 @@ function M.resolve_prompt(prompt, config) config = vim.tbl_deep_extend('force', M.config, config or {}) config, prompt = resolve(config, prompt or '') + -- Resolve system prompt (handle functions) + config.system_prompt = resolve_system_prompt(config.system_prompt, state.source) + if config.system_prompt then for name, prompt in pairs(prompts_to_use) do if prompt.system_prompt then From 8a5e5e77c64bc8b266c73db33d0954df5facab20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Aug 2025 04:58:47 +0000 Subject: [PATCH 519/728] 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 90c7a034..f308336d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 12 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 13 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 44f37584bdd2d1b984e4ce64f5378a773ba0fa1e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 06:59:06 +0200 Subject: [PATCH 520/728] docs: add rakotomandimby as a contributor for code (#1319) * 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 | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 72ae3b71..e8546bbf 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -437,7 +437,7 @@ "name": "Mihamina Rakotomandimby", "avatar_url": "https://avatars.githubusercontent.com/u/488088?v=4", "profile": "https://mihamina.rktmb.org", - "contributions": ["doc"] + "contributions": ["doc", "code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c766fbd4..5f2c8374 100644 --- a/README.md +++ b/README.md @@ -626,7 +626,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Aaron D Borden
Aaron D Borden

💻 Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

💻 📖 Danilo Horta
Danilo Horta

💻 - Mihamina Rakotomandimby
Mihamina Rakotomandimby

📖 + Mihamina Rakotomandimby
Mihamina Rakotomandimby

📖 💻 From 26f7b4f157ec75b168c05dc826b5fa3106cfc351 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Aug 2025 10:40:36 +0200 Subject: [PATCH 521/728] fix(prompt): recursive system prompt expansion (#1324) Refactor system prompt expansion to support recursive placeholders. The system prompt now uses curly braces for placeholders and expands nested prompts up to a maximum depth. This improves flexibility for prompt composition and avoids incomplete substitutions. Signed-off-by: Tomas Slusny Closes #1323 --- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/init.lua | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 1f35f33f..6a37eaa0 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -53,7 +53,7 @@ return { -- Shared config starts here (can be passed to functions at runtime and configured via setup function) - system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). + system_prompt = '{COPILOT_INSTRUCTIONS}', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 8a660183..dc868b0d 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -520,6 +520,23 @@ function M.resolve_prompt(prompt, config) return inner_config, inner_prompt end + local function expand_system_prompt(system_prompt, prompts_to_use) + local prev, curr = nil, system_prompt + local depth = 0 + repeat + prev = curr + curr = curr:gsub('{([%w_]+)}', function(name) + local prompt = prompts_to_use[name] + if prompt and prompt.system_prompt then + return prompt.system_prompt + end + return '{' .. name .. '}' + end) + depth = depth + 1 + until prev == curr or depth >= MAX_DEPTH + return curr + end + config = vim.tbl_deep_extend('force', M.config, config or {}) config, prompt = resolve(config, prompt or '') @@ -527,12 +544,7 @@ function M.resolve_prompt(prompt, config) config.system_prompt = resolve_system_prompt(config.system_prompt, state.source) if config.system_prompt then - for name, prompt in pairs(prompts_to_use) do - if prompt.system_prompt then - config.system_prompt = config.system_prompt:gsub('{' .. name .. '}', prompt.system_prompt) - end - end - + config.system_prompt = expand_system_prompt(config.system_prompt, prompts_to_use) config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) if state.source then From 02b97d8b3f54ed93f3c41091e578e0ac4a8abeaa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Aug 2025 08:40:55 +0000 Subject: [PATCH 522/728] 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 f308336d..844b5890 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -628,7 +628,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻This project follows the all-contributors specification. Contributions of any kind are welcome! From f99f1cdef151ac1c950850cdcc0dbeefad00603c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Aug 2025 10:43:47 +0200 Subject: [PATCH 523/728] fix(config): correct system_prompt type and callback usage (#1325) Update the type annotation for system_prompt to allow nil and fix its initialization to use the shared prompt. Also, update the callback in prompts.lua to use response.content for line parsing, ensuring correct diagnostics extraction. --- lua/CopilotChat/config.lua | 4 ++-- lua/CopilotChat/config/prompts.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 6a37eaa0..a3fce3a9 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -14,7 +14,7 @@ ---@field blend number? ---@class CopilotChat.config.Shared ----@field system_prompt string|fun(source: CopilotChat.source):string|nil +---@field system_prompt nil|string|fun(source: CopilotChat.source):string ---@field model string? ---@field tools string|table|nil ---@field sticky string|table|nil @@ -53,7 +53,7 @@ return { -- Shared config starts here (can be passed to functions at runtime and configured via setup function) - system_prompt = '{COPILOT_INSTRUCTIONS}', -- System prompt to use (can be specified manually in prompt via /). + system_prompt = require('CopilotChat.config.prompts').COPILOT_INSTRUCTIONS.system_prompt, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 559740e1..d01db26d 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -148,7 +148,7 @@ If no issues found, confirm the code is well-written and explain why. system_prompt = '{COPILOT_REVIEW}', callback = function(response, source) local diagnostics = {} - for line in response:gmatch('[^\r\n]+') do + for line in response.content:gmatch('[^\r\n]+') do if line:find('^line=') then local start_line = nil local end_line = nil From f62eaf3447530b34e92ba7c11d9cd7f980e088ed Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Aug 2025 18:18:47 +0200 Subject: [PATCH 524/728] refactor(prompt): simplify system prompt resolution logic (#1327) Remove recursive prompt expansion and placeholder substitution for system prompts. Now, system prompts reference other prompts by name directly, and the base instructions are appended automatically. This improves clarity and maintainability of prompt configuration. Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config/prompts.lua | 12 ++----- lua/CopilotChat/init.lua | 58 ++++++++++-------------------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 5f2c8374..ef385174 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Define your own prompts in the configuration: system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', }, NiceInstructions = { - system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner. {BASE_INSTRUCTIONS}', + system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.', } } } diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index d01db26d..203997a7 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -7,7 +7,7 @@ return { COPILOT_BASE = { system_prompt = [[ -When asked for your name, you must respond with "GitHub Copilot". +When asked for your name, you must respond with "Copilot". Follow the user's requirements carefully & to the letter. Keep your answers short and impersonal. Always answer in {LANGUAGE} unless explicitly asked otherwise. @@ -86,8 +86,6 @@ When presenting code changes: COPILOT_INSTRUCTIONS = { system_prompt = [[ You are a code-focused AI programming assistant that specializes in practical software engineering solutions. - -{COPILOT_BASE} ]], }, @@ -103,8 +101,6 @@ When explaining code: - Focus on complex parts rather than basic syntax - Use short paragraphs with clear structure - Mention performance considerations where relevant - -{COPILOT_BASE} ]], }, @@ -112,8 +108,6 @@ When explaining code: system_prompt = [[ You are a code reviewer focused on improving code quality and maintainability. -{COPILOT_BASE} - Format each issue you find precisely as: line=: OR @@ -140,12 +134,12 @@ If no issues found, confirm the code is well-written and explain why. Explain = { prompt = 'Write an explanation for the selected code as paragraphs of text.', - system_prompt = '{COPILOT_EXPLAIN}', + system_prompt = 'COPILOT_EXPLAIN', }, Review = { prompt = 'Review the selected code.', - system_prompt = '{COPILOT_REVIEW}', + system_prompt = 'COPILOT_REVIEW', callback = function(response, source) local diagnostics = {} for line in response.content:gmatch('[^\r\n]+') do diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index dc868b0d..38ee5568 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -189,26 +189,6 @@ local function list_prompts() return prompts_to_use end ---- Resolve system prompt - handle both string and function types ----@param system_prompt string|function|nil ----@return string? -local function resolve_system_prompt(system_prompt) - if not system_prompt then - return nil - end - - if type(system_prompt) == 'function' then - local ok, result = pcall(system_prompt) - if not ok then - log.warn('Failed to resolve system prompt function: ' .. result) - return nil - end - return result - end - - return system_prompt -end - --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) @@ -520,31 +500,31 @@ function M.resolve_prompt(prompt, config) return inner_config, inner_prompt end - local function expand_system_prompt(system_prompt, prompts_to_use) - local prev, curr = nil, system_prompt - local depth = 0 - repeat - prev = curr - curr = curr:gsub('{([%w_]+)}', function(name) - local prompt = prompts_to_use[name] - if prompt and prompt.system_prompt then - return prompt.system_prompt - end - return '{' .. name .. '}' - end) - depth = depth + 1 - until prev == curr or depth >= MAX_DEPTH - return curr + local function resolve_system_prompt(system_prompt) + if type(system_prompt) == 'function' then + local ok, result = pcall(system_prompt) + if not ok then + log.warn('Failed to resolve system prompt function: ' .. result) + return nil + end + return result + end + + return system_prompt end config = vim.tbl_deep_extend('force', M.config, config or {}) config, prompt = resolve(config, prompt or '') - -- Resolve system prompt (handle functions) - config.system_prompt = resolve_system_prompt(config.system_prompt, state.source) - if config.system_prompt then - config.system_prompt = expand_system_prompt(config.system_prompt, prompts_to_use) + config.system_prompt = resolve_system_prompt(config.system_prompt) + + if M.config.prompts[config.system_prompt] then + -- Name references are good for making system prompt auto sticky + config.system_prompt = M.config.prompts[config.system_prompt].system_prompt + end + + config.system_prompt = config.system_prompt .. '\n' .. M.config.prompts.COPILOT_BASE.system_prompt config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) if state.source then From f60803fca0b5b305a2d56361a732a07fb3c38c39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Aug 2025 16:19:04 +0000 Subject: [PATCH 525/728] 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 844b5890..b063c54e 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -361,7 +361,7 @@ Define your own prompts in the configuration: system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', }, NiceInstructions = { - system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner. {BASE_INSTRUCTIONS}', + system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.', } } } From 76cc41653d63cfdb653f584624b4bf5e721f9514 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Aug 2025 19:17:33 +0200 Subject: [PATCH 526/728] fix(completion): require tool uri for input completion (#1328) Previously, input completion was triggered for tools without a defined `uri`, which could lead to errors or unexpected behavior. This change ensures that input completion only occurs when both the tool and its schema are present and the tool has a valid `uri` property. --- lua/CopilotChat/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/completion.lua b/lua/CopilotChat/completion.lua index a32141eb..6c65a18d 100644 --- a/lua/CopilotChat/completion.lua +++ b/lua/CopilotChat/completion.lua @@ -146,7 +146,7 @@ function M.complete(without_input) if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then local found_tool = config.functions[prefix:sub(2, -2)] local found_schema = found_tool and functions.parse_schema(found_tool) - if found_tool and found_schema then + if found_tool and found_schema and found_tool.uri then async.run(function() local value = functions.enter_input(found_schema, source) if not value then From 6d6e39088bd21087ba7ca6e9943c9ee10bda1f9b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Aug 2025 18:34:58 +0200 Subject: [PATCH 527/728] refactor(diff): improve line matching and replacement logic (#1329) Refactors diff application logic to use a new utils.split_lines function for consistent line splitting. Applies diffs from bottom to top to preserve line numbering and ensures correct replacement of lines in buffers. This improves accuracy when applying multiple diffs to the same file and enhances code readability. --- lua/CopilotChat/config/mappings.lua | 56 ++++++++++++++++------------- lua/CopilotChat/utils.lua | 11 ++++++ 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 4928a876..e3e2e248 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -51,7 +51,8 @@ local function get_diff(block) -- 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') + local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false) + reference = table.concat(lines, '\n') filetype = vim.bo[bufnr].filetype end end @@ -212,7 +213,7 @@ return { return end - local lines = vim.split(message.content, '\n') + local lines = utils.split_lines(message.content) local new_lines = {} local changed = false @@ -241,9 +242,9 @@ return { return end - local lines = vim.split(diff.change, '\n', { trimempty = false }) + local lines = utils.split_lines(diff.change) vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) - copilot.set_selection(diff.bufnr, diff.start_line, diff.start_line + #lines - 1) + copilot.set_selection(diff.bufnr, diff.start_line, diff.end_line) end, }, @@ -352,10 +353,9 @@ return { } if copilot.config.mappings.show_diff.full_diff then - local modified = utils.buf_valid(diff.bufnr) and vim.api.nvim_buf_get_lines(diff.bufnr, 0, -1, false) or {} + local original = utils.buf_valid(diff.bufnr) and vim.api.nvim_buf_get_lines(diff.bufnr, 0, -1, false) or {} - -- Apply all diffs from same file - if #modified > 0 then + if #original > 0 then -- Find all diffs from the same file in this section local message = copilot.chat:get_message(constants.ROLE.ASSISTANT, true) local section = message and message.section @@ -367,30 +367,38 @@ return { table.insert(same_file_diffs, block_diff) end end + end - -- Sort diffs bottom to top to preserve line numbering - table.sort(same_file_diffs, function(a, b) - return a.start_line > b.start_line - end) + -- Ensure we at least apply the current diff + if #same_file_diffs == 0 then + table.insert(same_file_diffs, diff) end - for _, file_diff in ipairs(same_file_diffs) do - local start_idx = file_diff.start_line - local end_idx = file_diff.end_line - for _ = start_idx, end_idx do - table.remove(modified, start_idx) + -- Sort diffs by start_line in descending order (apply from bottom to top) + table.sort(same_file_diffs, function(a, b) + return a.start_line > b.start_line + end) + + local result = vim.deepcopy(original) + + -- Apply diffs from bottom to top so line numbers remain valid + for _, d in ipairs(same_file_diffs) do + local change_lines = utils.split_lines(d.change) + + -- Remove original lines (from end to start to avoid index shifting) + for i = d.end_line, d.start_line, -1 do + if result[i] then + table.remove(result, i) + end end - local change_lines = vim.split(file_diff.change, '\n') - for i, line in ipairs(change_lines) do - table.insert(modified, start_idx + i, line) + + -- Insert replacement lines at start_line + for i = #change_lines, 1, -1 do + table.insert(result, d.start_line, change_lines[i]) end end - modified = vim.tbl_filter(function(line) - return line ~= nil - end, modified) - - opts.text = table.concat(modified, '\n') + opts.text = table.concat(result, '\n') else opts.text = diff.change end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index a221b749..a6517f35 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -772,6 +772,17 @@ function M.empty(v) return false end +--- Split text into lines +---@param text string The text to split +---@return string[] A table of lines +function M.split_lines(text) + if not text or text == '' then + return {} + end + + return vim.split(text, '\r?\n', { trimempty = false }) +end + --- Convert glob pattern to regex pattern --- https://github.com/davidm/lua-glob-pattern/blob/master/lua/globtopattern.lua ---@param g string The glob pattern From 5f3c57083515ea511deda291ae72434db568ee6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 14 Aug 2025 16:35:15 +0000 Subject: [PATCH 528/728] 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 b063c54e..b3b7c88c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 13 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 14 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From b3b7a3c8d3f9c083f6e3d0f079072b61bd057183 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Aug 2025 18:34:05 +0200 Subject: [PATCH 529/728] test: migrate to MiniTest and add utils tests (#1333) Replaces vusted with MiniTest for running tests and updates CI workflow to use Neovim for testing. Adds initial tests for utils.glob_to_pattern. Removes old plugin_spec.lua and updates .luarc.json for new globals. Signed-off-by: Tomas Slusny --- .github/workflows/ci.yml | 6 +---- .gitignore | 2 ++ .luarc.json | 9 ++++++- Makefile | 18 +------------- README.md | 1 - scripts/minimal.lua | 16 +++++++++++++ scripts/test.lua | 14 +++++++++++ test/plugin_spec.lua | 18 -------------- tests/test_init.lua | 9 +++++++ tests/test_utils.lua | 52 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 103 insertions(+), 42 deletions(-) create mode 100644 scripts/minimal.lua create mode 100644 scripts/test.lua delete mode 100644 test/plugin_spec.lua create mode 100644 tests/test_init.lua create mode 100644 tests/test_utils.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e722714f..9ce3f370 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,8 +64,4 @@ jobs: luarocksVersion: "3.12.2" - name: run test - shell: bash - run: | - luarocks install luacheck - luarocks install vusted - vusted ./test + run: make test diff --git a/.gitignore b/.gitignore index 94e6f763..fc3fe2ac 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,5 @@ cython_debug/ # (neo)vim helptags /doc/tags + +.dependencies/ diff --git a/.luarc.json b/.luarc.json index b97a9f11..ad90d858 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,4 +1,11 @@ { - "diagnostics.globals": ["describe", "it"], + "runtime.version": "LuaJIT", + "diagnostics.globals": [ + "describe", + "it", + "MiniTest", + "before_each", + "after_each" + ], "diagnostics.disable": ["redefined-local"] } diff --git a/Makefile b/Makefile index c5d53c52..240be629 100644 --- a/Makefile +++ b/Makefile @@ -19,28 +19,12 @@ 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 " tiktoken - Download tiktoken_core library" - @echo " clean - Remove build directory" - -install-cli: - brew install luarocks - brew install lua - install-pre-commit: pip install pre-commit pre-commit install -install: - luarocks install vusted - test: - vusted test + nvim --headless --noplugin -u ./scripts/test.lua -c "lua MiniTest.run()" all: luajit diff --git a/README.md b/README.md index ef385174..62d03ee5 100644 --- a/README.md +++ b/README.md @@ -519,7 +519,6 @@ cd CopilotChat.nvim 2. Install development dependencies: ```bash -# Install pre-commit hooks make install-pre-commit ``` diff --git a/scripts/minimal.lua b/scripts/minimal.lua new file mode 100644 index 00000000..69c5cefb --- /dev/null +++ b/scripts/minimal.lua @@ -0,0 +1,16 @@ +-- https://github.com/neovim/neovim/blob/master/contrib/minimal.lua +vim.opt.runtimepath:append(vim.fn.getcwd()) + +for name, url in pairs({ + 'https://github.com/nvim-lua/plenary.nvim', +}) do + local install_path = vim.fn.fnamemodify('.dependencies/' .. name, ':p') + if vim.fn.isdirectory(install_path) == 0 then + vim.fn.system({ 'git', 'clone', '--depth=1', url, install_path }) + end + vim.opt.runtimepath:append(install_path) +end + +require('CopilotChat').setup({ + -- Add your configuration here +}) diff --git a/scripts/test.lua b/scripts/test.lua new file mode 100644 index 00000000..fd391b37 --- /dev/null +++ b/scripts/test.lua @@ -0,0 +1,14 @@ +vim.opt.runtimepath:append(vim.fn.getcwd()) + +for name, url in pairs({ + 'https://github.com/nvim-lua/plenary.nvim', + 'https://github.com/echasnovski/mini.test', +}) do + local install_path = vim.fn.fnamemodify('.dependencies/' .. name, ':p') + if vim.fn.isdirectory(install_path) == 0 then + vim.fn.system({ 'git', 'clone', '--depth=1', url, install_path }) + end + vim.opt.runtimepath:append(install_path) +end + +require('mini.test').setup() diff --git a/test/plugin_spec.lua b/test/plugin_spec.lua deleted file mode 100644 index 9497f016..00000000 --- a/test/plugin_spec.lua +++ /dev/null @@ -1,18 +0,0 @@ --- Mock packages -package.loaded['plenary.async'] = { - wrap = function(fn) - return function(...) - return fn(...) - end - end, -} -package.loaded['plenary.curl'] = {} -package.loaded['plenary.log'] = {} -package.loaded['plenary.scandir'] = {} -package.loaded['plenary.filetype'] = {} - -describe('CopilotChat plugin', function() - it('should be able to load', function() - assert.truthy(require('CopilotChat')) - end) -end) diff --git a/tests/test_init.lua b/tests/test_init.lua new file mode 100644 index 00000000..e4329607 --- /dev/null +++ b/tests/test_init.lua @@ -0,0 +1,9 @@ +local T = MiniTest.new_set() + +T['should be able to load'] = function() + MiniTest.expect.no_error(function() + require('CopilotChat') + end) +end + +return T diff --git a/tests/test_utils.lua b/tests/test_utils.lua new file mode 100644 index 00000000..ca23a4bb --- /dev/null +++ b/tests/test_utils.lua @@ -0,0 +1,52 @@ +local T = MiniTest.new_set() + +local cases = { + { glob = '', expected = '^$' }, + { glob = 'abc', expected = '^abc$' }, + { glob = 'ab#/.', expected = '^ab%#%/%.$' }, + { glob = '\\\\\\ab\\c\\', expected = '^%\\abc\\$' }, + + { glob = 'abc.*', expected = '^abc%..*$', matches = { 'abc.txt', 'abc.' }, not_matches = { 'abc' } }, + { glob = '??.txt', expected = '^..%.txt$' }, + + { glob = 'a[]', expected = '[^]' }, + { glob = 'a[^]b', expected = '^ab$' }, + { glob = 'a[!]b', expected = '^ab$' }, + { glob = 'a[a][b]z', expected = '^a[a][b]z$' }, + { glob = 'a[a-f]z', expected = '^a[a-f]z$' }, + { glob = 'a[a-f0-9]z', expected = '^a[a-f0-9]z$' }, + { glob = 'a[a-f0-]z', expected = '^a[a-f0%-]z$' }, + { glob = 'a[!a-f]z', expected = '^a[^a-f]z$' }, + { glob = 'a[^a-f]z', expected = '^a[^a-f]z$' }, + { glob = 'a[\\!\\^\\-z\\]]z', expected = '^a[%!%^%-z%]]z$' }, + { glob = 'a[\\a-\\f]z', expected = '^a[a-f]z$' }, + + { glob = 'a[', expected = '[^]' }, + { glob = 'a[a-', expected = '[^]' }, + { glob = 'a[a-b', expected = '[^]' }, + { glob = 'a[!', expected = '[^]' }, + { glob = 'a[!a', expected = '[^]' }, + { glob = 'a[!a-', expected = '[^]' }, + { glob = 'a[!a-b', expected = '[^]' }, + { glob = 'a[!a-b\\]', expected = '[^]' }, +} + +for _, case in ipairs(cases) do + T['glob_to_pattern: ' .. case.glob] = function() + local utils = require('CopilotChat.utils') + local pattern = utils.glob_to_pattern(case.glob) + MiniTest.expect.equality(pattern, case.expected) + if case.matches then + for _, str in ipairs(case.matches) do + MiniTest.expect.equality(str:match(pattern) ~= nil, true) + end + end + if case.not_matches then + for _, str in ipairs(case.not_matches) do + MiniTest.expect.equality(str:match(pattern) ~= nil, false) + end + end + end +end + +return T From d7d808b14a73e17c71fe6f153017a411f36cd3cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 Aug 2025 16:34:25 +0000 Subject: [PATCH 530/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index b3b7c88c..2d64906e 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 14 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 15 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -600,7 +600,6 @@ To set up the environment: 1. Install development dependencies: >bash - # Install pre-commit hooks make install-pre-commit < From c5057d3bb6d87e9b117b4f37162409d4c2c74e31 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Aug 2025 18:41:01 +0200 Subject: [PATCH 531/728] fix(test): run tests automatically in test script (#1334) Previously, the Makefile required a command to run tests after loading the test script. Now, the test script itself runs the tests automatically, simplifying the Makefile and ensuring consistent test execution. Signed-off-by: Tomas Slusny --- Makefile | 2 +- scripts/test.lua | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 240be629..04756dda 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ install-pre-commit: pre-commit install test: - nvim --headless --noplugin -u ./scripts/test.lua -c "lua MiniTest.run()" + nvim --headless --noplugin -u ./scripts/test.lua all: luajit diff --git a/scripts/test.lua b/scripts/test.lua index fd391b37..fdb0cdec 100644 --- a/scripts/test.lua +++ b/scripts/test.lua @@ -11,4 +11,6 @@ for name, url in pairs({ vim.opt.runtimepath:append(install_path) end -require('mini.test').setup() +local minitest = require('mini.test') +minitest.setup() +minitest.run() From b479d60f6e9df1186f33f2bbe8bfb8069ed5fc85 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Aug 2025 19:11:46 +0200 Subject: [PATCH 532/728] test: migrate to plenary.nvim for unit testing (#1335) Replace mini.test with plenary.nvim for running unit tests. Update test scripts and test files to use plenary's test harness and assertion methods. Remove mini.test references from configuration and codebase. This simplifies dependencies and aligns with common Neovim testing practices. --- .luarc.json | 1 - lua/CopilotChat/utils.lua | 2 +- scripts/test.lua | 5 +-- tests/init_spec.lua | 7 ++++ tests/test_init.lua | 9 ---- tests/test_utils.lua | 52 ----------------------- tests/utils_spec.lua | 88 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 67 deletions(-) create mode 100644 tests/init_spec.lua delete mode 100644 tests/test_init.lua delete mode 100644 tests/test_utils.lua create mode 100644 tests/utils_spec.lua diff --git a/.luarc.json b/.luarc.json index ad90d858..4a3cf0b6 100644 --- a/.luarc.json +++ b/.luarc.json @@ -3,7 +3,6 @@ "diagnostics.globals": [ "describe", "it", - "MiniTest", "before_each", "after_each" ], diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index a6517f35..c829d40e 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -157,7 +157,7 @@ end --- Writes text to a temporary file and returns path ---@param text string The text to write ----@return string? +---@return string function M.temp_file(text) local temp_file = os.tmpname() local f = io.open(temp_file, 'w+') diff --git a/scripts/test.lua b/scripts/test.lua index fdb0cdec..5da43da3 100644 --- a/scripts/test.lua +++ b/scripts/test.lua @@ -2,7 +2,6 @@ vim.opt.runtimepath:append(vim.fn.getcwd()) for name, url in pairs({ 'https://github.com/nvim-lua/plenary.nvim', - 'https://github.com/echasnovski/mini.test', }) do local install_path = vim.fn.fnamemodify('.dependencies/' .. name, ':p') if vim.fn.isdirectory(install_path) == 0 then @@ -11,6 +10,4 @@ for name, url in pairs({ vim.opt.runtimepath:append(install_path) end -local minitest = require('mini.test') -minitest.setup() -minitest.run() +require('plenary.test_harness').test_directory('tests') diff --git a/tests/init_spec.lua b/tests/init_spec.lua new file mode 100644 index 00000000..23102727 --- /dev/null +++ b/tests/init_spec.lua @@ -0,0 +1,7 @@ +describe('CopilotChat module', function() + it('should be able to load', function() + assert.has_no.errors(function() + require('CopilotChat') + end) + end) +end) diff --git a/tests/test_init.lua b/tests/test_init.lua deleted file mode 100644 index e4329607..00000000 --- a/tests/test_init.lua +++ /dev/null @@ -1,9 +0,0 @@ -local T = MiniTest.new_set() - -T['should be able to load'] = function() - MiniTest.expect.no_error(function() - require('CopilotChat') - end) -end - -return T diff --git a/tests/test_utils.lua b/tests/test_utils.lua deleted file mode 100644 index ca23a4bb..00000000 --- a/tests/test_utils.lua +++ /dev/null @@ -1,52 +0,0 @@ -local T = MiniTest.new_set() - -local cases = { - { glob = '', expected = '^$' }, - { glob = 'abc', expected = '^abc$' }, - { glob = 'ab#/.', expected = '^ab%#%/%.$' }, - { glob = '\\\\\\ab\\c\\', expected = '^%\\abc\\$' }, - - { glob = 'abc.*', expected = '^abc%..*$', matches = { 'abc.txt', 'abc.' }, not_matches = { 'abc' } }, - { glob = '??.txt', expected = '^..%.txt$' }, - - { glob = 'a[]', expected = '[^]' }, - { glob = 'a[^]b', expected = '^ab$' }, - { glob = 'a[!]b', expected = '^ab$' }, - { glob = 'a[a][b]z', expected = '^a[a][b]z$' }, - { glob = 'a[a-f]z', expected = '^a[a-f]z$' }, - { glob = 'a[a-f0-9]z', expected = '^a[a-f0-9]z$' }, - { glob = 'a[a-f0-]z', expected = '^a[a-f0%-]z$' }, - { glob = 'a[!a-f]z', expected = '^a[^a-f]z$' }, - { glob = 'a[^a-f]z', expected = '^a[^a-f]z$' }, - { glob = 'a[\\!\\^\\-z\\]]z', expected = '^a[%!%^%-z%]]z$' }, - { glob = 'a[\\a-\\f]z', expected = '^a[a-f]z$' }, - - { glob = 'a[', expected = '[^]' }, - { glob = 'a[a-', expected = '[^]' }, - { glob = 'a[a-b', expected = '[^]' }, - { glob = 'a[!', expected = '[^]' }, - { glob = 'a[!a', expected = '[^]' }, - { glob = 'a[!a-', expected = '[^]' }, - { glob = 'a[!a-b', expected = '[^]' }, - { glob = 'a[!a-b\\]', expected = '[^]' }, -} - -for _, case in ipairs(cases) do - T['glob_to_pattern: ' .. case.glob] = function() - local utils = require('CopilotChat.utils') - local pattern = utils.glob_to_pattern(case.glob) - MiniTest.expect.equality(pattern, case.expected) - if case.matches then - for _, str in ipairs(case.matches) do - MiniTest.expect.equality(str:match(pattern) ~= nil, true) - end - end - if case.not_matches then - for _, str in ipairs(case.not_matches) do - MiniTest.expect.equality(str:match(pattern) ~= nil, false) - end - end - end -end - -return T diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua new file mode 100644 index 00000000..2e45c83d --- /dev/null +++ b/tests/utils_spec.lua @@ -0,0 +1,88 @@ +local utils = require('CopilotChat.utils') + +describe('CopilotChat.utils', function() + local cases = { + { glob = '', expected = '^$' }, + { glob = 'abc', expected = '^abc$' }, + { glob = 'ab#/.', expected = '^ab%#%/%.$' }, + { glob = '\\\\\\ab\\c\\', expected = '^%\\abc\\$' }, + + { glob = 'abc.*', expected = '^abc%..*$', matches = { 'abc.txt', 'abc.' }, not_matches = { 'abc' } }, + { glob = '??.txt', expected = '^..%.txt$' }, + + { glob = 'a[]', expected = '[^]' }, + { glob = 'a[^]b', expected = '^ab$' }, + { glob = 'a[!]b', expected = '^ab$' }, + { glob = 'a[a][b]z', expected = '^a[a][b]z$' }, + { glob = 'a[a-f]z', expected = '^a[a-f]z$' }, + { glob = 'a[a-f0-9]z', expected = '^a[a-f0-9]z$' }, + { glob = 'a[a-f0-]z', expected = '^a[a-f0%-]z$' }, + { glob = 'a[!a-f]z', expected = '^a[^a-f]z$' }, + { glob = 'a[^a-f]z', expected = '^a[^a-f]z$' }, + { glob = 'a[\\!\\^\\-z\\]]z', expected = '^a[%!%^%-z%]]z$' }, + { glob = 'a[\\a-\\f]z', expected = '^a[a-f]z$' }, + + { glob = 'a[', expected = '[^]' }, + { glob = 'a[a-', expected = '[^]' }, + { glob = 'a[a-b', expected = '[^]' }, + { glob = 'a[!', expected = '[^]' }, + { glob = 'a[!a', expected = '[^]' }, + { glob = 'a[!a-', expected = '[^]' }, + { glob = 'a[!a-b', expected = '[^]' }, + { glob = 'a[!a-b\\]', expected = '[^]' }, + } + + for _, case in ipairs(cases) do + it('glob_to_pattern: ' .. case.glob, function() + local pattern = utils.glob_to_pattern(case.glob) + assert.equals(case.expected, pattern) + if case.matches then + for _, str in ipairs(case.matches) do + assert.is_true(str:match(pattern) ~= nil) + end + end + if case.not_matches then + for _, str in ipairs(case.not_matches) do + assert.is_false(str:match(pattern) ~= nil) + end + end + end) + end + + it('empty', function() + assert.is_true(utils.empty(nil)) + assert.is_true(utils.empty('')) + assert.is_true(utils.empty(' ')) + assert.is_true(utils.empty({})) + assert.is_false(utils.empty({ 1 })) + assert.is_false(utils.empty('abc')) + assert.is_false(utils.empty(0)) + end) + + it('split_lines', function() + assert.are.same(utils.split_lines(''), {}) + assert.are.same(utils.split_lines('a\nb'), { 'a', 'b' }) + assert.are.same(utils.split_lines('a\r\nb'), { 'a', 'b' }) + assert.are.same(utils.split_lines('a\nb\n'), { 'a', 'b', '' }) + end) + + it('make_string', function() + assert.equals('a b 1', utils.make_string('a', 'b', 1)) + assert.equals(vim.inspect({ x = 1 }), utils.make_string({ x = 1 })) + assert.equals('msg', utils.make_string('error:1: msg')) + end) + + it('uuid', function() + local uuid1 = utils.uuid() + local uuid2 = utils.uuid() + assert.equals('string', type(uuid1)) + assert.not_equals(uuid1, uuid2) + assert.equals(36, #uuid1) + end) + + it('to_table', function() + assert.are.same({ 1, 2, 3 }, utils.to_table(1, 2, 3)) + assert.are.same({ 1, 2, 3 }, utils.to_table({ 1, 2 }, 3)) + assert.are.same({ 1 }, utils.to_table(nil, 1)) + end) +end) From 97cc5143f07f3106b2dd4116105384866e4e7a27 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Aug 2025 20:19:32 +0200 Subject: [PATCH 533/728] test: add setup test for CopilotChat module (#1336) * test: add setup test for CopilotChat module Added a test to verify that CopilotChat can be set up without errors and that the chat module is initialized. Also updated .luarc.json to include additional globals used in tests. * [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> --- .luarc.json | 6 +++++- tests/init_spec.lua | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.luarc.json b/.luarc.json index 4a3cf0b6..c4cebd58 100644 --- a/.luarc.json +++ b/.luarc.json @@ -3,8 +3,12 @@ "diagnostics.globals": [ "describe", "it", + "pending", "before_each", - "after_each" + "after_each", + "clear", + "assert", + "print" ], "diagnostics.disable": ["redefined-local"] } diff --git a/tests/init_spec.lua b/tests/init_spec.lua index 23102727..59a42f62 100644 --- a/tests/init_spec.lua +++ b/tests/init_spec.lua @@ -4,4 +4,10 @@ describe('CopilotChat module', function() require('CopilotChat') end) end) + it('should be able to set up', function() + assert.has_no.errors(function() + require('CopilotChat').setup({}) + end) + assert.is_not_nil(require('CopilotChat').chat) + end) end) From 07d43b41d31c60d7b8f4afbc05b1a77e0239856e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Aug 2025 20:33:24 +0200 Subject: [PATCH 534/728] test(functions): add unit tests for functions module (#1337) * test(functions): add unit tests for functions module Add comprehensive unit tests for CopilotChat.functions covering uri_to_url, match_uri, parse_schema, and parse_input. This improves test coverage and ensures correct behavior for URI parsing and schema handling. * [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> --- tests/functions_spec.lua | 62 ++++++++++++++++++++++++++++++++++++++++ tests/init_spec.lua | 1 + 2 files changed, 63 insertions(+) create mode 100644 tests/functions_spec.lua diff --git a/tests/functions_spec.lua b/tests/functions_spec.lua new file mode 100644 index 00000000..93939cdb --- /dev/null +++ b/tests/functions_spec.lua @@ -0,0 +1,62 @@ +local functions = require('CopilotChat.functions') + +describe('CopilotChat.functions', function() + describe('uri_to_url', function() + it('replaces parameters in uri template', function() + local uri = 'file://{path}' + local input = { path = '/tmp/test.txt' } + assert.equals('file:///tmp/test.txt', functions.uri_to_url(uri, input)) + end) + it('leaves missing params empty', function() + local uri = 'file://{path}/{id}' + local input = { path = '/tmp' } + assert.equals('file:///tmp/', functions.uri_to_url(uri, input)) + end) + end) + + describe('match_uri', function() + it('matches uri and extracts parameters', function() + local uri = 'file:///tmp/test.txt' + local pattern = 'file://{path}' + local result = functions.match_uri(uri, pattern) + assert.are.same({ path = '/tmp/test.txt' }, result) + end) + it('returns nil for non-matching uri', function() + assert.is_nil(functions.match_uri('abc', 'file://{path}')) + end) + it('returns empty table for exact match with no params', function() + assert.are.same({}, functions.match_uri('abc', 'abc')) + end) + end) + + describe('parse_schema', function() + it('returns schema if present', function() + local fn = { schema = { type = 'object', properties = { foo = { type = 'string' } } } } + assert.equals(fn.schema, functions.parse_schema(fn)) + end) + it('generates schema from uri if missing', function() + local fn = { uri = 'file://{path}/{id}' } + local schema = functions.parse_schema(fn) + assert.are.same({ + type = 'object', + properties = { path = { type = 'string' }, id = { type = 'string' } }, + required = { 'path', 'id' }, + }, schema) + end) + end) + + describe('parse_input', function() + it('parses input string into table', function() + local schema = { properties = { a = {}, b = {} }, required = { 'a', 'b' } } + local input = 'foo;;bar' + assert.are.same({ a = 'foo', b = 'bar' }, functions.parse_input(input, schema)) + end) + it('returns input if already table', function() + local input = { a = 1 } + assert.equals(input, functions.parse_input(input)) + end) + it('returns empty table if no schema', function() + assert.are.same({}, functions.parse_input('foo')) + end) + end) +end) diff --git a/tests/init_spec.lua b/tests/init_spec.lua index 59a42f62..995a84c3 100644 --- a/tests/init_spec.lua +++ b/tests/init_spec.lua @@ -4,6 +4,7 @@ describe('CopilotChat module', function() require('CopilotChat') end) end) + it('should be able to set up', function() assert.has_no.errors(function() require('CopilotChat').setup({}) From 7b15d0350d1d96e5651961130287dbbb22397e9f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Aug 2025 21:39:01 +0200 Subject: [PATCH 535/728] test(makefile): use --clean for isolated test runs (#1338) Switches the test target to use `nvim --headless --clean` instead of `--noplugin` to ensure tests run in a fully isolated environment, avoiding interference from user or system configuration. This improves test reliability and consistency. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 04756dda..98cba272 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ install-pre-commit: pre-commit install test: - nvim --headless --noplugin -u ./scripts/test.lua + nvim --headless --clean -u ./scripts/test.lua all: luajit From a6434d56b5ff5344c20772c04cfdf66416ffe03f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Aug 2025 10:58:25 +0200 Subject: [PATCH 536/728] refactor(utils): remove custom glob to pattern logic (#1339) Replace custom glob pattern conversion and scandir fallback with native vim.lpeg and vim.uv.fs_scandir for file matching. This simplifies code, removes redundant logic, and improves compatibility. Also remove related unit tests for glob_to_pattern. Closes #1331 --- lua/CopilotChat/utils.lua | 211 ++++++++++++-------------------------- tests/utils_spec.lua | 48 --------- 2 files changed, 64 insertions(+), 195 deletions(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index c829d40e..b9d1ade0 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,6 +1,5 @@ local async = require('plenary.async') local curl = require('plenary.curl') -local scandir = require('plenary.scandir') local log = require('plenary.log') local M = {} @@ -479,7 +478,7 @@ end ---@class CopilotChat.utils.ScanOpts ---@field max_count number? The maximum number of files to scan ---@field max_depth number? The maximum depth to scan ----@field glob? string The glob pattern to match files +---@field pattern? string The glob pattern to match files ---@field hidden? boolean Whether to include hidden files ---@field no_ignore? boolean Whether to respect or ignore .gitignore @@ -527,19 +526,69 @@ M.glob = async.wrap(function(path, opts, callback) return end - -- Fall back to scandir if rg is not available or fails - scandir.scan_dir_async( - path, - vim.tbl_deep_extend('force', opts, { - depth = opts.max_depth, - add_dirs = false, - search_pattern = opts.glob and M.glob_to_pattern(opts.glob) or nil, - respect_gitignore = not opts.no_ignore, - on_exit = function(files) - callback(filter_files(files, opts.max_count)) - end, - }) - ) + -- Fallback to vim.uv.fs_scandir + local matchers = {} + if opts.pattern then + local file_pattern = vim.glob.to_lpeg(opts.pattern) + local path_pattern = vim.lpeg.P(path .. '/') * file_pattern + + table.insert(matchers, function(name, dir) + return file_pattern:match(name) or path_pattern:match(dir .. '/' .. name) + end) + end + + if not opts.hidden then + table.insert(matchers, function(name) + return not name:match('^%.') + end) + end + + local data = {} + local next_dir = { path } + local current_depths = { [path] = 1 } + + local function read_dir(err, fd) + local current_dir = table.remove(next_dir, 1) + local depth = current_depths[current_dir] or 1 + + if not err and fd then + while true do + local name, typ = vim.uv.fs_scandir_next(fd) + if name == nil then + break + end + + local full_path = current_dir .. '/' .. name + + if typ == 'directory' and not name:match('^%.git') then + if not opts.max_depth or depth < opts.max_depth then + table.insert(next_dir, full_path) + current_depths[full_path] = depth + 1 + end + else + local match = true + for _, matcher in ipairs(matchers) do + if not matcher(name, current_dir) then + match = false + break + end + end + + if match then + table.insert(data, full_path) + end + end + end + end + + if #next_dir == 0 then + callback(data) + else + vim.uv.fs_scandir(next_dir[1], read_dir) + end + end + + vim.uv.fs_scandir(path, read_dir) end, 3) --- Grep a directory @@ -783,136 +832,4 @@ function M.split_lines(text) return vim.split(text, '\r?\n', { trimempty = false }) end ---- Convert glob pattern to regex pattern ---- https://github.com/davidm/lua-glob-pattern/blob/master/lua/globtopattern.lua ----@param g string The glob pattern ----@return string -function M.glob_to_pattern(g) - local p = '^' -- pattern being built - local i = 0 -- index in g - local c -- char at index i in g. - - -- unescape glob char - local function unescape() - if c == '\\' then - i = i + 1 - c = g:sub(i, i) - if c == '' then - p = '[^]' - return false - end - end - return true - end - - -- escape pattern char - local function escape(c) - return c:match('^%w$') and c or '%' .. c - end - - -- Convert tokens at end of charset. - local function charset_end() - while 1 do - if c == '' then - p = '[^]' - return false - elseif c == ']' then - p = p .. ']' - break - else - if not unescape() then - break - end - local c1 = c - i = i + 1 - c = g:sub(i, i) - if c == '' then - p = '[^]' - return false - elseif c == '-' then - i = i + 1 - c = g:sub(i, i) - if c == '' then - p = '[^]' - return false - elseif c == ']' then - p = p .. escape(c1) .. '%-]' - break - else - if not unescape() then - break - end - p = p .. escape(c1) .. '-' .. escape(c) - end - elseif c == ']' then - p = p .. escape(c1) .. ']' - break - else - p = p .. escape(c1) - i = i - 1 -- put back - end - end - i = i + 1 - c = g:sub(i, i) - end - return true - end - - -- Convert tokens in charset. - local function charset() - i = i + 1 - c = g:sub(i, i) - if c == '' or c == ']' then - p = '[^]' - return false - elseif c == '^' or c == '!' then - i = i + 1 - c = g:sub(i, i) - if c == ']' then - -- ignored - else - p = p .. '[^' - if not charset_end() then - return false - end - end - else - p = p .. '[' - if not charset_end() then - return false - end - end - return true - end - - -- Convert tokens. - while 1 do - i = i + 1 - c = g:sub(i, i) - if c == '' then - p = p .. '$' - break - elseif c == '?' then - p = p .. '.' - elseif c == '*' then - p = p .. '.*' - elseif c == '[' then - if not charset() then - break - end - elseif c == '\\' then - i = i + 1 - c = g:sub(i, i) - if c == '' then - p = p .. '\\$' - break - end - p = p .. escape(c) - else - p = p .. escape(c) - end - end - return p -end - return M diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua index 2e45c83d..5352395d 100644 --- a/tests/utils_spec.lua +++ b/tests/utils_spec.lua @@ -1,54 +1,6 @@ local utils = require('CopilotChat.utils') describe('CopilotChat.utils', function() - local cases = { - { glob = '', expected = '^$' }, - { glob = 'abc', expected = '^abc$' }, - { glob = 'ab#/.', expected = '^ab%#%/%.$' }, - { glob = '\\\\\\ab\\c\\', expected = '^%\\abc\\$' }, - - { glob = 'abc.*', expected = '^abc%..*$', matches = { 'abc.txt', 'abc.' }, not_matches = { 'abc' } }, - { glob = '??.txt', expected = '^..%.txt$' }, - - { glob = 'a[]', expected = '[^]' }, - { glob = 'a[^]b', expected = '^ab$' }, - { glob = 'a[!]b', expected = '^ab$' }, - { glob = 'a[a][b]z', expected = '^a[a][b]z$' }, - { glob = 'a[a-f]z', expected = '^a[a-f]z$' }, - { glob = 'a[a-f0-9]z', expected = '^a[a-f0-9]z$' }, - { glob = 'a[a-f0-]z', expected = '^a[a-f0%-]z$' }, - { glob = 'a[!a-f]z', expected = '^a[^a-f]z$' }, - { glob = 'a[^a-f]z', expected = '^a[^a-f]z$' }, - { glob = 'a[\\!\\^\\-z\\]]z', expected = '^a[%!%^%-z%]]z$' }, - { glob = 'a[\\a-\\f]z', expected = '^a[a-f]z$' }, - - { glob = 'a[', expected = '[^]' }, - { glob = 'a[a-', expected = '[^]' }, - { glob = 'a[a-b', expected = '[^]' }, - { glob = 'a[!', expected = '[^]' }, - { glob = 'a[!a', expected = '[^]' }, - { glob = 'a[!a-', expected = '[^]' }, - { glob = 'a[!a-b', expected = '[^]' }, - { glob = 'a[!a-b\\]', expected = '[^]' }, - } - - for _, case in ipairs(cases) do - it('glob_to_pattern: ' .. case.glob, function() - local pattern = utils.glob_to_pattern(case.glob) - assert.equals(case.expected, pattern) - if case.matches then - for _, str in ipairs(case.matches) do - assert.is_true(str:match(pattern) ~= nil) - end - end - if case.not_matches then - for _, str in ipairs(case.not_matches) do - assert.is_false(str:match(pattern) ~= nil) - end - end - end) - end - it('empty', function() assert.is_true(utils.empty(nil)) assert.is_true(utils.empty('')) From 28e13a37eb31a667ed94379d3c249788eb59fb38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Aug 2025 08:58:47 +0000 Subject: [PATCH 537/728] 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 2d64906e..09e08ff9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 15 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 7993e6d2a97cb851b8b3a4087005cfaf8427dbf3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Aug 2025 22:41:22 +0200 Subject: [PATCH 538/728] fix(utils): avoid vim.filetype.match in fast event (#1344) Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index b9d1ade0..e8735a69 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -219,7 +219,7 @@ function M.filetype(filename) fs_access = false, }) - if ft == '' or not ft then + if ft == '' or not ft and not vim.in_fast_event() then return vim.filetype.match({ filename = filename }) end From f7bb32dbbe2ff5e26f5033e2142b5920cf427236 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 20:41:40 +0000 Subject: [PATCH 539/728] 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 09e08ff9..bbc0cd58 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 19 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9769bf9a1d215cf0dc22874712d5dcda53a075ee Mon Sep 17 00:00:00 2001 From: Ajmal S <23806715+AjmalShajahan@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:55:28 +0530 Subject: [PATCH 540/728] fix(makefile): handle MSYS_NT as a valid Windows environment (#1347) --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 98cba272..ebfe5768 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ else ifeq ($(UNAME), Darwin) else ifeq ($(UNAME), Windows_NT) OS := windows EXT := dll +else ifneq ($(findstring MSYS_NT,$(UNAME)),) + OS := windows + EXT := dll else $(error Unsupported operating system: $(UNAME)) endif From c62077134be1c3bd0bda52c9b1b5a7b7739fac27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Aug 2025 20:25:48 +0000 Subject: [PATCH 541/728] 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 bbc0cd58..2546dbe0 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 19 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 21 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 3496a487caf5776f33617648e06bf641f49d09fc Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 22:26:21 +0200 Subject: [PATCH 542/728] docs: add AjmalShajahan as a contributor for code (#1348) * 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 e8546bbf..3f6268f0 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -438,6 +438,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/488088?v=4", "profile": "https://mihamina.rktmb.org", "contributions": ["doc", "code"] + }, + { + "login": "AjmalShajahan", + "name": "Ajmal S", + "avatar_url": "https://avatars.githubusercontent.com/u/23806715?v=4", + "profile": "http://ajmalshajahan.me", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 62d03ee5..f45e06cc 100644 --- a/README.md +++ b/README.md @@ -626,6 +626,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

💻 📖 Danilo Horta
Danilo Horta

💻 Mihamina Rakotomandimby
Mihamina Rakotomandimby

📖 💻 + Ajmal S
Ajmal S

💻 From 80a0994f01096705e0c24dd7ed09032594689e01 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 24 Aug 2025 00:01:02 +0200 Subject: [PATCH 543/728] feat(ui): add auto_fold option for chat messages (#1354) Introduce the `auto_fold` config option to automatically fold non-assistant messages in the chat window and unfold assistant messages. This improves readability and navigation in long chat sessions. Closes #1300 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/ui/chat.lua | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index a3fce3a9..c9a86dbb 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -31,6 +31,7 @@ ---@field highlight_headers boolean? ---@field auto_follow_cursor boolean? ---@field auto_insert_mode boolean? +---@field auto_fold boolean? ---@field insert_at_end boolean? ---@field clear_chat_on_new_prompt boolean? @@ -90,6 +91,7 @@ return { highlight_headers = true, -- Highlight headers in chat auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + auto_fold = false, -- Automatically non-assistant messages in chat 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 diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index e7f0c75c..25d875d5 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -450,6 +450,18 @@ function Chat:add_message(message, replace) or current_message.role ~= message.role or (message.id and current_message.id ~= message.id) + if + self.config.auto_fold + and current_message + and current_message.role ~= constants.ROLE.ASSISTANT + and message.role ~= constants.ROLE.USER + and self:visible() + then + vim.api.nvim_win_call(self.winnr, function() + vim.cmd('normal! zc') + end) + end + if is_new then -- Add appropriate header based on role and generate a new ID if not provided message.id = message.id or utils.uuid() @@ -489,6 +501,12 @@ function Chat:add_message(message, replace) current_message.content = current_message.content .. message.content self:append(message.content) end + + if self.config.auto_fold and message.role == constants.ROLE.ASSISTANT and self:visible() then + vim.api.nvim_win_call(self.winnr, function() + vim.cmd('normal! zo') + end) + end end --- Remove a message from the chat window by role. From 81a7992584bcf4b634ab87d8fa98bef3905a937c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 23 Aug 2025 22:01:21 +0000 Subject: [PATCH 544/728] 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 2546dbe0..b0bb1c68 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 21 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 23 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -627,7 +627,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻This project follows the all-contributors specification. Contributions of any kind are welcome! From f30698d0163a7ce7c8d43a638d76a65fa354c9c7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 24 Aug 2025 00:20:50 +0200 Subject: [PATCH 545/728] docs: update README with nicer window config (#1355) Signed-off-by: Tomas Slusny --- README.md | 9 +++++---- lua/CopilotChat/config.lua | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f45e06cc..7dbba797 100644 --- a/README.md +++ b/README.md @@ -228,12 +228,13 @@ Most users only need to configure a few options: }, headers = { - user = '👤 You: ', - assistant = '🤖 Copilot: ', - tool = '🔧 Tool: ', + user = '👤 You', + assistant = '🤖 Copilot', + tool = '🔧 Tool', }, + separator = '━━', - show_folds = false, -- Disable folding for cleaner look + auto_fold = true, -- Automatically folds non-assistant messages } ``` diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index c9a86dbb..1a739c67 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -87,11 +87,11 @@ return { show_help = true, -- Shows help message as virtual lines when waiting for user input show_folds = true, -- Shows folds for sections in chat + auto_fold = false, -- Automatically non-assistant messages in chat (requires 'show_folds' to be true) highlight_selection = true, -- Highlight selection highlight_headers = true, -- Highlight headers in chat auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - auto_fold = false, -- Automatically non-assistant messages in chat 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 From f79f2928a3d2cb1ca5b7bc3c71bbf79d485c00cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 23 Aug 2025 22:21:08 +0000 Subject: [PATCH 546/728] 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 b0bb1c68..ce0418c3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -294,12 +294,13 @@ WINDOW & APPEARANCE *CopilotChat-window-&-appearance* }, headers = { - user = '👤 You: ', - assistant = '🤖 Copilot: ', - tool = '🔧 Tool: ', + user = '👤 You', + assistant = '🤖 Copilot', + tool = '🔧 Tool', }, + separator = '━━', - show_folds = false, -- Disable folding for cleaner look + auto_fold = true, -- Automatically folds non-assistant messages } < From a7679e118af8038046b2fc4c841406db7fe71216 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 15:31:39 +0200 Subject: [PATCH 547/728] feat(ui): improve auto folding logic in chat window (#1356) Refactors auto folding to occur during chat rendering instead of message addition. This ensures more consistent folding behavior for non-assistant messages and improves code clarity. Removes redundant fold open/close commands from add_message. No breaking changes. Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 25d875d5..670ed2b4 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -450,18 +450,6 @@ function Chat:add_message(message, replace) or current_message.role ~= message.role or (message.id and current_message.id ~= message.id) - if - self.config.auto_fold - and current_message - and current_message.role ~= constants.ROLE.ASSISTANT - and message.role ~= constants.ROLE.USER - and self:visible() - then - vim.api.nvim_win_call(self.winnr, function() - vim.cmd('normal! zc') - end) - end - if is_new then -- Add appropriate header based on role and generate a new ID if not provided message.id = message.id or utils.uuid() @@ -501,12 +489,6 @@ function Chat:add_message(message, replace) current_message.content = current_message.content .. message.content self:append(message.content) end - - if self.config.auto_fold and message.role == constants.ROLE.ASSISTANT and self:visible() then - vim.api.nvim_win_call(self.winnr, function() - vim.cmd('normal! zo') - end) - end end --- Remove a message from the chat window by role. @@ -754,7 +736,7 @@ function Chat:render() -- Replace self.messages with new_messages (preserving tool_calls, etc.) self.messages = new_messages - for _, message in ipairs(self.messages) do + for i, message in ipairs(self.messages) do -- Show tool call details as virt lines if message.tool_calls and #message.tool_calls > 0 then local section = message.section @@ -808,6 +790,16 @@ function Chat:render() strict = false, }) end + + if self.config.auto_fold and self:visible() then + if message.role ~= constants.ROLE.ASSISTANT and message.section and i < #self.messages then + vim.api.nvim_win_call(self.winnr, function() + if vim.fn.foldclosed(message.section.start_line) == -1 then + vim.api.nvim_cmd({ cmd = 'foldclose', range = { message.section.start_line } }, {}) + end + end) + end + end end -- Show help as before, using last user message From 98b50a6143ba2dedb545203e9b50eb36703def00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Aug 2025 13:36:59 +0000 Subject: [PATCH 548/728] 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 ce0418c3..ced4f2be 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 23 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 25 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From a2429ed44438f694f1fca60429a7984022d4a9f0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 19:45:40 +0200 Subject: [PATCH 549/728] refactor(core): remove selection API in favor of resources (#1340) Replaces the selection API with a unified resources mechanism for sharing context with the LLM. Updates config, mappings, and internal logic to use resources instead of selection. Deprecates selection-related functions and highlights. Improves flexibility and future extensibility for context injection. --- README.md | 28 +---- lua/CopilotChat/client.lua | 48 +++------ lua/CopilotChat/config.lua | 6 +- lua/CopilotChat/config/functions.lua | 29 +++++- lua/CopilotChat/config/mappings.lua | 77 +++++++------- lua/CopilotChat/init.lua | 125 ++++++---------------- lua/CopilotChat/select.lua | 149 ++++++++++++++------------- plugin/CopilotChat.lua | 26 ++--- 8 files changed, 214 insertions(+), 274 deletions(-) diff --git a/README.md b/README.md index 7dbba797..2be9d184 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,6 @@ EOF - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -- **Selection** - Automatically includes current user selection in prompts ## Examples @@ -266,6 +265,7 @@ Types of copilot highlights: - `CopilotChatHeader` - Header highlight in chat buffer - `CopilotChatSeparator` - Separator highlight in chat buffer +- `CopilotChatSelection` - Selection highlight in source buffer - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help text in chat buffer - `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) @@ -273,7 +273,6 @@ Types of copilot highlights: - `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) - `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) - `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) -- `CopilotChatSelection` - Selection highlight in source buffer - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) ## Prompts @@ -334,27 +333,6 @@ Define your own functions in the configuration with input handling and schema: } ``` -## Selections - -Control what content is automatically included: - -```lua -{ - -- Use visual selection, fallback to current line - selection = function(source) - return require('CopilotChat.select').visual(source) or - require('CopilotChat.select').line(source) - end, -} -``` - -**Available selections:** - -- `require('CopilotChat.select').visual` - Current visual selection -- `require('CopilotChat.select').buffer` - Entire buffer content -- `require('CopilotChat.select').line` - Current line content -- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked) - ## Providers Add custom AI providers: @@ -430,10 +408,6 @@ chat.stop() -- Stop current output chat.get_source() -- Get the current source buffer and window chat.set_source(winnr) -- Set the source window --- Selection Management -chat.get_selection() -- Get the current selection -chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection - -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index f2dba5b0..6121bdfe 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,7 +1,6 @@ ---@class CopilotChat.client.AskOptions ---@field headless boolean ---@field history table ----@field selection CopilotChat.select.Selection? ---@field tools table? ---@field resources table? ---@field system_prompt string @@ -32,11 +31,16 @@ ---@field description string description of the tool ---@field schema table? schema of the tool +---@class CopilotChat.client.ResourceAnnotations +---@field start_line number? +---@field end_line number? + ---@class CopilotChat.client.Resource ---@field data string ---@field name string? ---@field mimetype string? ---@field uri string? +---@field annotations CopilotChat.client.ResourceAnnotations? ---@class CopilotChat.client.Model ---@field provider string? @@ -106,29 +110,6 @@ local function generate_resource_block(content, mimetype, name, path, start_line end end ---- Generate messages for the given selection ---- @param selection CopilotChat.select.Selection ---- @return CopilotChat.client.Message? -local function generate_selection_message(selection) - local content = selection.content - - if not content or content == '' then - return nil - end - - return { - content = generate_resource_block( - content, - selection.filetype, - "User's active selection", - selection.filename, - selection.start_line, - selection.end_line - ), - role = constants.ROLE.USER, - } -end - --- Generate messages for the given resources --- @param resources CopilotChat.client.Resource[] --- @return table @@ -139,8 +120,17 @@ local function generate_resource_messages(resources) return resource.data and resource.data ~= '' end) :map(function(resource) + local start_line = resource.annotations and resource.annotations.start_line or 1 + local end_line = resource.annotations and resource.annotations.end_line or nil return { - content = generate_resource_block(resource.data, resource.mimetype, resource.uri, resource.name, 1, nil), + content = generate_resource_block( + resource.data, + resource.mimetype, + resource.uri, + resource.name, + start_line, + end_line + ), role = constants.ROLE.USER, } end) @@ -359,20 +349,14 @@ function Client:ask(prompt, opts) local history = not opts.headless and vim.deepcopy(opts.history) or {} local tool_calls = utils.ordered_map() local generated_messages = {} - local selection_message = opts.selection and generate_selection_message(opts.selection) local resource_messages = generate_resource_messages(opts.resources) - if selection_message then - table.insert(generated_messages, selection_message) - end - if max_tokens then -- Count required tokens that we cannot reduce - local selection_tokens = selection_message and tiktoken:count(selection_message.content) or 0 local prompt_tokens = tiktoken:count(prompt) local system_tokens = tiktoken:count(opts.system_prompt) local resource_tokens = #resource_messages > 0 and tiktoken:count(resource_messages[1].content) or 0 - local required_tokens = prompt_tokens + system_tokens + selection_tokens + resource_tokens + local required_tokens = prompt_tokens + system_tokens + resource_tokens -- Calculate how many tokens we can use for history local history_limit = max_tokens - required_tokens diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 1a739c67..d3f2b8cb 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -17,13 +17,13 @@ ---@field system_prompt nil|string|fun(source: CopilotChat.source):string ---@field model string? ---@field tools string|table|nil +---@field resources string|table|nil ---@field sticky string|table|nil ---@field language string? ---@field temperature number? ---@field headless boolean? ---@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.source) ---@field remember_as_sticky boolean? ----@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.Selection? ---@field window CopilotChat.config.Window? ---@field show_help boolean? ---@field show_folds boolean? @@ -58,6 +58,7 @@ return { model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + resources = 'selection', -- Default resources to share with LLM (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). language = 'English', -- Default language to use for answers @@ -66,9 +67,6 @@ return { callback = nil, -- Function called when full response is received remember_as_sticky = true, -- Remember config as sticky prompts when asking questions - -- default selection - selection = require('CopilotChat.select').visual, - -- default window options window = { layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 9050d267..eb067376 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -6,7 +6,7 @@ local utils = require('CopilotChat.utils') ---@field schema table? ---@field group string? ---@field uri string? ----@field resolve fun(input: table, source: CopilotChat.source, prompt: string):table +---@field resolve fun(input: table, source: CopilotChat.source):CopilotChat.client.Resource[] ---@type table return { @@ -214,6 +214,33 @@ return { end, }, + selection = { + group = 'copilot', + uri = 'neovim://selection', + description = 'Includes the content of the current visual selection. Useful for discussing specific code snippets or text blocks.', + + resolve = function(_, source) + utils.schedule_main() + local selection = require('CopilotChat.select').get(source.bufnr) + if not selection then + return {} + end + + return { + { + uri = 'neovim://selection', + name = selection.filename, + mimetype = utils.mimetype_to_filetype(selection.filetype), + data = selection.content, + annotations = { + start_line = selection.start_line, + end_line = selection.end_line, + }, + }, + } + end, + }, + quickfix = { group = 'copilot', uri = 'neovim://quickfix', diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index e3e2e248..ec42be85 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -2,6 +2,7 @@ local async = require('plenary.async') local copilot = require('CopilotChat') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') +local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') ---@class CopilotChat.config.mappings.Diff @@ -14,23 +15,33 @@ local utils = require('CopilotChat.utils') ---@field bufnr number? --- Get diff data from a block +---@param bufnr number ---@param block CopilotChat.ui.chat.Block? ---@return CopilotChat.config.mappings.Diff? -local function get_diff(block) +local function get_diff(bufnr, block) -- If no block found, return nil if not block then return nil end - -- Initialize variables with selection if available local header = block.header - local selection = copilot.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 + local selection = select.get(bufnr) + local filename = nil + local filetype = nil + local start_line = nil + local end_line = nil + local reference = nil + local bufnr = nil + + if selection then + -- If we have a selection, use it as default source of truth + filename = selection.filename + filetype = selection.filetype + start_line = selection.start_line + end_line = selection.end_line + reference = selection.content + bufnr = selection.bufnr + end -- If we have header info, use it as source of truth if header.start_line and header.end_line then @@ -236,7 +247,7 @@ return { normal = '', insert = '', callback = function(source) - local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) + local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -244,20 +255,22 @@ return { local lines = utils.split_lines(diff.change) vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) - copilot.set_selection(diff.bufnr, diff.start_line, diff.end_line) + select.set(source.bufnr, source.winnr, diff.start_line, diff.start_line + #lines - 1) + select.highlight(source.bufnr) end, }, jump_to_diff = { normal = 'gj', callback = function(source) - local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) + local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) diff = prepare_diff_buffer(diff, source) if not diff then return end - copilot.set_selection(diff.bufnr, diff.start_line, diff.end_line) + select.set(source.bufnr, source.winnr, diff.start_line, diff.end_line) + select.highlight(source.bufnr) end, }, @@ -289,32 +302,26 @@ return { quickfix_diffs = { normal = 'gqd', - callback = function() - local selection = copilot.get_selection() + callback = function(source) local items = {} for _, message in ipairs(copilot.chat.messages) do if message.section then for _, block in ipairs(message.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 + local diff = get_diff(source.bufnr, block) + if diff then + local text = string.format('%s (%s)', diff.filename, diff.filetype) + if diff.start_line and diff.end_line then + text = text .. string.format(' [lines %d-%d]', diff.start_line, diff.end_line) + end - 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) + table.insert(items, { + bufnr = copilot.chat.bufnr, + lnum = block.start_line, + end_lnum = block.end_line, + text = text, + }) end - - table.insert(items, { - bufnr = copilot.chat.bufnr, - lnum = block.start_line, - end_lnum = block.end_line, - text = text, - }) end end @@ -341,7 +348,7 @@ return { normal = 'gd', full_diff = false, -- Show full diff instead of unified diff when showing diff window callback = function(source) - local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) + local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) diff = prepare_diff_buffer(diff, source) if not diff then return @@ -362,7 +369,7 @@ return { local same_file_diffs = {} if section then for _, block in ipairs(section.blocks) do - local block_diff = get_diff(block) + local block_diff = get_diff(source.bufnr, block) if block_diff and block_diff.bufnr == diff.bufnr then table.insert(same_file_diffs, block_diff) end @@ -497,7 +504,7 @@ return { table.insert(lines, '') end - local selection = copilot.get_selection() + local selection = select.get(source.bufnr) if selection then table.insert(lines, '**Selection**') table.insert(lines, '') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 38ee5568..ba32542e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -4,6 +4,7 @@ local functions = require('CopilotChat.functions') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') +local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') local WORD = '([^%s:]+)' @@ -25,18 +26,22 @@ local M = setmetatable({}, { }) --- @class CopilotChat.source ---- @field bufnr number ---- @field winnr number +--- @field bufnr number? +--- @field winnr number? --- @field cwd fun():string --- @class CopilotChat.state --- @field source CopilotChat.source? --- @field sticky string[]? local state = { - -- Current state tracking - source = nil, + source = { + bufnr = nil, + winnr = nil, + cwd = function() + return '.' + end, + }, - -- Last state tracking sticky = nil, } @@ -80,6 +85,12 @@ local function insert_sticky(prompt, config) end end + if config.remember_as_sticky and config.resources and not vim.deep_equal(config.resources, M.config.resources) then + for _, resource in ipairs(utils.to_table(config.resources)) do + stickies:set('#' .. resource, true) + end + end + if config.remember_as_sticky and config.system_prompt @@ -129,27 +140,6 @@ local function store_sticky(prompt) state.sticky = sticky end ---- Update the highlights for chat buffer -local function update_highlights() - 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 M.chat.config.highlight_selection and M.chat:focused() then - local selection = M.get_selection() - if not selection or not utils.buf_valid(selection.bufnr) or not selection.start_line or not selection.end_line then - return - end - - 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 -end - --- List available models. --- @return CopilotChat.client.Model[] local function list_models() @@ -315,6 +305,16 @@ function M.resolve_functions(prompt, config) tools[tool.name] = tool end + if config.resources then + local resources = utils.to_table(config.resources) + local lines = utils.split_lines(prompt) + for i = #resources, 1, -1 do + local resource = resources[i] + table.insert(lines, 1, '#' .. resource) + end + prompt = table.concat(lines, '\n') + end + local enabled_tools = {} local resolved_resources = {} local resolved_tools = {} @@ -413,7 +413,7 @@ function M.resolve_functions(prompt, config) local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, prompt) + local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source) if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else @@ -527,9 +527,7 @@ function M.resolve_prompt(prompt, config) config.system_prompt = config.system_prompt .. '\n' .. M.config.prompts.COPILOT_BASE.system_prompt config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) - if state.source then - config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) - end + config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end return config, prompt @@ -592,55 +590,6 @@ function M.set_source(source_winnr) return false end ---- Get the selection from the source buffer. ----@return CopilotChat.select.Selection? -function M.get_selection() - local config = vim.tbl_deep_extend('force', M.config, M.chat.config) - local selection = config.selection - local bufnr = state.source and state.source.bufnr - local winnr = state.source and state.source.winnr - - if selection and utils.buf_valid(bufnr) and winnr and vim.api.nvim_win_is_valid(winnr) then - return selection(state.source) - end - - return nil -end - ---- Sets the selection to specific lines in buffer. ----@param bufnr number ----@param start_line number ----@param end_line number ----@param clear boolean? -function M.set_selection(bufnr, start_line, end_line, clear) - if not utils.buf_valid(bufnr) then - return - end - - if clear then - for _, mark in ipairs({ '<', '>', '[', ']' }) do - pcall(vim.api.nvim_buf_del_mark, bufnr, mark) - end - update_highlights() - return - end - - local winnr = vim.fn.win_findbuf(bufnr)[1] - if not winnr and state.source then - winnr = state.source.winnr - end - if not winnr then - return - end - - 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, {}) - pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) - update_highlights() -end - --- Open the chat window. ---@param config CopilotChat.config.Shared? function M.open(config) @@ -667,7 +616,7 @@ end --- Close the chat window. function M.close() - M.chat:close(state.source and state.source.bufnr or nil) + M.chat:close(state.source.bufnr) end --- Toggle the chat window. @@ -822,9 +771,6 @@ function M.ask(prompt, config) '\n' ) - -- Retrieve the selection - local selection = M.get_selection() - async.run(handle_error(config, function() local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) @@ -883,7 +829,6 @@ function M.ask(prompt, config) local ask_response = client.ask(client, prompt, { headless = config.headless, history = M.chat.messages, - selection = selection, resources = resolved_resources, tools = selected_tools, system_prompt = system_prompt, @@ -937,11 +882,7 @@ function M.stop(reset) if reset then M.chat:clear() vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) - - -- Clear the selection - if state.source then - M.set_selection(state.source.bufnr, 0, 0, true) - end + select.set(state.source.bufnr) end if stopped or reset then @@ -1078,7 +1019,7 @@ function M.setup(config) end if M.chat then - M.chat:close(state.source and state.source.bufnr or nil) + M.chat:close(state.source.bufnr) M.chat:delete() end M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) @@ -1095,7 +1036,9 @@ function M.setup(config) update_source() end - vim.schedule(update_highlights) + vim.schedule(function() + select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) + end) end, }) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 8bef366c..1d2b8bcb 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -6,90 +6,70 @@ ---@field filetype string ---@field bufnr number +local constants = require('CopilotChat.constants') +local utils = require('CopilotChat.utils') + local M = {} ---- Select and process current visual selection ---- @param source CopilotChat.source ---- @return CopilotChat.select.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, '>')) - 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 +---@deprecated +function M.visual(_) + vim.deprecate('CopilotChat.select.visual', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil +end - 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 +---@deprecated +function M.buffer(_) + vim.deprecate('CopilotChat.select.buffer', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil +end - return { - 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, - } +---@deprecated +function M.line(_) + vim.deprecate('CopilotChat.select.line', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil end ---- Select and process whole buffer ---- @param source CopilotChat.source ---- @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) - if not lines or #lines == 0 then - return nil +---@deprecated +function M.unnamed(_) + vim.deprecate('CopilotChat.select.unnamed', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil +end + +--- Highlight selection in target buffer or clear it +---@param bufnr number +---@param clear boolean? +function M.highlight(bufnr, 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 { - 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, - } -end + if clear then + return + end ---- Select and process current line ---- @param source CopilotChat.source ---- @return CopilotChat.select.Selection|nil -function M.line(source) - local bufnr = source.bufnr - 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 + local selection = M.get(bufnr) + if not selection then + return end - return { - 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, - } + 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 ---- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. ---- @param source CopilotChat.source ---- @return CopilotChat.select.Selection|nil -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, ']')) +--- Get the selection from the target buffer +---@param bufnr number +---@return CopilotChat.select.Selection? +function M.get(bufnr) + if not utils.buf_valid(bufnr) then + return nil + end + + 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 @@ -116,4 +96,31 @@ function M.unnamed(source) } end +--- Sets the selection to specific lines in buffer or clears it +---@param bufnr number +---@param winnr number? +---@param start_line number? +---@param end_line number? +function M.set(bufnr, winnr, start_line, end_line) + if not utils.buf_valid(bufnr) then + return + end + + if not start_line or not end_line then + for _, mark in ipairs({ '<', '>', '[', ']' }) do + pcall(vim.api.nvim_buf_del_mark, bufnr, mark) + end + return + end + + 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, {}) + + if winnr and vim.api.nvim_win_is_valid(winnr) then + pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) + end +end + return M diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index e59ee49e..db83d5b2 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -14,6 +14,7 @@ local group = vim.api.nvim_create_augroup('CopilotChat', {}) local function setup_highlights() vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatResource', { link = 'Constant', default = true }) @@ -21,7 +22,6 @@ local function setup_highlights() vim.api.nvim_set_hl(0, 'CopilotChatPrompt', { link = 'Statement', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatModel', { link = 'Type', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatUri', { link = 'Underlined', default = true }) - vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatAnnotation', { link = 'ColorColumn', default = true }) local fg = vim.api.nvim_get_hl(0, { name = 'CopilotChatStatus', link = false }).fg @@ -36,6 +36,18 @@ vim.api.nvim_create_autocmd('ColorScheme', { }) setup_highlights() +vim.api.nvim_create_autocmd('FileType', { + pattern = 'copilot-chat', + group = group, + callback = vim.schedule_wrap(function() + vim.cmd.syntax('match CopilotChatResource "#\\S\\+"') + vim.cmd.syntax('match CopilotChatTool "@\\S\\+"') + vim.cmd.syntax('match CopilotChatPrompt "/\\S\\+"') + vim.cmd.syntax('match CopilotChatModel "\\$\\S\\+"') + vim.cmd.syntax('match CopilotChatUri "##\\S\\+"') + end), +}) + -- Setup commands vim.api.nvim_create_user_command('CopilotChat', function(args) local chat = require('CopilotChat') @@ -79,18 +91,6 @@ vim.api.nvim_create_user_command('CopilotChatReset', function() chat.reset() end, { force = true }) -vim.api.nvim_create_autocmd('FileType', { - pattern = 'copilot-chat', - group = group, - callback = vim.schedule_wrap(function() - vim.cmd.syntax('match CopilotChatResource "#\\S\\+"') - vim.cmd.syntax('match CopilotChatTool "@\\S\\+"') - vim.cmd.syntax('match CopilotChatPrompt "/\\S\\+"') - vim.cmd.syntax('match CopilotChatModel "\\$\\S\\+"') - vim.cmd.syntax('match CopilotChatUri "##\\S\\+"') - end), -}) - local function complete_load() local chat = require('CopilotChat') local options = vim.tbl_map(function(file) From a7d95ffebf14ddb6709c328754c4164337dc70d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Aug 2025 17:46:05 +0000 Subject: [PATCH 550/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ced4f2be..e9de6f50 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -23,7 +23,6 @@ Table of Contents *CopilotChat-table-of-contents* - Highlights |CopilotChat-highlights| - Prompts |CopilotChat-prompts| - Functions |CopilotChat-functions| - - Selections |CopilotChat-selections| - Providers |CopilotChat-providers| 5. API Reference |CopilotChat-api-reference| - Core |CopilotChat-core| @@ -128,7 +127,6 @@ VIM-PLUG *CopilotChat-vim-plug* - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -- **Selection** - Automatically includes current user selection in prompts EXAMPLES *CopilotChat-examples* @@ -334,6 +332,7 @@ Types of copilot highlights: - `CopilotChatHeader` - Header highlight in chat buffer - `CopilotChatSeparator` - Separator highlight in chat buffer +- `CopilotChatSelection` - Selection highlight in source buffer - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help text in chat buffer - `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) @@ -341,7 +340,6 @@ Types of copilot highlights: - `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) - `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) - `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) -- `CopilotChatSelection` - Selection highlight in source buffer - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) @@ -405,28 +403,6 @@ Define your own functions in the configuration with input handling and schema: < -SELECTIONS *CopilotChat-selections* - -Control what content is automatically included: - ->lua - { - -- Use visual selection, fallback to current line - selection = function(source) - return require('CopilotChat.select').visual(source) or - require('CopilotChat.select').line(source) - end, - } -< - -**Available selections:** - -- `require('CopilotChat.select').visual` - Current visual selection -- `require('CopilotChat.select').buffer` - Entire buffer content -- `require('CopilotChat.select').line` - Current line content -- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked) - - PROVIDERS *CopilotChat-providers* Add custom AI providers: @@ -505,10 +481,6 @@ CORE *CopilotChat-core* chat.get_source() -- Get the current source buffer and window chat.set_source(winnr) -- Set the source window - -- Selection Management - chat.get_selection() -- Get the current selection - chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection - -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector From 05708d30f763084619914852d1d531cbaf4a7117 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 19:48:45 +0200 Subject: [PATCH 551/728] refactor(prompts): use resources instead of sticky for Commit (#1357) Replaces the 'sticky' property with 'resources' in the Commit prompt configuration to improve clarity and maintain consistency with other prompt definitions. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/prompts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 203997a7..073a8e8b 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -195,6 +195,6 @@ If no issues found, confirm the code is well-written and explain why. Commit = { prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - sticky = '#gitdiff:staged', + resources = 'gitdiff:staged', }, } From c7d85478f775a65ca777cb9b2f685911cbcd8def Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 20:01:36 +0200 Subject: [PATCH 552/728] feat(docs): add selection source to function table (#1358) Documented the new `selection` source in the function table, clarifying its usage for including the current visual selection. This improves discoverability and helps users understand available sources. Signed-off-by: Tomas Slusny --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2be9d184..ad3cd3ef 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ All predefined functions belong to the `copilot` group. | `grep` | Searches for a pattern across files in workspace | `#grep:TODO` | | `quickfix` | Includes content of files in quickfix list | `#quickfix` | | `register` | Provides access to specified Vim register | `#register:+` | +| `selection` | Includes the current visual selection | `#selection` | | `url` | Fetches content from a specified URL | `#url:https://...` | ## Predefined Prompts From 8e2c2e2d792e9b68dc5a229f097ca3c61fb1d047 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Aug 2025 18:01:53 +0000 Subject: [PATCH 553/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e9de6f50..e489a6c6 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -230,6 +230,8 @@ All predefined functions belong to the `copilot` group. register Provides access to specified Vim register #register:+ + selection Includes the current visual selection #selection + url Fetches content from a specified URL #url:https://... ------------------------------------------------------------------------------ From c37ec3cbdb2c29be73d7d0c48057d64306aa185f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 20:09:46 +0200 Subject: [PATCH 554/728] feat(config): add back selection source config option (#1360) Reintroduce `selection` config option to choose between 'visual' and 'unnamed' selection sources. Update selection mark handling in select.lua to respect the configured source, improving flexibility for user workflows. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/select.lua | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index d3f2b8cb..ba24edb1 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -20,6 +20,7 @@ ---@field resources string|table|nil ---@field sticky string|table|nil ---@field language string? +---@field selection 'visual'|'unnamed'|nil ---@field temperature number? ---@field headless boolean? ---@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.source) @@ -62,6 +63,7 @@ return { sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). language = 'English', -- Default language to use for answers + selection = 'visual', -- Selection source temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 1d2b8bcb..669876df 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -8,6 +8,7 @@ local constants = require('CopilotChat.constants') local utils = require('CopilotChat.utils') +local config = require('CopilotChat.config') local M = {} @@ -35,6 +36,16 @@ function M.unnamed(_) return nil end +--- Get the marks used for selection +---@return string[] +function M.marks() + local marks = { '<', '>' } + if config.selection == 'unnamed' then + marks = { '[', ']' } + end + return marks +end + --- Highlight selection in target buffer or clear it ---@param bufnr number ---@param clear boolean? @@ -68,8 +79,9 @@ function M.get(bufnr) return nil end - local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) - local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) + local marks = M.marks() + local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, marks[1])) + local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, marks[2])) if start_line == 0 or finish_line == 0 then return nil end @@ -106,17 +118,17 @@ function M.set(bufnr, winnr, start_line, end_line) return end + local marks = M.marks() + if not start_line or not end_line then - for _, mark in ipairs({ '<', '>', '[', ']' }) do + for _, mark in ipairs(marks) do pcall(vim.api.nvim_buf_del_mark, bufnr, mark) end return end - 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, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, marks[1], start_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, marks[2], end_line, 0, {}) if winnr and vim.api.nvim_win_is_valid(winnr) then pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) From 19a38dd34e1b61c49349552598e43b2559be2fc7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 20:15:33 +0200 Subject: [PATCH 555/728] fix(select): move config inside of marks function to prevent import loop (#1361) Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 669876df..e177aff0 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -8,7 +8,6 @@ local constants = require('CopilotChat.constants') local utils = require('CopilotChat.utils') -local config = require('CopilotChat.config') local M = {} @@ -39,6 +38,7 @@ end --- Get the marks used for selection ---@return string[] function M.marks() + local config = require('CopilotChat.config') local marks = { '<', '>' } if config.selection == 'unnamed' then marks = { '[', ']' } From fdac67ab62085436b60003f420ae45f104bdf935 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Tue, 26 Aug 2025 00:24:31 +0600 Subject: [PATCH 556/728] fix(completion.lua): check if window is valid before calling get_cursor (#1359) Signed-off-by: sami --- lua/CopilotChat/completion.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/CopilotChat/completion.lua b/lua/CopilotChat/completion.lua index 6c65a18d..97b3e9d4 100644 --- a/lua/CopilotChat/completion.lua +++ b/lua/CopilotChat/completion.lua @@ -167,6 +167,10 @@ function M.complete(without_input) local items = M.items() utils.schedule_main() + if not vim.api.nvim_win_is_valid(win) then + return + end + local row_changed = vim.api.nvim_win_get_cursor(win)[1] ~= row local mode = vim.api.nvim_get_mode().mode if row_changed or not (mode == 'i' or mode == 'ic') then From e879659ed67be1c774d4598bce4a119a200f6622 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:25:10 +0200 Subject: [PATCH 557/728] docs: add samiulsami as a contributor for code (#1362) * 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, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3f6268f0..3dee6f1f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -445,6 +445,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/23806715?v=4", "profile": "http://ajmalshajahan.me", "contributions": ["code"] + }, + { + "login": "samiulsami", + "name": "Samiul Islam", + "avatar_url": "https://avatars.githubusercontent.com/u/33352407?v=4", + "profile": "https://github.com/samiulsami", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ad3cd3ef..79e8d7e1 100644 --- a/README.md +++ b/README.md @@ -604,6 +604,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Mihamina Rakotomandimby
Mihamina Rakotomandimby

📖 💻 Ajmal S
Ajmal S

💻 + + Samiul Islam
Samiul Islam

💻 + From 8d8f1e7ea594b2db3368e1fa62dd7d0d128e8860 Mon Sep 17 00:00:00 2001 From: Mihamina Rakotomandimby Date: Mon, 25 Aug 2025 22:48:58 +0300 Subject: [PATCH 558/728] feat(functions): add configuration parameter to stop on tool failure (#1364) Co-authored-by: Mihamina RKTMB --- lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/init.lua | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index ba24edb1..49205784 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -35,6 +35,7 @@ ---@field auto_fold boolean? ---@field insert_at_end boolean? ---@field clear_chat_on_new_prompt boolean? +---@field stop_on_tool_failure boolean? --- CopilotChat default configuration ---@class CopilotChat.config.Config : CopilotChat.config.Shared @@ -94,6 +95,7 @@ return { 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 + stop_on_tool_failure = false, -- Stop processing prompt if any tool fails (preserves quota) -- Static config starts here (can be configured only via setup function) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ba32542e..2e62f4d9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -413,7 +413,13 @@ function M.resolve_functions(prompt, config) local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source) + local ok, output + if config.stop_on_tool_failure then + output = tool.resolve(functions.parse_input(input, schema), state.source) + ok = true + else + ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source) + end if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else From e75d50358fd7cfd82ed98b5dea95ac33925a026f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Aug 2025 19:49:24 +0000 Subject: [PATCH 559/728] 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 e489a6c6..9ac94fd3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -602,7 +602,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 3ab915042a118f22c4730dff4b20cb7d7220fd64 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Aug 2025 21:54:32 +0200 Subject: [PATCH 560/728] refactor(config): rename tool failure option for clarity (#1365) Renames `stop_on_tool_failure` to `stop_on_function_failure` in config and related code for improved clarity and consistency. Updates type annotations and usage to reflect the new naming. No functional changes. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 8 ++++---- lua/CopilotChat/init.lua | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 49205784..b525e995 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -20,7 +20,6 @@ ---@field resources string|table|nil ---@field sticky string|table|nil ---@field language string? ----@field selection 'visual'|'unnamed'|nil ---@field temperature number? ---@field headless boolean? ---@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.source) @@ -35,7 +34,7 @@ ---@field auto_fold boolean? ---@field insert_at_end boolean? ---@field clear_chat_on_new_prompt boolean? ----@field stop_on_tool_failure boolean? +---@field stop_on_function_failure boolean? --- CopilotChat default configuration ---@class CopilotChat.config.Config : CopilotChat.config.Shared @@ -43,6 +42,7 @@ ---@field log_level 'trace'|'debug'|'info'|'warn'|'error'|'fatal'? ---@field proxy string? ---@field allow_insecure boolean? +---@field selection 'visual'|'unnamed'|nil ---@field chat_autocomplete boolean? ---@field log_path string? ---@field history_path string? @@ -64,7 +64,6 @@ return { sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). language = 'English', -- Default language to use for answers - selection = 'visual', -- Selection source temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received @@ -95,7 +94,7 @@ return { 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 - stop_on_tool_failure = false, -- Stop processing prompt if any tool fails (preserves quota) + stop_on_function_failure = false, -- Stop processing prompt if any function fails (preserves quota) -- Static config starts here (can be configured only via setup function) @@ -104,6 +103,7 @@ return { proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + selection = 'visual', -- Selection source chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 2e62f4d9..d7b111f8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -412,14 +412,15 @@ function M.resolve_functions(prompt, config) end local schema = tools[name] and tools[name].schema or nil - local result = '' local ok, output - if config.stop_on_tool_failure then + if config.stop_on_function_failure then output = tool.resolve(functions.parse_input(input, schema), state.source) ok = true else ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source) end + + local result = '' if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else From 6bdac49da12b0ac2ddbc1a59997a872ea5396112 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Aug 2025 01:11:00 +0200 Subject: [PATCH 561/728] refactor(utils): split class, ordered map, string buffer (#1366) Move class, ordered map, and string buffer utilities into separate modules for better organization and maintainability. Update all references to use the new modules. This change improves code clarity and makes future extensions easier. Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 16 ++-- lua/CopilotChat/init.lua | 5 +- lua/CopilotChat/tiktoken.lua | 2 +- lua/CopilotChat/ui/chat.lua | 2 +- lua/CopilotChat/ui/overlay.lua | 2 +- lua/CopilotChat/ui/spinner.lua | 3 +- lua/CopilotChat/utils.lua | 108 ------------------------- lua/CopilotChat/utils/class.lua | 38 +++++++++ lua/CopilotChat/utils/orderedmap.lua | 39 +++++++++ lua/CopilotChat/utils/stringbuffer.lua | 46 +++++++++++ 10 files changed, 140 insertions(+), 121 deletions(-) create mode 100644 lua/CopilotChat/utils/class.lua create mode 100644 lua/CopilotChat/utils/orderedmap.lua create mode 100644 lua/CopilotChat/utils/stringbuffer.lua diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 6121bdfe..a78fd3e9 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -58,7 +58,9 @@ local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') -local class = utils.class +local class = require('CopilotChat.utils.class') +local orderedmap = require('CopilotChat.utils.orderedmap') +local stringbuffer = require('CopilotChat.utils.stringbuffer') --- Constants local RESOURCE_SHORT_FORMAT = '# %s\n```%s start_line=% end_line=%s\n%s\n```' @@ -186,7 +188,7 @@ end) ---@param supported_method? string: The method to filter providers by (optional) ---@return OrderedMap function Client:get_providers(supported_method) - local out = utils.ordered_map() + local out = orderedmap() if not self.provider_resolver then return out @@ -347,7 +349,7 @@ function Client:ask(prompt, opts) end local history = not opts.headless and vim.deepcopy(opts.history) or {} - local tool_calls = utils.ordered_map() + local tool_calls = orderedmap() local generated_messages = {} local resource_messages = generate_resource_messages(opts.resources) @@ -392,8 +394,8 @@ function Client:ask(prompt, opts) local errored = nil local finished = false local token_count = 0 - local response_content_buffer = utils.string_buffer() - local response_reasoning_buffer = utils.string_buffer() + local response_content_buffer = stringbuffer() + local response_reasoning_buffer = stringbuffer() local function finish_stream(err, job) if err then @@ -447,11 +449,11 @@ function Client:ask(prompt, opts) end if out.content then - response_content_buffer:add(out.content) + response_content_buffer:put(out.content) end if out.reasoning then - response_reasoning_buffer:add(out.reasoning) + response_reasoning_buffer:put(out.reasoning) end if opts.on_progress then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d7b111f8..6c56a380 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -6,6 +6,7 @@ local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') +local orderedmap = require('CopilotChat.utils.orderedmap') local WORD = '([^%s:]+)' local WORD_NO_INPUT = '([^%s]+)' @@ -52,7 +53,7 @@ local function insert_sticky(prompt, config) local existing_prompt = M.chat:get_message(constants.ROLE.USER) local combined_prompt = (existing_prompt and existing_prompt.content or '') .. '\n' .. (prompt or '') local lines = vim.split(prompt or '', '\n') - local stickies = utils.ordered_map() + local stickies = orderedmap() local sticky_indices = {} local in_code_block = false @@ -346,7 +347,7 @@ function M.resolve_functions(prompt, config) end end - local matches = utils.ordered_map() + local matches = orderedmap() -- Check for #word:`input` pattern for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_QUOTED) do diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 3f631428..652196eb 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,6 +1,6 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') -local class = utils.class +local class = require('CopilotChat.utils.class') --- Get the library extension based on the operating system --- @return string diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 670ed2b4..2de10bde 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -3,7 +3,7 @@ local Spinner = require('CopilotChat.ui.spinner') local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') -local class = utils.class +local class = require('CopilotChat.utils.class') function CopilotChatFoldExpr(lnum, separator) local to_match = separator .. '$' diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index a23c022e..298bfcb2 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -1,5 +1,5 @@ local utils = require('CopilotChat.utils') -local class = utils.class +local class = require('CopilotChat.utils.class') ---@class CopilotChat.ui.overlay.Overlay : Class ---@field bufnr number? diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index 0f582032..44c77b80 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -1,6 +1,7 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') -local class = utils.class +local class = require('CopilotChat.utils.class') + local spinner_frames = { '⠋', '⠙', diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index e8735a69..f0579669 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -28,81 +28,6 @@ M.curl_args = { }, } ----@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 Class -function M.class(fn, parent) - local out = {} - out.__index = out - - local mt = { - __call = function(cls, ...) - return cls.new(...) - end, - } - - if parent then - mt.__index = parent - end - - setmetatable(out, mt) - - function out.new(...) - local self = setmetatable({}, out) - fn(self, ...) - return self - end - - function out.init(self, ...) - fn(self, ...) - end - - 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 ----@generic K, V ----@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 - --- Convert arguments to a table ---@param ... any The arguments ---@return table @@ -121,39 +46,6 @@ function M.to_table(...) return result end ----@class StringBuffer ----@field add fun(self:StringBuffer, s:string) ----@field set fun(self:StringBuffer, s:string) ----@field tostring fun(self:StringBuffer):string - ---- Create a string buffer for efficient string concatenation ----@return StringBuffer -function M.string_buffer() - return { - _buf = { '' }, - - add = function(self, s) - table.insert(self._buf, s) - -- Keep track of lengths to know when to merge - for i = #self._buf - 1, 1, -1 do - if #self._buf[i] > #self._buf[i + 1] then - break - end - self._buf[i] = self._buf[i] .. table.remove(self._buf) - end - end, - - set = function(self, s) - self._buf = { s } - end, - - -- Get final string - tostring = function(self) - return table.concat(self._buf) - end, - } -end - --- Writes text to a temporary file and returns path ---@param text string The text to write ---@return string diff --git a/lua/CopilotChat/utils/class.lua b/lua/CopilotChat/utils/class.lua new file mode 100644 index 00000000..b8dfce83 --- /dev/null +++ b/lua/CopilotChat/utils/class.lua @@ -0,0 +1,38 @@ +---@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 Class +local function class(fn, parent) + local out = {} + out.__index = out + + local mt = { + __call = function(cls, ...) + return cls.new(...) + end, + } + + if parent then + mt.__index = parent + end + + setmetatable(out, mt) + + function out.new(...) + local self = setmetatable({}, out) + fn(self, ...) + return self + end + + function out.init(self, ...) + fn(self, ...) + end + + return out +end + +return class diff --git a/lua/CopilotChat/utils/orderedmap.lua b/lua/CopilotChat/utils/orderedmap.lua new file mode 100644 index 00000000..778c686d --- /dev/null +++ b/lua/CopilotChat/utils/orderedmap.lua @@ -0,0 +1,39 @@ +---@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 ordered map +---@generic K, V +---@return OrderedMap +local function orderedmap() + 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 orderedmap diff --git a/lua/CopilotChat/utils/stringbuffer.lua b/lua/CopilotChat/utils/stringbuffer.lua new file mode 100644 index 00000000..de89f2db --- /dev/null +++ b/lua/CopilotChat/utils/stringbuffer.lua @@ -0,0 +1,46 @@ +local ok, jit_buffer = pcall(require, 'string.buffer') + +---@class StringBuffer +---@field put fun(self:StringBuffer, s:string) +---@field set fun(self:StringBuffer, s:string) +---@field tostring fun(self:StringBuffer):string + +--- Create a string buffer for efficient string concatenation +---@return StringBuffer +local function stringbuffer() + if ok and jit_buffer then + return { + _buf = jit_buffer.new(), + put = function(self, s) + self._buf:put(s) + end, + set = function(self, s) + self._buf:set(s) + end, + tostring = function(self) + return self._buf:tostring() + end, + } + end + + return { + _buf = { '' }, + put = function(self, s) + table.insert(self._buf, s) + for i = #self._buf - 1, 1, -1 do + if #self._buf[i] > #self._buf[i + 1] then + break + end + self._buf[i] = self._buf[i] .. table.remove(self._buf) + end + end, + set = function(self, s) + self._buf = { s } + end, + tostring = function(self) + return table.concat(self._buf) + end, + } +end + +return stringbuffer From 7b4a56b29ed926b680ea936bd29fc8568b909d97 Mon Sep 17 00:00:00 2001 From: Rui Costa Date: Tue, 26 Aug 2025 00:21:06 +0100 Subject: [PATCH 562/728] feat(functions): add scope=selection to diagnostics (#1351) * feat(functions): add scope=selection to diagnostics Adds a new scope to the diagnostics function where the user specifies that only diagnostics found inside the visual selection are to be included in the prompt's context. * fix(functions) use new selection API on diagnostics:selection * fix(functions) address review comments - no need for scope conditional bc we're using the whole buffer as default selection - pass buffer id when using selection.get - return early if mode is selection but there's nothing selected * fix(functions) flip conditional to avoid empty block --- lua/CopilotChat/config/functions.lua | 49 +++++++++++++++++++--------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index eb067376..7d7be042 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -307,7 +307,7 @@ return { scope = { type = 'string', description = 'Scope of buffers to use for retrieving diagnostics.', - enum = { 'current', 'listed', 'visible' }, + enum = { 'current', 'listed', 'visible', 'selection' }, default = 'current', }, severity = { @@ -326,7 +326,7 @@ return { local buffers = {} -- Get buffers based on scope - if scope == 'current' then + if scope == 'current' or scope == 'selection' then if source and source.bufnr and utils.buf_valid(source.bufnr) then buffers = { source.bufnr } end @@ -344,6 +344,21 @@ return { end, vim.api.nvim_list_bufs()) end + -- By default, collect from the whole buffer + local selection_start_line = 1 + local selection_end_line = vim.api.nvim_buf_line_count(source.bufnr) + -- Determine selection range if scope is 'selection' + if scope == 'selection' then + local select = require('CopilotChat.select') + local selection = select.get(source.bufnr) + if selection then + selection_start_line = selection.start_line + selection_end_line = selection.end_line + else + return out + end + end + -- Collect diagnostics for each buffer for _, bufnr in ipairs(buffers) do local name = vim.api.nvim_buf_get_name(bufnr) @@ -356,20 +371,24 @@ return { if #diagnostics > 0 then local diag_lines = {} for _, diag in ipairs(diagnostics) do - local severity = vim.diagnostic.severity[diag.severity] or 'UNKNOWN' - local line_text = vim.api.nvim_buf_get_lines(bufnr, diag.lnum, diag.lnum + 1, false)[1] or '' - - table.insert( - diag_lines, - string.format( - '%s line=%d-%d: %s\n > %s', - severity, - diag.lnum + 1, - diag.end_lnum and (diag.end_lnum + 1) or (diag.lnum + 1), - diag.message, - line_text + -- Diagnostics.lnum are 0-indexed, so add 1 for comparison + local diag_lnum = diag.lnum + 1 + if diag_lnum >= selection_start_line and diag_lnum <= selection_end_line then + local severity = vim.diagnostic.severity[diag.severity] or 'UNKNOWN' + local line_text = vim.api.nvim_buf_get_lines(bufnr, diag.lnum, diag.lnum + 1, false)[1] or '' + + table.insert( + diag_lines, + string.format( + '%s line=%d-%d: %s\n > %s', + severity, + diag.lnum + 1, + diag.end_lnum and (diag.end_lnum + 1) or (diag.lnum + 1), + diag.message, + line_text + ) ) - ) + end end table.insert(out, { From f4ff91e32893c89e5970c1a9cd5196c57596321a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 01:22:21 +0200 Subject: [PATCH 563/728] docs: add ruicsh as a contributor for code (#1368) * 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 3dee6f1f..933a828e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -452,6 +452,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/33352407?v=4", "profile": "https://github.com/samiulsami", "contributions": ["code"] + }, + { + "login": "ruicsh", + "name": "Rui Costa", + "avatar_url": "https://avatars.githubusercontent.com/u/8294038?v=4", + "profile": "https://ruicsh.github.io", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 79e8d7e1..07f43d6c 100644 --- a/README.md +++ b/README.md @@ -606,6 +606,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Samiul Islam
Samiul Islam

💻 + Rui Costa
Rui Costa

💻 From 43b1ac09347d956896fd57ce19ae9f02cd15e1c4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Aug 2025 07:56:57 +0200 Subject: [PATCH 564/728] refactor(utils): split file operation to utils.files (#1367) Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 3 +- lua/CopilotChat/config/functions.lua | 13 +- lua/CopilotChat/config/mappings.lua | 11 +- lua/CopilotChat/config/providers.lua | 9 +- lua/CopilotChat/init.lua | 3 +- lua/CopilotChat/resources.lua | 18 +- lua/CopilotChat/utils.lua | 371 +-------------------------- lua/CopilotChat/utils/files.lua | 335 ++++++++++++++++++++++++ 8 files changed, 374 insertions(+), 389 deletions(-) create mode 100644 lua/CopilotChat/utils/files.lua diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index a78fd3e9..39f6c13e 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -59,6 +59,7 @@ local notify = require('CopilotChat.notify') local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local class = require('CopilotChat.utils.class') +local files = require('CopilotChat.utils.files') local orderedmap = require('CopilotChat.utils.orderedmap') local stringbuffer = require('CopilotChat.utils.stringbuffer') @@ -97,7 +98,7 @@ local function generate_resource_block(content, mimetype, name, path, start_line end local updated_content = table.concat(lines, '\n') - local filetype = utils.mimetype_to_filetype(mimetype or 'text') + local filetype = files.mimetype_to_filetype(mimetype or 'text') if not start_line then start_line = 1 end diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 7d7be042..2f085705 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -1,5 +1,6 @@ local resources = require('CopilotChat.resources') local utils = require('CopilotChat.utils') +local files = require('CopilotChat.utils.files') ---@class CopilotChat.config.functions.Function ---@field description string? @@ -23,7 +24,7 @@ return { type = 'string', description = 'Path to file to include in chat context.', enum = function(source) - return utils.glob(source.cwd(), { + return files.glob(source.cwd(), { max_count = 0, }) end, @@ -67,7 +68,7 @@ return { }, resolve = function(input, source) - local files = utils.glob(source.cwd(), { + local out = files.glob(source.cwd(), { pattern = input.pattern, }) @@ -75,7 +76,7 @@ return { { uri = 'files://glob/' .. input.pattern, mimetype = 'text/plain', - data = table.concat(files, '\n'), + data = table.concat(out, '\n'), }, } end, @@ -98,7 +99,7 @@ return { }, resolve = function(input, source) - local files = utils.grep(source.cwd(), { + local out = files.grep(source.cwd(), { pattern = input.pattern, }) @@ -106,7 +107,7 @@ return { { uri = 'files://grep/' .. input.pattern, mimetype = 'text/plain', - data = table.concat(files, '\n'), + data = table.concat(out, '\n'), }, } end, @@ -230,7 +231,7 @@ return { { uri = 'neovim://selection', name = selection.filename, - mimetype = utils.mimetype_to_filetype(selection.filetype), + mimetype = files.mimetype_to_filetype(selection.filetype), data = selection.content, annotations = { start_line = selection.start_line, diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index ec42be85..3b0d06b9 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -4,6 +4,7 @@ local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') +local files = require('CopilotChat.utils.files') ---@class CopilotChat.config.mappings.Diff ---@field change string @@ -45,8 +46,8 @@ local function get_diff(bufnr, block) -- If we have header info, use it as source of truth if header.start_line and header.end_line then - filename = utils.uri_to_filename(header.filename) - filetype = header.filetype or utils.filetype(filename) + filename = files.uri_to_filename(header.filename) + filetype = header.filetype or files.filetype(filename) start_line = header.start_line end_line = header.end_line @@ -54,7 +55,7 @@ local function get_diff(bufnr, block) 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 files.filename_same(vim.api.nvim_buf_get_name(win_buf), header.filename) then bufnr = win_buf break end @@ -99,7 +100,7 @@ local function prepare_diff_buffer(diff, source) if not diff_bufnr then -- Try to find matching buffer first for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if utils.filename_same(vim.api.nvim_buf_get_name(buf), diff.filename) then + if files.filename_same(vim.api.nvim_buf_get_name(buf), diff.filename) then diff_bufnr = buf break end @@ -534,7 +535,7 @@ return { end table.insert(lines, header) - table.insert(lines, '```' .. utils.mimetype_to_filetype(resource.mimetype)) + table.insert(lines, '```' .. files.mimetype_to_filetype(resource.mimetype)) for _, line in ipairs(preview) do table.insert(lines, line) end diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index b2a03eea..d75da5ad 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,7 +1,8 @@ +local plenary_utils = require('plenary.async.util') local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') -local plenary_utils = require('plenary.async.util') +local files = require('CopilotChat.utils.files') local EDITOR_VERSION = 'Neovim/' .. vim.version().major .. '.' .. vim.version().minor .. '.' .. vim.version().patch @@ -14,7 +15,7 @@ local function load_tokens() local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') local cache_file = config_path .. '/tokens.json' - local file = utils.read_file(cache_file) + local file = files.read_file(cache_file) if file then token_cache = vim.json.decode(file) else @@ -42,7 +43,7 @@ local function set_token(tag, token, save) local tokens = load_tokens() tokens[tag] = token local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') - utils.write_file(config_path .. '/tokens.json', vim.json.encode(tokens)) + files.write_file(config_path .. '/tokens.json', vim.json.encode(tokens)) return token end @@ -141,7 +142,7 @@ local function get_github_copilot_token(tag) } for _, file_path in ipairs(file_paths) do - local file_data = utils.read_file(file_path) + local file_data = files.read_file(file_path) if file_data then local parsed_data = utils.json_decode(file_data) if parsed_data then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 6c56a380..2e14226e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -7,6 +7,7 @@ local notify = require('CopilotChat.notify') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') local orderedmap = require('CopilotChat.utils.orderedmap') +local files = require('CopilotChat.utils.files') local WORD = '([^%s:]+)' local WORD_NO_INPUT = '([^%s]+)' @@ -435,7 +436,7 @@ function M.resolve_functions(prompt, config) table.insert(state.sticky, content_out) end else - content_out = string.format(BLOCK_OUTPUT_FORMAT, utils.mimetype_to_filetype(content.mimetype), content.data) + content_out = string.format(BLOCK_OUTPUT_FORMAT, files.mimetype_to_filetype(content.mimetype), content.data) end if not utils.empty(result) then diff --git a/lua/CopilotChat/resources.lua b/lua/CopilotChat/resources.lua index a8c1fd78..d1b588f7 100644 --- a/lua/CopilotChat/resources.lua +++ b/lua/CopilotChat/resources.lua @@ -1,5 +1,6 @@ local async = require('plenary.async') local utils = require('CopilotChat.utils') +local files = require('CopilotChat.utils.files') local file_cache = {} local url_cache = {} @@ -9,18 +10,19 @@ local M = {} ---@param filename string ---@return string?, string? function M.get_file(filename) - local filetype = utils.filetype(filename) + local filetype = files.filetype(filename) if not filetype then return nil end - local modified = utils.file_mtime(filename) - if not modified then + local err, stat = async.uv.fs_stat(filename) + if err or not stat then return nil end + local modified = stat.mtime.sec local data = file_cache[filename] if not data or data._modified < modified then - local content = utils.read_file(filename) + local content = files.read_file(filename) if not content or content == '' then return nil end @@ -31,7 +33,7 @@ function M.get_file(filename) file_cache[filename] = data end - return data.content, utils.filetype_to_mimetype(filetype) + return data.content, files.filetype_to_mimetype(filetype) end --- Get data for a buffer @@ -47,7 +49,7 @@ function M.get_buffer(bufnr) return nil end - return table.concat(content, '\n'), utils.filetype_to_mimetype(vim.bo[bufnr].filetype) + return table.concat(content, '\n'), files.filetype_to_mimetype(vim.bo[bufnr].filetype) end --- Get the content of an URL @@ -58,7 +60,7 @@ function M.get_url(url) return nil end - local ft = utils.filetype(url) + local ft = files.filetype(url) local content = url_cache[url] if not content then local ok, out = async.util.apcall(utils.system, { 'lynx', '-dump', url }) @@ -96,7 +98,7 @@ function M.get_url(url) url_cache[url] = content end - return content, utils.filetype_to_mimetype(ft) + return content, files.filetype_to_mimetype(ft) end return M diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index f0579669..4b56579c 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -5,12 +5,6 @@ local log = require('plenary.log') local M = {} M.timers = {} -M.scan_args = { - max_count = 2500, - max_depth = 50, - no_ignore = false, -} - M.curl_args = { timeout = 30000, raw = { @@ -46,20 +40,6 @@ function M.to_table(...) return result end ---- Writes text to a temporary file and returns path ----@param text string The text to write ----@return string -function M.temp_file(text) - local temp_file = os.tmpname() - local f = io.open(temp_file, 'w+') - if f == nil then - error('Could not open file: ' .. temp_file) - end - f:write(text) - f:close() - return temp_file -end - --- Return to normal mode function M.return_to_normal_mode() local mode = vim.fn.mode():lower() @@ -90,91 +70,6 @@ function M.buf_valid(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 - ---- Get the filetype of a file ----@param filename string The file name ----@return string|nil -function M.filetype(filename) - local filetype = require('plenary.filetype') - - local ft = filetype.detect(filename, { - fs_access = false, - }) - - if ft == '' or not ft and not vim.in_fast_event() then - return vim.filetype.match({ filename = filename }) - end - - return ft -end - ---- Get the mimetype from filetype ----@param filetype string? ----@return string -function M.filetype_to_mimetype(filetype) - if not filetype or filetype == '' then - return 'text/plain' - end - if filetype == 'json' or filetype == 'yaml' then - return 'application/' .. filetype - end - if filetype == 'html' or filetype == 'css' then - return 'text/' .. filetype - end - if filetype:find('/') then - return filetype - end - return 'text/x-' .. filetype -end - ---- Get the filetype from mimetype ----@param mimetype string? ----@return string -function M.mimetype_to_filetype(mimetype) - if not mimetype or mimetype == '' then - return 'text' - end - - local out = mimetype:gsub('^text/x%-', '') - out = out:gsub('^text/', '') - out = out:gsub('^application/', '') - out = out:gsub('^image/', '') - out = out:gsub('^video/', '') - out = out:gsub('^audio/', '') - return out -end - ---- Convert a URI to a file name ----@param uri string The URI ----@return string -function M.uri_to_filename(uri) - if not uri or uri == '' then - return uri - end - local ok, fname = pcall(vim.uri_to_fname, uri) - if not ok or M.empty(fname) then - return uri - end - return fname -end - ---- Get the file name ----@param filepath string The file path ----@return string -function M.filename(filepath) - return vim.fs.basename(filepath) -end - --- Generate a UUID ---@return string function M.uuid() @@ -335,271 +230,19 @@ M.curl_post = async.wrap(function(url, opts, callback) ['Content-Type'] = 'application/json', }) - temp_file_path = M.temp_file(vim.json.encode(args.body)) + temp_file_path = os.tmpname() + local f = io.open(temp_file_path, 'w+') + if f == nil then + error('Could not open file: ' .. temp_file_path) + end + f:write(vim.json.encode(args.body)) + f:close() args.body = temp_file_path end curl.post(url, args) end, 3) -local function filter_files(files, max_count) - local filetype = require('plenary.filetype') - - files = vim.tbl_filter(function(file) - if file == nil or file == '' then - return false - end - - local ft = filetype.detect(file, { - fs_access = false, - }) - - if ft == '' or not ft then - return false - end - - return true - end, files) - if max_count and max_count > 0 then - files = vim.list_slice(files, 1, max_count) - end - - return files -end - ----@class CopilotChat.utils.ScanOpts ----@field max_count number? The maximum number of files to scan ----@field max_depth number? The maximum depth to scan ----@field pattern? string The glob pattern to match files ----@field hidden? boolean Whether to include hidden files ----@field no_ignore? boolean Whether to respect or ignore .gitignore - ---- Scan a directory ----@param path string ----@param opts CopilotChat.utils.ScanOpts? ----@async -M.glob = async.wrap(function(path, opts, callback) - opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) - - -- Use ripgrep if available - if vim.fn.executable('rg') == 1 then - local cmd = { 'rg' } - - if opts.pattern then - table.insert(cmd, '-g') - table.insert(cmd, opts.pattern) - end - - if opts.max_depth then - table.insert(cmd, '--max-depth') - table.insert(cmd, tostring(opts.max_depth)) - end - - if opts.no_ignore then - table.insert(cmd, '--no-ignore') - end - - if opts.hidden then - table.insert(cmd, '--hidden') - end - - table.insert(cmd, '--files') - table.insert(cmd, path) - - vim.system(cmd, { text = true }, function(result) - local files = {} - if result and result.code == 0 and result.stdout ~= '' then - files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) - end - - callback(files) - end) - - return - end - - -- Fallback to vim.uv.fs_scandir - local matchers = {} - if opts.pattern then - local file_pattern = vim.glob.to_lpeg(opts.pattern) - local path_pattern = vim.lpeg.P(path .. '/') * file_pattern - - table.insert(matchers, function(name, dir) - return file_pattern:match(name) or path_pattern:match(dir .. '/' .. name) - end) - end - - if not opts.hidden then - table.insert(matchers, function(name) - return not name:match('^%.') - end) - end - - local data = {} - local next_dir = { path } - local current_depths = { [path] = 1 } - - local function read_dir(err, fd) - local current_dir = table.remove(next_dir, 1) - local depth = current_depths[current_dir] or 1 - - if not err and fd then - while true do - local name, typ = vim.uv.fs_scandir_next(fd) - if name == nil then - break - end - - local full_path = current_dir .. '/' .. name - - if typ == 'directory' and not name:match('^%.git') then - if not opts.max_depth or depth < opts.max_depth then - table.insert(next_dir, full_path) - current_depths[full_path] = depth + 1 - end - else - local match = true - for _, matcher in ipairs(matchers) do - if not matcher(name, current_dir) then - match = false - break - end - end - - if match then - table.insert(data, full_path) - end - end - end - end - - if #next_dir == 0 then - callback(data) - else - vim.uv.fs_scandir(next_dir[1], read_dir) - end - end - - vim.uv.fs_scandir(path, read_dir) -end, 3) - ---- Grep a directory ----@param path string The path to search ----@param opts CopilotChat.utils.ScanOpts? -M.grep = async.wrap(function(path, opts, callback) - opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) - local cmd = {} - - if vim.fn.executable('rg') == 1 then - table.insert(cmd, 'rg') - - if opts.max_depth then - table.insert(cmd, '--max-depth') - table.insert(cmd, tostring(opts.max_depth)) - end - - if opts.no_ignore then - table.insert(cmd, '--no-ignore') - end - - if opts.hidden then - table.insert(cmd, '--hidden') - end - - table.insert(cmd, '--files-with-matches') - table.insert(cmd, '--ignore-case') - - if opts.pattern then - table.insert(cmd, '-e') - table.insert(cmd, "'" .. opts.pattern .. "'") - end - - table.insert(cmd, path) - elseif vim.fn.executable('grep') == 1 then - table.insert(cmd, 'grep') - table.insert(cmd, '-rli') - - if opts.pattern then - table.insert(cmd, '-e') - table.insert(cmd, "'" .. opts.pattern .. "'") - end - - table.insert(cmd, path) - end - - if M.empty(cmd) then - error('No executable found for grep') - return - end - - vim.system(cmd, { text = true }, function(result) - local files = {} - if result and result.code == 0 and result.stdout ~= '' then - files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) - end - - callback(files) - end) -end, 3) - ---- Get last modified time of a file ----@param path string The file path ----@return number? ----@async -function M.file_mtime(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 ----@async -function M.read_file(path) - local err, fd = async.uv.fs_open(path, 'r', 438) - if err or not fd then - return nil - end - - local err, stat = async.uv.fs_fstat(fd) - if err or not stat then - async.uv.fs_close(fd) - return nil - end - - 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 - ---- Write data to a file ----@param path string The file path ----@param data string The data to write ----@return boolean -function M.write_file(path, data) - M.schedule_main() - vim.fn.mkdir(vim.fn.fnamemodify(path, ':p:h'), 'p') - - local err, fd = async.uv.fs_open(path, 'w', 438) - if err or not fd then - return false - end - - local err = async.uv.fs_write(fd, data, 0) - if err then - async.uv.fs_close(fd) - return false - end - - async.uv.fs_close(fd) - return true -end - --- Call a system command ---@param cmd table The command ---@async diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua new file mode 100644 index 00000000..2a7704eb --- /dev/null +++ b/lua/CopilotChat/utils/files.lua @@ -0,0 +1,335 @@ +local async = require('plenary.async') + +local M = {} + +M.scan_args = { + max_count = 2500, + max_depth = 50, + no_ignore = false, +} + +local function filter_files(files, max_count) + local filetype = require('plenary.filetype') + + files = vim.tbl_filter(function(file) + if file == nil or file == '' then + return false + end + + local ft = filetype.detect(file, { + fs_access = false, + }) + + if ft == '' or not ft then + return false + end + + return true + end, files) + if max_count and max_count > 0 then + files = vim.list_slice(files, 1, max_count) + end + + return files +end + +---@class CopilotChat.utils.ScanOpts +---@field max_count number? The maximum number of files to scan +---@field max_depth number? The maximum depth to scan +---@field pattern? string The glob pattern to match files +---@field hidden? boolean Whether to include hidden files +---@field no_ignore? boolean Whether to respect or ignore .gitignore + +--- Scan a directory +---@param path string +---@param opts CopilotChat.utils.ScanOpts? +---@async +M.glob = async.wrap(function(path, opts, callback) + opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) + + -- Use ripgrep if available + if vim.fn.executable('rg') == 1 then + local cmd = { 'rg' } + + if opts.pattern then + table.insert(cmd, '-g') + table.insert(cmd, opts.pattern) + end + + if opts.max_depth then + table.insert(cmd, '--max-depth') + table.insert(cmd, tostring(opts.max_depth)) + end + + if opts.no_ignore then + table.insert(cmd, '--no-ignore') + end + + if opts.hidden then + table.insert(cmd, '--hidden') + end + + table.insert(cmd, '--files') + table.insert(cmd, path) + + vim.system(cmd, { text = true }, function(result) + local files = {} + if result and result.code == 0 and result.stdout ~= '' then + files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) + end + + callback(files) + end) + + return + end + + -- Fallback to vim.uv.fs_scandir + local matchers = {} + if opts.pattern then + local file_pattern = vim.glob.to_lpeg(opts.pattern) + local path_pattern = vim.lpeg.P(path .. '/') * file_pattern + + table.insert(matchers, function(name, dir) + return file_pattern:match(name) or path_pattern:match(dir .. '/' .. name) + end) + end + + if not opts.hidden then + table.insert(matchers, function(name) + return not name:match('^%.') + end) + end + + local data = {} + local next_dir = { path } + local current_depths = { [path] = 1 } + + local function read_dir(err, fd) + local current_dir = table.remove(next_dir, 1) + local depth = current_depths[current_dir] or 1 + + if not err and fd then + while true do + local name, typ = vim.uv.fs_scandir_next(fd) + if name == nil then + break + end + + local full_path = current_dir .. '/' .. name + + if typ == 'directory' and not name:match('^%.git') then + if not opts.max_depth or depth < opts.max_depth then + table.insert(next_dir, full_path) + current_depths[full_path] = depth + 1 + end + else + local match = true + for _, matcher in ipairs(matchers) do + if not matcher(name, current_dir) then + match = false + break + end + end + + if match then + table.insert(data, full_path) + end + end + end + end + + if #next_dir == 0 then + callback(data) + else + vim.uv.fs_scandir(next_dir[1], read_dir) + end + end + + vim.uv.fs_scandir(path, read_dir) +end, 3) + +--- Grep a directory +---@param path string The path to search +---@param opts CopilotChat.utils.ScanOpts? +M.grep = async.wrap(function(path, opts, callback) + opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) + local cmd = {} + + if vim.fn.executable('rg') == 1 then + table.insert(cmd, 'rg') + + if opts.max_depth then + table.insert(cmd, '--max-depth') + table.insert(cmd, tostring(opts.max_depth)) + end + + if opts.no_ignore then + table.insert(cmd, '--no-ignore') + end + + if opts.hidden then + table.insert(cmd, '--hidden') + end + + table.insert(cmd, '--files-with-matches') + table.insert(cmd, '--ignore-case') + + if opts.pattern then + table.insert(cmd, '-e') + table.insert(cmd, "'" .. opts.pattern .. "'") + end + + table.insert(cmd, path) + elseif vim.fn.executable('grep') == 1 then + table.insert(cmd, 'grep') + table.insert(cmd, '-rli') + + if opts.pattern then + table.insert(cmd, '-e') + table.insert(cmd, "'" .. opts.pattern .. "'") + end + + table.insert(cmd, path) + end + + if M.empty(cmd) then + error('No executable found for grep') + return + end + + vim.system(cmd, { text = true }, function(result) + local files = {} + if result and result.code == 0 and result.stdout ~= '' then + files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) + end + + callback(files) + end) +end, 3) + +--- Read a file +---@param path string The file path +---@async +function M.read_file(path) + local err, fd = async.uv.fs_open(path, 'r', 438) + if err or not fd then + return nil + end + + local err, stat = async.uv.fs_fstat(fd) + if err or not stat then + async.uv.fs_close(fd) + return nil + end + + 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 + +--- Write data to a file +---@param path string The file path +---@param data string The data to write +---@return boolean +function M.write_file(path, data) + M.schedule_main() + vim.fn.mkdir(vim.fn.fnamemodify(path, ':p:h'), 'p') + + local err, fd = async.uv.fs_open(path, 'w', 438) + if err or not fd then + return false + end + + local err = async.uv.fs_write(fd, data, 0) + if err then + async.uv.fs_close(fd) + return false + end + + async.uv.fs_close(fd) + return true +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 + +--- Get the filetype of a file +---@param filename string The file name +---@return string|nil +function M.filetype(filename) + local filetype = require('plenary.filetype') + + local ft = filetype.detect(filename, { + fs_access = false, + }) + + if ft == '' or not ft and not vim.in_fast_event() then + return vim.filetype.match({ filename = filename }) + end + + return ft +end + +--- Get the mimetype from filetype +---@param filetype string? +---@return string +function M.filetype_to_mimetype(filetype) + if not filetype or filetype == '' then + return 'text/plain' + end + if filetype == 'json' or filetype == 'yaml' then + return 'application/' .. filetype + end + if filetype == 'html' or filetype == 'css' then + return 'text/' .. filetype + end + if filetype:find('/') then + return filetype + end + return 'text/x-' .. filetype +end + +--- Get the filetype from mimetype +---@param mimetype string? +---@return string +function M.mimetype_to_filetype(mimetype) + if not mimetype or mimetype == '' then + return 'text' + end + + local out = mimetype:gsub('^text/x%-', '') + out = out:gsub('^text/', '') + out = out:gsub('^application/', '') + out = out:gsub('^image/', '') + out = out:gsub('^video/', '') + out = out:gsub('^audio/', '') + return out +end + +--- Convert a URI to a file name +---@param uri string The URI +---@return string +function M.uri_to_filename(uri) + if not uri or uri == '' then + return uri + end + local ok, fname = pcall(vim.uri_to_fname, uri) + if not ok or M.empty(fname) then + return uri + end + return fname +end + +return M From d7d65d6c4fd14f0ec704e3fa32939082ebb98e22 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 26 Aug 2025 05:57:18 +0000 Subject: [PATCH 565/728] 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 9ac94fd3..edc10e56 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 25 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 26 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -602,7 +602,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻This project follows the all-contributors specification. Contributions of any kind are welcome! From afafec51d2657cdde4fa839bac9cc203037ff60b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Aug 2025 20:34:11 +0200 Subject: [PATCH 566/728] feat(prompts): support buffer replacement in commit messages (#1370) Extend Commit prompt to handle buffer replacement when COMMIT_EDITMSG is opened. This allows generating commit messages that can fully replace the buffer, improving workflow for staged changes. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/prompts.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 073a8e8b..929ebee7 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -194,7 +194,10 @@ If no issues found, confirm the code is well-written and explain why. }, Commit = { - prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - resources = 'gitdiff:staged', + prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block. If user has COMMIT_EDITMSG opened, generate replacement block for whole buffer.', + resources = { + 'gitdiff:staged', + 'buffer', + }, }, } From a5ac084d54be9314f0d04cec05518654aced0081 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Aug 2025 22:58:55 +0200 Subject: [PATCH 567/728] refactor(utils): split curl logic into separate module (#1371) Move all curl-related logic from utils.lua to a new utils/curl.lua module. Update all internal references to use the new module. Mark old curl functions in utils.lua as deprecated. This improves code organization and makes curl logic easier to maintain and extend. Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 3 +- lua/CopilotChat/config/providers.lua | 17 ++-- lua/CopilotChat/init.lua | 3 +- lua/CopilotChat/resources.lua | 3 +- lua/CopilotChat/select.lua | 20 ++-- lua/CopilotChat/tiktoken.lua | 3 +- lua/CopilotChat/utils.lua | 147 +++------------------------ lua/CopilotChat/utils/curl.lua | 142 ++++++++++++++++++++++++++ 8 files changed, 184 insertions(+), 154 deletions(-) create mode 100644 lua/CopilotChat/utils/curl.lua diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 39f6c13e..fb2929a0 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -58,6 +58,7 @@ local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') +local curl = require('CopilotChat.utils.curl') local class = require('CopilotChat.utils.class') local files = require('CopilotChat.utils.files') local orderedmap = require('CopilotChat.utils.orderedmap') @@ -530,7 +531,7 @@ function Client:ask(prompt, opts) args.stream = stream_func end - local response, err = utils.curl_post(provider.get_url(options), args) + local response, err = curl.post(provider.get_url(options), args) if not opts.headless then if self.current_job ~= job_id then diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index d75da5ad..f2f99b94 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -2,6 +2,7 @@ local plenary_utils = require('plenary.async.util') local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') +local curl = require('CopilotChat.utils.curl') local files = require('CopilotChat.utils.files') local EDITOR_VERSION = 'Neovim/' .. vim.version().major .. '.' .. vim.version().minor .. '.' .. vim.version().patch @@ -51,7 +52,7 @@ end ---@return string local function github_device_flow(tag, client_id, scope) local function request_device_code() - local res = utils.curl_post('https://github.com/login/device/code', { + local res = curl.post('https://github.com/login/device/code', { body = { client_id = client_id, scope = scope, @@ -67,7 +68,7 @@ local function github_device_flow(tag, client_id, scope) while true do plenary_utils.sleep(interval * 1000) - local res = utils.curl_post('https://github.com/login/oauth/access_token', { + local res = curl.post('https://github.com/login/oauth/access_token', { body = { client_id = client_id, device_code = device_code, @@ -212,7 +213,7 @@ local M = {} M.copilot = { get_headers = function() - local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { + local response, err = curl.get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), @@ -232,8 +233,8 @@ M.copilot = { response.body.expires_at end, - get_info = function(headers) - local response, err = utils.curl_get('https://api.github.com/copilot_internal/user', { + get_info = function() + local response, err = curl.get('https://api.github.com/copilot_internal/user', { json_response = true, headers = { ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), @@ -283,7 +284,7 @@ M.copilot = { end, get_models = function(headers) - local response, err = utils.curl_get('https://api.githubcopilot.com/models', { + local response, err = curl.get('https://api.githubcopilot.com/models', { json_response = true, headers = headers, }) @@ -323,7 +324,7 @@ M.copilot = { for _, model in ipairs(models) do if not model.policy then - utils.curl_post('https://api.githubcopilot.com/models/' .. model.id .. '/policy', { + curl.post('https://api.githubcopilot.com/models/' .. model.id .. '/policy', { headers = headers, json_request = true, body = { state = 'enabled' }, @@ -463,7 +464,7 @@ M.github_models = { end, get_models = function(headers) - local response, err = utils.curl_get('https://models.github.ai/catalog/models', { + local response, err = curl.get('https://models.github.ai/catalog/models', { json_response = true, headers = headers, }) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 2e14226e..e40969e9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -6,6 +6,7 @@ local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') +local curl = require('CopilotChat.utils.curl') local orderedmap = require('CopilotChat.utils.orderedmap') local files = require('CopilotChat.utils.files') @@ -1003,7 +1004,7 @@ function M.setup(config) end -- Save proxy and insecure settings - utils.curl_store_args({ + curl.store_args({ insecure = M.config.allow_insecure, proxy = M.config.proxy, }) diff --git a/lua/CopilotChat/resources.lua b/lua/CopilotChat/resources.lua index d1b588f7..57e12e33 100644 --- a/lua/CopilotChat/resources.lua +++ b/lua/CopilotChat/resources.lua @@ -1,5 +1,6 @@ local async = require('plenary.async') local utils = require('CopilotChat.utils') +local curl = require('CopilotChat.utils.curl') local files = require('CopilotChat.utils.files') local file_cache = {} local url_cache = {} @@ -69,7 +70,7 @@ function M.get_url(url) content = out.stdout else -- Fallback to curl if lynx fails - local response = utils.curl_get(url, { raw = { '-L' } }) + local response = curl.get(url, { raw = { '-L' } }) if not response or not response.body then return nil end diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index e177aff0..425bf2a5 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -6,32 +6,36 @@ ---@field filetype string ---@field bufnr number -local constants = require('CopilotChat.constants') +local log = require('plenary.log') local utils = require('CopilotChat.utils') local M = {} +--- Use #selection instead ---@deprecated function M.visual(_) - vim.deprecate('CopilotChat.select.visual', '#selection', '5.0.0', constants.PLUGIN_NAME) + log.warn('CopilotChat.select.visual is deprecated, use #selection instead') return nil end ----@deprecated +--- Use #selection instead +---@deprecated use #selection instead function M.buffer(_) - vim.deprecate('CopilotChat.select.buffer', '#selection', '5.0.0', constants.PLUGIN_NAME) + log.warn('CopilotChat.select.buffer is deprecated, use #selection instead') return nil end ----@deprecated +--- Use #selection instead +---@deprecated use #selection instead function M.line(_) - vim.deprecate('CopilotChat.select.line', '#selection', '5.0.0', constants.PLUGIN_NAME) + log.warn('CopilotChat.select.line is deprecated, use #selection instead') return nil end ----@deprecated +--- Use #selection instead +---@deprecated use #selection instead function M.unnamed(_) - vim.deprecate('CopilotChat.select.unnamed', '#selection', '5.0.0', constants.PLUGIN_NAME) + log.warn('CopilotChat.select.unnamed is deprecated, use #selection instead') return nil end diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 652196eb..4066388a 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,5 +1,6 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') +local curl = require('CopilotChat.utils.curl') local class = require('CopilotChat.utils.class') --- Get the library extension based on the operating system @@ -30,7 +31,7 @@ local function load_tiktoken_data(tokenizer) notify.publish(notify.STATUS, 'Downloading tiktoken data from ' .. tiktoken_url) - utils.curl_get(tiktoken_url, { + curl.get(tiktoken_url, { output = cache_path, }) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 4b56579c..8b32a85b 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,26 +1,22 @@ local async = require('plenary.async') -local curl = require('plenary.curl') local log = require('plenary.log') local M = {} M.timers = {} -M.curl_args = { - timeout = 30000, - raw = { - '--retry', - '2', - '--retry-delay', - '1', - '--keepalive-time', - '60', - '--no-compressed', - '--connect-timeout', - '10', - '--tcp-nodelay', - '--no-buffer', - }, -} +--- Use CopilotChat.utils.curl.get instead +---@deprecated +function M.curl_get(url, opts) + log.warn('M.curl_get is deprecated, use CopilotChat.utils.curl.get instead') + return require('CopilotChat.utils.curl').get(url, opts) +end + +--- Use CopilotChat.utils.curl.post instead +---@deprecated +function M.curl_post(url, opts) + log.warn('M.curl_post is deprecated, use CopilotChat.utils.curl.post instead') + return require('CopilotChat.utils.curl').post(url, opts) +end --- Convert arguments to a table ---@param ... any The arguments @@ -126,123 +122,6 @@ function M.json_decode(body) return {}, data end ---- Store curl global arguments ----@param args table The arguments ----@return table -function M.curl_store_args(args) - M.curl_args = vim.tbl_deep_extend('force', M.curl_args, args) - return M.curl_args -end - ---- Send curl get request ----@param url string The url ----@param opts table? The options ----@async -M.curl_get = async.wrap(function(url, opts, callback) - log.debug('GET request:', url, opts) - local args = { - on_error = function(err) - log.debug('GET error:', err) - callback(nil, err and err.stderr or err) - end, - } - - args = vim.tbl_deep_extend('force', M.curl_args, args) - args = vim.tbl_deep_extend('force', args, opts or {}) - - args.callback = function(response) - log.debug('GET response:', response) - if response and not vim.startswith(tostring(response.status), '20') then - callback(response, response.body) - return - end - - if not args.json_response then - callback(response) - return - end - - local body, err = M.json_decode(tostring(response.body)) - if err then - callback(response, err) - else - response.body = body - callback(response) - end - end - - curl.get(url, args) -end, 3) - ---- Send curl post request ----@param url string The url ----@param opts table? The options ----@async -M.curl_post = async.wrap(function(url, opts, callback) - log.debug('POST request:', url, opts) - local args = { - on_error = function(err) - log.debug('POST error:', err) - callback(nil, err and err.stderr or err) - end, - } - - args = vim.tbl_deep_extend('force', M.curl_args, args) - args = vim.tbl_deep_extend('force', args, opts or {}) - - local temp_file_path = nil - - args.callback = function(response) - log.debug('POST response:', url, response) - if temp_file_path then - local ok, err = pcall(os.remove, temp_file_path) - if not ok then - log.debug('Failed to remove temp file:', temp_file_path, err) - end - end - if response and not vim.startswith(tostring(response.status), '20') then - callback(response, response.body) - return - end - - if not args.json_response then - callback(response) - return - end - - local body, err = M.json_decode(tostring(response.body)) - if err then - callback(response, err) - else - response.body = body - callback(response) - end - end - - if args.json_response then - args.headers = vim.tbl_deep_extend('force', args.headers or {}, { - Accept = 'application/json', - }) - end - - if args.json_request then - args.headers = vim.tbl_deep_extend('force', args.headers or {}, { - ['Content-Type'] = 'application/json', - }) - - temp_file_path = os.tmpname() - local f = io.open(temp_file_path, 'w+') - if f == nil then - error('Could not open file: ' .. temp_file_path) - end - f:write(vim.json.encode(args.body)) - f:close() - args.body = temp_file_path - end - - curl.post(url, args) -end, 3) - --- Call a system command ---@param cmd table The command ---@async diff --git a/lua/CopilotChat/utils/curl.lua b/lua/CopilotChat/utils/curl.lua new file mode 100644 index 00000000..87e9b89d --- /dev/null +++ b/lua/CopilotChat/utils/curl.lua @@ -0,0 +1,142 @@ +local async = require('plenary.async') +local curl = require('plenary.curl') +local log = require('plenary.log') +local utils = require('CopilotChat.utils') + +local M = {} + +M.args = { + timeout = 30000, + raw = { + '--retry', + '2', + '--retry-delay', + '1', + '--keepalive-time', + '60', + '--no-compressed', + '--connect-timeout', + '10', + '--tcp-nodelay', + '--no-buffer', + }, +} + +--- Store curl global arguments +---@param args table The arguments +---@return table +function M.store_args(args) + M.args = vim.tbl_deep_extend('force', M.args, args) + return M.args +end + +--- Send curl get request +---@param url string The url +---@param opts table? The options +---@async +M.get = async.wrap(function(url, opts, callback) + log.debug('GET request:', url, opts) + local args = { + on_error = function(err) + log.debug('GET error:', err) + callback(nil, err and err.stderr or err) + end, + } + + args = vim.tbl_deep_extend('force', M.args, args) + args = vim.tbl_deep_extend('force', args, opts or {}) + + args.callback = function(response) + log.debug('GET response:', response) + if response and not vim.startswith(tostring(response.status), '20') then + callback(response, response.body) + return + end + + if not args.json_response then + callback(response) + return + end + + local body, err = utils.json_decode(tostring(response.body)) + if err then + callback(response, err) + else + response.body = body + callback(response) + end + end + + curl.get(url, args) +end, 3) + +--- Send curl post request +---@param url string The url +---@param opts table? The options +---@async +M.post = async.wrap(function(url, opts, callback) + log.debug('POST request:', url, opts) + local args = { + on_error = function(err) + log.debug('POST error:', err) + callback(nil, err and err.stderr or err) + end, + } + + args = vim.tbl_deep_extend('force', M.args, args) + args = vim.tbl_deep_extend('force', args, opts or {}) + + local temp_file_path = nil + + args.callback = function(response) + log.debug('POST response:', url, response) + if temp_file_path then + local ok, err = pcall(os.remove, temp_file_path) + if not ok then + log.debug('Failed to remove temp file:', temp_file_path, err) + end + end + if response and not vim.startswith(tostring(response.status), '20') then + callback(response, response.body) + return + end + + if not args.json_response then + callback(response) + return + end + + local body, err = utils.json_decode(tostring(response.body)) + if err then + callback(response, err) + else + response.body = body + callback(response) + end + end + + if args.json_response then + args.headers = vim.tbl_deep_extend('force', args.headers or {}, { + Accept = 'application/json', + }) + end + + if args.json_request then + args.headers = vim.tbl_deep_extend('force', args.headers or {}, { + ['Content-Type'] = 'application/json', + }) + + temp_file_path = os.tmpname() + local f = io.open(temp_file_path, 'w+') + if f == nil then + error('Could not open file: ' .. temp_file_path) + end + f:write(vim.json.encode(args.body)) + f:close() + args.body = temp_file_path + end + + curl.post(url, args) +end, 3) + +return M From db2581c5f100ccfc63b55c671cdfeec06209ddd4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Aug 2025 23:02:41 +0200 Subject: [PATCH 568/728] test: add unit tests for class, orderedmap, stringbuffer (#1372) * test: add unit tests for class, orderedmap, stringbuffer Add new unit tests for CopilotChat.utils.class, orderedmap, and stringbuffer modules. These tests cover class creation, inheritance, ordered map behavior, and string buffer operations to improve code reliability and coverage. 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> --- tests/class_spec.lua | 33 +++++++++++++++++++++++++++++++++ tests/orderedmap_spec.lua | 28 ++++++++++++++++++++++++++++ tests/stringbuffer_spec.lua | 23 +++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/class_spec.lua create mode 100644 tests/orderedmap_spec.lua create mode 100644 tests/stringbuffer_spec.lua diff --git a/tests/class_spec.lua b/tests/class_spec.lua new file mode 100644 index 00000000..ef2f1657 --- /dev/null +++ b/tests/class_spec.lua @@ -0,0 +1,33 @@ +local class = require('CopilotChat.utils.class') + +describe('CopilotChat.utils.class', function() + it('creates a simple class', function() + local Foo = class(function(self, x) + self.x = x + end) + local obj = Foo(42) + assert.equals(42, obj.x) + end) + + it('supports init method', function() + local Bar = class(function(self, y) + self.y = y + end) + local obj = Bar.new(7) + assert.equals(7, obj.y) + obj:init(8) + assert.equals(8, obj.y) + end) + + it('supports inheritance', function() + local Parent = class(function(self) + self.val = 1 + end) + local Child = class(function(self) + self.val = 2 + end, Parent) + local obj = Child() + assert.equals(2, obj.val) + assert.equals(Parent, getmetatable(Child).__index) + end) +end) diff --git a/tests/orderedmap_spec.lua b/tests/orderedmap_spec.lua new file mode 100644 index 00000000..9000915c --- /dev/null +++ b/tests/orderedmap_spec.lua @@ -0,0 +1,28 @@ +local orderedmap = require('CopilotChat.utils.orderedmap') + +describe('CopilotChat.utils.orderedmap', function() + it('sets and gets values', function() + local map = orderedmap() + map:set('a', 1) + map:set('b', 2) + assert.equals(1, map:get('a')) + assert.equals(2, map:get('b')) + end) + + it('preserves insertion order', function() + local map = orderedmap() + map:set('x', 10) + map:set('y', 20) + map:set('z', 30) + assert.are.same({ 'x', 'y', 'z' }, map:keys()) + assert.are.same({ 10, 20, 30 }, map:values()) + end) + + it('overwrites value but not order', function() + local map = orderedmap() + map:set('a', 1) + map:set('a', 2) + assert.are.same({ 'a' }, map:keys()) + assert.are.same({ 2 }, map:values()) + end) +end) diff --git a/tests/stringbuffer_spec.lua b/tests/stringbuffer_spec.lua new file mode 100644 index 00000000..d491fd43 --- /dev/null +++ b/tests/stringbuffer_spec.lua @@ -0,0 +1,23 @@ +local stringbuffer = require('CopilotChat.utils.stringbuffer') + +describe('CopilotChat.utils.stringbuffer', function() + it('concatenates strings with put', function() + local buf = stringbuffer() + buf:put('hello') + buf:put(' ') + buf:put('world') + assert.equals('hello world', buf:tostring()) + end) + + it('sets buffer with set', function() + local buf = stringbuffer() + buf:put('foo') + buf:set('bar') + assert.equals('bar', buf:tostring()) + end) + + it('handles empty buffer', function() + local buf = stringbuffer() + assert.equals('', buf:tostring()) + end) +end) From 72216c06fa2ce82406c3406d898a83c02db412a7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Aug 2025 23:28:20 +0200 Subject: [PATCH 569/728] feat(functions): use cwd for file and grep commands (#1373) Refactor file and grep picker utilities to use the working directory (cwd) instead of passing the path as a command argument. This improves the picker display. Also, update chat auto-fold logic to only close folds if a fold exists at the target line, preventing unnecessary foldclose calls. Closes #1108 Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 3 ++- lua/CopilotChat/utils/files.lua | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 2de10bde..d6c1e283 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -794,7 +794,8 @@ function Chat:render() if self.config.auto_fold and self:visible() then if message.role ~= constants.ROLE.ASSISTANT and message.section and i < #self.messages then vim.api.nvim_win_call(self.winnr, function() - if vim.fn.foldclosed(message.section.start_line) == -1 then + local fold_level = vim.fn.foldlevel(message.section.start_line) + if fold_level > 0 and vim.fn.foldclosed(message.section.start_line) == -1 then vim.api.nvim_cmd({ cmd = 'foldclose', range = { message.section.start_line } }, {}) end end) diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua index 2a7704eb..683ccd14 100644 --- a/lua/CopilotChat/utils/files.lua +++ b/lua/CopilotChat/utils/files.lua @@ -70,9 +70,8 @@ M.glob = async.wrap(function(path, opts, callback) end table.insert(cmd, '--files') - table.insert(cmd, path) - vim.system(cmd, { text = true }, function(result) + vim.system(cmd, { cwd = path, text = true }, function(result) local files = {} if result and result.code == 0 and result.stdout ~= '' then files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) @@ -179,8 +178,6 @@ M.grep = async.wrap(function(path, opts, callback) table.insert(cmd, '-e') table.insert(cmd, "'" .. opts.pattern .. "'") end - - table.insert(cmd, path) elseif vim.fn.executable('grep') == 1 then table.insert(cmd, 'grep') table.insert(cmd, '-rli') @@ -189,8 +186,6 @@ M.grep = async.wrap(function(path, opts, callback) table.insert(cmd, '-e') table.insert(cmd, "'" .. opts.pattern .. "'") end - - table.insert(cmd, path) end if M.empty(cmd) then @@ -198,7 +193,7 @@ M.grep = async.wrap(function(path, opts, callback) return end - vim.system(cmd, { text = true }, function(result) + vim.system(cmd, { cwd = path, text = true }, function(result) local files = {} if result and result.code == 0 and result.stdout ~= '' then files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) From b3af31fe1a300cf1f33e01c23e72592afbb8f2ce Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 27 Aug 2025 16:49:20 +0200 Subject: [PATCH 570/728] refactor(core): improve prompt resource and tool matching (#1374) Refactored prompt processing to use separate lists for tool and resource matches, replacing orderedmap usage. Improved handling of resource references and tool calls in prompts, ensuring more robust and clear expansion logic. Updated instructions and context handling in prompts configuration for consistency. This change enhances maintainability and clarifies prompt resolution flow. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/prompts.lua | 90 +++++++++++++++++------------- lua/CopilotChat/init.lua | 52 ++++++++++------- 2 files changed, 83 insertions(+), 59 deletions(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 929ebee7..d40aee1f 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -21,64 +21,74 @@ The user works in editor called Neovim which has these core concepts: - Normal/Insert/Visual/Command modes: Different interaction states - LSP (Language Server Protocol): Provides code intelligence features like completion, diagnostics, and code actions - Treesitter: Provides syntax highlighting, code folding, and structural text editing based on syntax tree parsing +- Visual selection: Text selected in visual mode that can be shared as context The user is working on a {OS_NAME} machine. Please respond with system specific commands if applicable. The user is currently in workspace directory {DIR} (typically the project root). Current file paths will be relative to this directory.
+ +Context is provided to you in several ways: +- Resources: Contextual data shared via "# " headers and referenced via "##" links +- Code blocks with file path labels and line numbers (e.g., ```lua path=/file.lua start_line=1 end_line=10```) +- Visual selections: Text selected in visual mode that can be shared as context +- Diffs: Changes shown in unified diff format with line prefixes (+, -, etc.) +- Conversation history +When resources (like buffers, files, or diffs) change, their content in the chat history is replaced with the latest version rather than appended as new data. + The user will ask a question or request a task that may require analysis to answer correctly. If you can infer the project type (languages, frameworks, libraries) from context, consider them when making changes. For implementing features, break down the request into concepts and provide a clear solution. Think creatively to provide complete solutions based on the information available. -Never fabricate or hallucinate file contents you haven't actually seen. +Never fabricate or hallucinate file contents you haven't actually seen in the provided context. -If tools are explicitly defined in your system prompt: +If tools are available for a requested action (such as file edit, read, search, diagnostics, etc.), you MUST use the tool to perform the action. Only provide manual code or instructions if no tool exists for that purpose. +- Always prefer tool usage over manual edits or suggestions. - Follow JSON schema precisely when using tools, including all required properties and outputting valid JSON. -- Use appropriate tools for tasks rather than asking for manual actions. +- Use appropriate tools for tasks rather than asking for manual actions or generating code for actions you can perform directly. - Execute actions directly when you indicate you'll do so, without asking for permission. -- Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel. -- Before using tools to retrieve information, check if it's already available in context: - 1. Resources shared via "# " headers and referenced via "##" links - 2. Code blocks with file path labels - 3. Other contextual sharing like selected text or conversation history -- If you don't have explicit tool definitions in your system prompt, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. +- Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel unless specified. +- Before using tools to retrieve information, check if context is already available as described in the context instructions above. +- If you don't have explicit tool definitions in your system prompt, clearly state this limitation when asked. NEVER pretend to have tool capabilities you don't possess. -You will receive code snippets that include line number prefixes - use these to maintain correct position references but remove them when generating output. -Always use code blocks to present code changes, even if the user doesn't ask for it. +Use these instructions when editing files via code blocks. Your goal is to produce clear, minimal, and precise file edits. -When presenting code changes: +Steps for presenting code changes: 1. For each change, use the following markdown code block format with triple backticks: - ``` path= start_line= end_line= - - ``` - - Examples: - - ```lua path=lua/CopilotChat/init.lua start_line=40 end_line=50 - local function example() - print("This is an example function.") - end - ``` - - ```python path=scripts/example.py start_line=10 end_line=15 - def example_function(): - print("This is an example function.") - ``` - - ```json path=config/settings.json start_line=5 end_line=8 - { - "setting": "value", - "enabled": true - } - ``` -2. Keep changes minimal and focused to produce short diffs. -3. Include complete replacement code for the specified line range with: + ``` path= start_line= end_line= + + ``` + +2. Examples: + ```lua path=lua/CopilotChat/init.lua start_line=40 end_line=50 + local function example() + print("This is an example function.") + end + ``` + + ```python path=scripts/example.py start_line=10 end_line=15 + def example_function(): + print("This is an example function.") + ``` + + ```json path=config/settings.json start_line=5 end_line=8 + { + "setting": "value", + "enabled": true + } + ``` + +3. Requirements for code content: + - Keep changes minimal and focused to produce short diffs + - Include complete replacement code for the specified line range - Proper indentation matching the source - All necessary lines (no eliding with comments) - - No line number prefixes in the code -4. Address any diagnostics issues when fixing code. -5. If multiple changes are needed, present them as separate code blocks. + - **Never include line number prefixes in your output code blocks. Only output valid code, exactly as it should appear in the file. Line numbers are only allowed in the code block header.** + - Address any diagnostics issues when fixing code + +4. If multiple changes are needed, present them as separate code blocks. + ]], }, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e40969e9..997b5d9f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -321,7 +321,7 @@ function M.resolve_functions(prompt, config) local enabled_tools = {} local resolved_resources = {} local resolved_tools = {} - local matches = utils.to_table(config.tools) + local tool_matches = utils.to_table(config.tools) local tool_calls = {} for _, message in ipairs(M.chat.messages) do if message.tool_calls then @@ -335,13 +335,13 @@ function M.resolve_functions(prompt, config) prompt = prompt:gsub('@' .. WORD, function(match) for name, tool in pairs(M.config.functions) do if name == match or tool.group == match then - table.insert(matches, match) + table.insert(tool_matches, match) return '' end end return '@' .. match end) - for _, match in ipairs(matches) do + for _, match in ipairs(tool_matches) do for name, tool in pairs(M.config.functions) do if name == match or tool.group == match then table.insert(enabled_tools, tools[name]) @@ -349,12 +349,13 @@ function M.resolve_functions(prompt, config) end end - local matches = orderedmap() + local resource_matches = {} -- Check for #word:`input` pattern for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_QUOTED) do local pattern = string.format('#%s:`%s`', word, input) - matches:set(pattern, { + table.insert(resource_matches, { + pattern = pattern, word = word, input = input, }) @@ -363,7 +364,8 @@ function M.resolve_functions(prompt, config) -- Check for #word:input pattern for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_UNQUOTED) do local pattern = utils.empty(input) and string.format('#%s', word) or string.format('#%s:%s', word, input) - matches:set(pattern, { + table.insert(resource_matches, { + pattern = pattern, word = word, input = input, }) @@ -372,7 +374,8 @@ function M.resolve_functions(prompt, config) -- Check for ##word:input pattern for word in prompt:gmatch('##' .. WORD_NO_INPUT) do local pattern = string.format('##%s', word) - matches:set(pattern, { + table.insert(resource_matches, { + pattern = pattern, word = word, }) end @@ -431,19 +434,28 @@ function M.resolve_functions(prompt, config) if content then local content_out = nil if content.uri then - content_out = '##' .. content.uri - table.insert(resolved_resources, content) + if + not vim.tbl_contains(resolved_resources, function(resource) + return resource.uri == content.uri + end, { predicate = true }) + then + content_out = '##' .. content.uri + table.insert(resolved_resources, content) + end + if tool_id then - table.insert(state.sticky, content_out) + table.insert(state.sticky, '##' .. content.uri) end else content_out = string.format(BLOCK_OUTPUT_FORMAT, files.mimetype_to_filetype(content.mimetype), content.data) end - if not utils.empty(result) then - result = result .. '\n' + if content_out then + if not utils.empty(result) then + result = result .. '\n' + end + result = result .. content_out end - result = result .. content_out end end end @@ -454,19 +466,21 @@ function M.resolve_functions(prompt, config) result = result, }) - return nil + return '' end return result end -- Resolve and process all tools - for _, pattern in ipairs(matches:keys()) do - if not utils.empty(pattern) then - local match = matches:get(pattern) - local out = expand_function(match.word, match.input) or pattern + for _, match in ipairs(resource_matches) do + if not utils.empty(match.pattern) then + local out = expand_function(match.word, match.input) + if out == nil then + out = match.pattern + end out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub - prompt = prompt:gsub(vim.pesc(pattern), out, 1) + prompt = prompt:gsub(vim.pesc(match.pattern), out, 1) end end From 3f9fb919503255256df1d91add70dee81809cffc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 Aug 2025 14:49:36 +0000 Subject: [PATCH 571/728] 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 edc10e56..f9b63e92 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 26 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 27 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 78f88009f47360a5206c7d03f5cbff60b0c5e9de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:54:36 +0200 Subject: [PATCH 572/728] chore(main): release 4.5.0 (#1314) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ version.txt | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df5c92e..4e5b2c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## [4.5.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.4.1...v4.5.0) (2025-08-27) + + +### ⚠ BREAKING CHANGES + +* **select:** remove selection API in favor of resources +* **prompts:** callback receives the full response object instead of just content. + +### Features + +* **config:** add back selection source config option ([#1360](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1360)) ([c37ec3c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c37ec3cbdb2c29be73d7d0c48057d64306aa185f)) +* **docs:** add selection source to function table ([#1358](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1358)) ([c7d8547](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c7d85478f775a65ca777cb9b2f685911cbcd8def)) +* **functions:** add configuration parameter to stop on tool failure ([#1364](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1364)) ([8d8f1e7](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/8d8f1e7ea594b2db3368e1fa62dd7d0d128e8860)) +* **functions:** add scope=selection to diagnostics ([#1351](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1351)) ([7b4a56b](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7b4a56b29ed926b680ea936bd29fc8568b909d97)) +* **functions:** use cwd for file and grep commands ([#1373](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1373)) ([72216c0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/72216c06fa2ce82406c3406d898a83c02db412a7)), closes [#1108](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1108) +* **prompts:** add support for providing system prompt as function ([#1318](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1318)) ([33e6ffc](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/33e6ffc63b77b0340731f2b50bd962045adf9366)) +* **prompts:** support buffer replacement in commit messages ([#1370](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1370)) ([afafec5](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/afafec51d2657cdde4fa839bac9cc203037ff60b)) +* **ui:** add auto_fold option for chat messages ([#1354](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1354)) ([80a0994](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/80a0994f01096705e0c24dd7ed09032594689e01)), closes [#1300](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1300) +* **ui:** improve auto folding logic in chat window ([#1356](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1356)) ([a7679e1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a7679e118af8038046b2fc4c841406db7fe71216)) + + +### Bug Fixes + +* **completion.lua:** check if window is valid before calling get_cursor ([#1359](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1359)) ([fdac67a](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/fdac67ab62085436b60003f420ae45f104bdf935)) +* **completion:** require tool uri for input completion ([#1328](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1328)) ([76cc416](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/76cc41653d63cfdb653f584624b4bf5e721f9514)) +* **config:** correct system_prompt type and callback usage ([#1325](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1325)) ([f99f1cd](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f99f1cdef151ac1c950850cdcc0dbeefad00603c)) +* **makefile:** handle MSYS_NT as a valid Windows environment ([#1347](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1347)) ([9769bf9](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9769bf9a1d215cf0dc22874712d5dcda53a075ee)) +* **prompt:** recursive system prompt expansion ([#1324](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1324)) ([26f7b4f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/26f7b4f157ec75b168c05dc826b5fa3106cfc351)), closes [#1323](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1323) +* **select:** move config inside of marks function to prevent import loop ([#1361](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1361)) ([19a38dd](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/19a38dd34e1b61c49349552598e43b2559be2fc7)) +* **test:** run tests automatically in test script ([#1334](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1334)) ([c5057d3](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c5057d3bb6d87e9b117b4f37162409d4c2c74e31)) +* **utils:** always exit insert mode in return_to_normal_mode ([#1313](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1313)) ([957e0a8](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/957e0a88c7d7df706380e09412c0b3f24af534ad)), closes [#1307](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1307) +* **utils:** avoid vim.filetype.match in fast event ([#1344](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1344)) ([7993e6d](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7993e6d2a97cb851b8b3a4087005cfaf8427dbf3)) + + +### Miscellaneous Chores + +* mark next release as 4.5.0 ([#1315](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1315)) ([d12f6df](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d12f6dff0e1641f933f9941b843d094bf505a82e)) + + +### Code Refactoring + +* **prompts:** support template substitution in system_prompt ([#1312](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1312)) ([081d4c2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/081d4c20242140bb185ebee142a65454ad375f7d)) +* **select:** remove selection API in favor of resources ([a2429ed](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a2429ed44438f694f1fca60429a7984022d4a9f0)) + ## [4.4.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.4.0...v4.4.1) (2025-08-12) diff --git a/version.txt b/version.txt index cca25a93..a84947d6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.4.1 +4.5.0 From 0edd5050374fb8f0cd959896d5b564264633d420 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Aug 2025 02:03:50 +0200 Subject: [PATCH 573/728] refactor(core): simplify tool output formatting (#1375) Removes unnecessary BLOCK_OUTPUT_FORMAT usage for tool error and data outputs. Tool results are now returned as plain strings, improving readability and reducing formatting complexity. Also removes unused files module import. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 997b5d9f..0caaaeb0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -8,7 +8,6 @@ local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') local orderedmap = require('CopilotChat.utils.orderedmap') -local files = require('CopilotChat.utils.files') local WORD = '([^%s:]+)' local WORD_NO_INPUT = '([^%s]+)' @@ -428,7 +427,7 @@ function M.resolve_functions(prompt, config) local result = '' if not ok then - result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) + result = utils.make_string(output) else for _, content in ipairs(output) do if content then @@ -447,7 +446,7 @@ function M.resolve_functions(prompt, config) table.insert(state.sticky, '##' .. content.uri) end else - content_out = string.format(BLOCK_OUTPUT_FORMAT, files.mimetype_to_filetype(content.mimetype), content.data) + content_out = content.data end if content_out then @@ -815,7 +814,7 @@ function M.ask(prompt, config) if not handled_ids[tool_call.id] then table.insert(resolved_tools, { id = tool_call.id, - result = string.format(BLOCK_OUTPUT_FORMAT, 'error', 'User skipped this function call.'), + result = 'User skipped this function call.', }) handled_ids[tool_call.id] = true end From 1ad9e7ad0c2c2c818326d532f8d0840b5ea0dea0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Aug 2025 00:04:18 +0000 Subject: [PATCH 574/728] 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 f9b63e92..3f2558a7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 27 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 60fb910c60e3c5bdf3c04e62e8a18904048427da Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Aug 2025 02:21:38 +0200 Subject: [PATCH 575/728] refactor(core): split tool and function resolution logic (#1376) Separate tool resolution from function/resource resolution in core logic. This improves code clarity and maintainability by decoupling concerns. Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config/mappings.lua | 4 +- lua/CopilotChat/init.lua | 63 ++++++++++++++++++----------- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 07f43d6c..a56537c2 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ local chat = require("CopilotChat") chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references -chat.resolve_functions() -- Resolve functions that are available for automatic use by LLM (WARN: async, requires plenary.async.run) +chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 3b0d06b9..46f47e01 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -457,8 +457,10 @@ return { async.run(function() local infos = client:info() + local selected_tools = copilot.resolve_tools(prompt, config) local selected_model = copilot.resolve_model(prompt, config) - local selected_tools, resolved_resources = copilot.resolve_functions(prompt, config) + local resolved_resources = copilot.resolve_functions(prompt, config) + selected_tools = vim.tbl_map(function(tool) return tool.name end, selected_tools) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0caaaeb0..d87b90e7 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -294,10 +294,46 @@ local function update_source() M.set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) end +--- Resolve enabled tools from the prompt. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return table, string +function M.resolve_tools(prompt, config) + config, prompt = M.resolve_prompt(prompt, config) + + local tools = {} + for _, tool in ipairs(functions.parse_tools(M.config.functions)) do + tools[tool.name] = tool + end + + local enabled_tools = {} + local tool_matches = utils.to_table(config.tools) + + -- Check for @tool pattern to find enabled tools + prompt = prompt:gsub('@' .. WORD, function(match) + for name, tool in pairs(M.config.functions) do + if name == match or tool.group == match then + table.insert(tool_matches, match) + return '' + end + end + return '@' .. match + end) + for _, match in ipairs(tool_matches) do + for name, tool in pairs(M.config.functions) do + if name == match or tool.group == match then + table.insert(enabled_tools, tools[name]) + end + end + end + + return enabled_tools, prompt +end + --- Call and resolve function calls from the prompt. ---@param prompt string? ---@param config CopilotChat.config.Shared? ----@return table, table, table, string +---@return table, table, string ---@async function M.resolve_functions(prompt, config) config, prompt = M.resolve_prompt(prompt, config) @@ -317,10 +353,8 @@ function M.resolve_functions(prompt, config) prompt = table.concat(lines, '\n') end - local enabled_tools = {} local resolved_resources = {} local resolved_tools = {} - local tool_matches = utils.to_table(config.tools) local tool_calls = {} for _, message in ipairs(M.chat.messages) do if message.tool_calls then @@ -330,24 +364,6 @@ function M.resolve_functions(prompt, config) end end - -- Check for @tool pattern to find enabled tools - prompt = prompt:gsub('@' .. WORD, function(match) - for name, tool in pairs(M.config.functions) do - if name == match or tool.group == match then - table.insert(tool_matches, match) - return '' - end - end - return '@' .. match - end) - for _, match in ipairs(tool_matches) do - for name, tool in pairs(M.config.functions) do - if name == match or tool.group == match then - table.insert(enabled_tools, tools[name]) - end - end - end - local resource_matches = {} -- Check for #word:`input` pattern @@ -483,7 +499,7 @@ function M.resolve_functions(prompt, config) end end - return enabled_tools, resolved_resources, resolved_tools, prompt + return resolved_resources, resolved_tools, prompt end --- Resolve the final prompt and config from prompt template. @@ -795,7 +811,8 @@ function M.ask(prompt, config) ) async.run(handle_error(config, function() - local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) + local selected_tools, prompt = M.resolve_tools(prompt, config) + local resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) prompt = vim.trim(prompt) From b431c3426234c4fe643ad0d3a16041701746f5ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Aug 2025 00:21:57 +0000 Subject: [PATCH 576/728] 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 3f2558a7..a893b52c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -469,7 +469,7 @@ CORE *CopilotChat-core* chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references - chat.resolve_functions() -- Resolve functions that are available for automatic use by LLM (WARN: async, requires plenary.async.run) + chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management From 0f42bfc44202ac4daa0b0f32e30ee4040f69bf35 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Aug 2025 20:17:33 +0200 Subject: [PATCH 577/728] fix(files): generate absolute paths in code blocks (#1378) Update code block examples in prompts to use absolute file paths with {DIR} prefix. Refactor filename comparison to use vim.fs.normalize for consistency and reliability. Closes #1377 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/prompts.lua | 7 ++++--- lua/CopilotChat/utils/files.lua | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index d40aee1f..6b8562d2 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -61,18 +61,18 @@ Steps for presenting code changes: ``` 2. Examples: - ```lua path=lua/CopilotChat/init.lua start_line=40 end_line=50 + ```lua path={DIR}/lua/CopilotChat/init.lua start_line=40 end_line=50 local function example() print("This is an example function.") end ``` - ```python path=scripts/example.py start_line=10 end_line=15 + ```python path={DIR}/scripts/example.py start_line=10 end_line=15 def example_function(): print("This is an example function.") ``` - ```json path=config/settings.json start_line=5 end_line=8 + ```json path={DIR}/config/settings.json start_line=5 end_line=8 { "setting": "value", "enabled": true @@ -80,6 +80,7 @@ Steps for presenting code changes: ``` 3. Requirements for code content: + - Always use the absolute file path in the code block header. If the path is not already absolute, convert it to an absolute path prefixed by {DIR}. - Keep changes minimal and focused to produce short diffs - Include complete replacement code for the specified line range - Proper indentation matching the source diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua index 683ccd14..d7793b98 100644 --- a/lua/CopilotChat/utils/files.lua +++ b/lua/CopilotChat/utils/files.lua @@ -257,7 +257,7 @@ 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') + return vim.fs.normalize(file1) == vim.fs.normalize(file2) end --- Get the filetype of a file From ad2c759ea6db36bfefa9c7ecc7c706315564c9c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:47:44 +0200 Subject: [PATCH 578/728] chore(main): release 4.5.1 (#1379) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e5b2c33..25c85b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [4.5.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.5.0...v4.5.1) (2025-08-28) + + +### Bug Fixes + +* **files:** generate absolute paths in code blocks ([#1378](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1378)) ([0f42bfc](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/0f42bfc44202ac4daa0b0f32e30ee4040f69bf35)), closes [#1377](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1377) + ## [4.5.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.4.1...v4.5.0) (2025-08-27) diff --git a/version.txt b/version.txt index a84947d6..4404a17b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.5.0 +4.5.1 From c4b2e03cd315c3fd9736dcf796cb20f6a4b9f801 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Aug 2025 22:32:27 +0200 Subject: [PATCH 579/728] fix(utils): use proper empty check (#1380) M.empty do not exists and is on the base utils Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils/files.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua index d7793b98..4eec773a 100644 --- a/lua/CopilotChat/utils/files.lua +++ b/lua/CopilotChat/utils/files.lua @@ -188,7 +188,7 @@ M.grep = async.wrap(function(path, opts, callback) end end - if M.empty(cmd) then + if vim.tbl_isempty(cmd) then error('No executable found for grep') return end @@ -321,7 +321,7 @@ function M.uri_to_filename(uri) return uri end local ok, fname = pcall(vim.uri_to_fname, uri) - if not ok or M.empty(fname) then + if not ok or not fname or fname == '' then return uri end return fname From a6576949e821e7abf9d0135e87576a51ec0e2e68 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Aug 2025 22:44:11 +0200 Subject: [PATCH 580/728] feat(tiktoken): improve token counting accuracy (#1382) Use more accurate token prediction when tiktoken core is not available Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 4 ++++ lua/CopilotChat/tiktoken.lua | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index fb2929a0..5cd45ffc 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -362,6 +362,10 @@ function Client:ask(prompt, opts) local resource_tokens = #resource_messages > 0 and tiktoken:count(resource_messages[1].content) or 0 local required_tokens = prompt_tokens + system_tokens + resource_tokens + log.debug('Prompt tokens:', prompt_tokens) + log.debug('System tokens:', system_tokens) + log.debug('Resource tokens:', resource_tokens) + -- Calculate how many tokens we can use for history local history_limit = max_tokens - required_tokens local history_tokens = 0 diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 4066388a..09ccaf37 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,3 +1,4 @@ +local log = require('plenary.log') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') @@ -105,12 +106,12 @@ end ---@return number function Tiktoken:count(prompt) if not self.tiktoken_core then - return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count + return math.ceil(#prompt / 4) end local tokens = self:encode(prompt) if not tokens then - return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count + return math.ceil(#prompt / 4) end return #tokens end From f844a684bd9e59b4bfc8882b4beb9be81cccfe23 Mon Sep 17 00:00:00 2001 From: CTCHEN Date: Sat, 30 Aug 2025 01:43:54 +0800 Subject: [PATCH 581/728] fix(chat): correct header highlighting for multi-byte characters (#1385) The previous implementation used vim.fn.strwidth() to calculate the end column for header highlighting. This function returns the display width, which can differ from the byte length when multi-byte characters like emojis are present. Since the end_col for extmarks expects a byte-based index, this caused the highlighting to be applied incorrectly. This patch corrects the issue by using the byte length (#header_value) for the end_col of the highlight extmark, while still using vim.fn.strwidth() for virt_text_win_col to ensure correct visual alignment of UI elements. Fixes #1384 --- lua/CopilotChat/ui/chat.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index d6c1e283..315134e5 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -620,21 +620,21 @@ function Chat:render() if id then -- Draw the separator as virtual text over the header line, hiding the id and anything after the header if self.config.highlight_headers then - local sep_col = vim.fn.strwidth(header_value) - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, sep_col, { + local header_width = vim.fn.strwidth(header_value) + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { + end_col = #header_value, + hl_group = 'CopilotChatHeader', + priority = 100, + strict = false, + }) + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, #header_value, { virt_text = { { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, }, - virt_text_win_col = sep_col, + virt_text_win_col = header_width, priority = 200, strict = false, }) - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { - end_col = sep_col, - hl_group = 'CopilotChatHeader', - priority = 100, - strict = false, - }) end -- Finish previous message From 069c806c4066c52d8d3d3172ef7a476b8e71b120 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Aug 2025 17:44:22 +0000 Subject: [PATCH 582/728] 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 a893b52c..ea990272 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From ae2f5932983f42d54bee9ff803b5ab157b145787 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:44:53 +0200 Subject: [PATCH 583/728] docs: add ctchen222 as a contributor for code (#1386) * 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 933a828e..f658f7f4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -459,6 +459,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/8294038?v=4", "profile": "https://ruicsh.github.io", "contributions": ["code"] + }, + { + "login": "ctchen222", + "name": "CTCHEN", + "avatar_url": "https://avatars.githubusercontent.com/u/49014608?v=4", + "profile": "https://github.com/ctchen222", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index a56537c2..85bee0e5 100644 --- a/README.md +++ b/README.md @@ -607,6 +607,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Samiul Islam
Samiul Islam

💻 Rui Costa
Rui Costa

💻 + CTCHEN
CTCHEN

💻 From b7728f450bfc95c7c749a322b3f130a16f80e35c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 31 Aug 2025 23:03:57 +0200 Subject: [PATCH 584/728] fix(auth): improve token saving and polling logic (#1389) - Remove redundant directory creation and scheduling from file write - Ensure token file directory is created only when saving token - Refactor GitHub device flow polling to use recursion instead of loop - Add logging for token save location - Restore chat overlay when message is empty - Clear status and message notifications after authorization Closes #1388 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/providers.lua | 45 +++++++++++++++++----------- lua/CopilotChat/ui/chat.lua | 6 +++- lua/CopilotChat/utils/files.lua | 3 -- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index f2f99b94..3a4c7d24 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,3 +1,4 @@ +local log = require('plenary.log') local plenary_utils = require('plenary.async.util') local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') @@ -41,10 +42,14 @@ local function set_token(tag, token, save) return token end + utils.schedule_main() local tokens = load_tokens() tokens[tag] = token local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') - files.write_file(config_path .. '/tokens.json', vim.json.encode(tokens)) + local file_path = config_path .. '/tokens.json' + vim.fn.mkdir(vim.fn.fnamemodify(file_path, ':p:h'), 'p') + files.write_file(file_path, vim.json.encode(tokens)) + log.info('Token for ' .. tag .. ' saved to ' .. file_path) return token end @@ -65,23 +70,25 @@ local function github_device_flow(tag, client_id, scope) end local function poll_for_token(device_code, interval) - while true do - plenary_utils.sleep(interval * 1000) - - local res = curl.post('https://github.com/login/oauth/access_token', { - body = { - client_id = client_id, - device_code = device_code, - grant_type = 'urn:ietf:params:oauth:grant-type:device_code', - }, - headers = { ['Accept'] = 'application/json' }, - }) - local data = vim.json.decode(res.body) - if data.access_token then - return data.access_token - elseif data.error ~= 'authorization_pending' then - error('Auth error: ' .. (data.error or 'unknown')) - end + plenary_utils.sleep(interval * 1000) + + local res = curl.post('https://github.com/login/oauth/access_token', { + json_response = true, + body = { + client_id = client_id, + device_code = device_code, + grant_type = 'urn:ietf:params:oauth:grant-type:device_code', + }, + headers = { ['Accept'] = 'application/json' }, + }) + + local data = res.body + if data.access_token then + return data.access_token + elseif data.error ~= 'authorization_pending' then + error('Auth error: ' .. (data.error or 'unknown')) + else + return poll_for_token(device_code, interval) end end @@ -97,6 +104,8 @@ local function github_device_flow(tag, client_id, scope) ) notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for authorization...') token = poll_for_token(code_data.device_code, code_data.interval) + notify.publish(notify.MESSAGE, '') + notify.publish(notify.STATUS, '') return set_token(tag, token, true) end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 315134e5..98701b0a 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -109,7 +109,11 @@ local Chat = class(function(self, config, on_buf_create) self:open(self.config) end - self:overlay({ text = msg }) + if not msg or msg == '' then + self.chat_overlay:restore(self.winnr, self.bufnr) + else + self:overlay({ text = msg }) + end end) end, Overlay) diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua index 4eec773a..28184585 100644 --- a/lua/CopilotChat/utils/files.lua +++ b/lua/CopilotChat/utils/files.lua @@ -231,9 +231,6 @@ end ---@param data string The data to write ---@return boolean function M.write_file(path, data) - M.schedule_main() - vim.fn.mkdir(vim.fn.fnamemodify(path, ':p:h'), 'p') - local err, fd = async.uv.fs_open(path, 'w', 438) if err or not fd then return false From 0ab8ad192dd5cf45c8a3e37d07b7b939cb9c52bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 31 Aug 2025 21:04:16 +0000 Subject: [PATCH 585/728] 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 ea990272..af834e4c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 31 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -602,7 +602,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 1d8aa27e2317950b0b7ddc023487c6f2b7b074ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 00:34:03 +0200 Subject: [PATCH 586/728] chore(main): release 4.6.0 (#1381) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 14 ++++++++++++++ version.txt | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c85b74..e7ea1e6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [4.6.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.5.1...v4.6.0) (2025-08-31) + + +### Features + +* **tiktoken:** improve token counting accuracy ([#1382](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1382)) ([a657694](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a6576949e821e7abf9d0135e87576a51ec0e2e68)) + + +### Bug Fixes + +* **auth:** improve token saving and polling logic ([#1389](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1389)) ([b7728f4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b7728f450bfc95c7c749a322b3f130a16f80e35c)), closes [#1388](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1388) +* **chat:** correct header highlighting for multi-byte characters ([#1385](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1385)) ([f844a68](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f844a684bd9e59b4bfc8882b4beb9be81cccfe23)), closes [#1384](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1384) +* **utils:** use proper empty check ([#1380](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1380)) ([c4b2e03](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c4b2e03cd315c3fd9736dcf796cb20f6a4b9f801)) + ## [4.5.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.5.0...v4.5.1) (2025-08-28) diff --git a/version.txt b/version.txt index 4404a17b..6016e8ad 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.5.1 +4.6.0 From ba364fe04b36121a594435c3f54261c7a8e450a6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 03:29:50 +0200 Subject: [PATCH 587/728] feat(chat): switch to treesitter based chat parsing (#1394) this greatly improves performance compared to line by line matching Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 385 +++++++++++++++++-------------- lua/CopilotChat/ui/overlay.lua | 9 +- queries/markdown/copilotchat.scm | 13 ++ 3 files changed, 231 insertions(+), 176 deletions(-) create mode 100644 queries/markdown/copilotchat.scm diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 98701b0a..aa657afb 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -16,13 +16,28 @@ function CopilotChatFoldExpr(lnum, separator) end local HEADER_PATTERNS = { - '^```?(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', - '^```(%w+)$', + '^(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', + '^(%w+)$', } +---@param headers table? +---@return string?, string? +local function match_section_header(headers, separator, line) + if not headers then + return + end + + for header_name, header_value in pairs(headers) do + local id = line:match('^' .. vim.pesc(header_value) .. ' %(([^)]+)%) ' .. vim.pesc(separator) .. '$') + if id then + return id, header_name + end + end +end + ---@param header? string ---@return string?, string?, number?, number? -local function match_header(header) +local function match_block_header(header) if not header then return end @@ -47,7 +62,7 @@ end ---@field header CopilotChat.ui.chat.Header ---@field start_line number ---@field end_line number ----@field content string? +---@field content string ---@class CopilotChat.ui.chat.Section ---@field start_line number @@ -55,7 +70,7 @@ end ---@field blocks table ---@class CopilotChat.ui.chat.Message : CopilotChat.client.Message ----@field id string +---@field id string? ---@field section CopilotChat.ui.chat.Section? ---@class CopilotChat.ui.chat.Chat : CopilotChat.ui.overlay.Overlay @@ -79,7 +94,10 @@ local Chat = class(function(self, config, on_buf_create) self.messages = {} self.layout = nil - self.headers = config.headers + self.headers = {} + for k, v in pairs(config.headers or {}) do + self.headers[k] = v:gsub('^#+', ''):gsub('^%s+', '') + end self.separator = config.separator self.spinner = Spinner() @@ -140,7 +158,6 @@ function Chat:get_block(role, cursor) 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 @@ -176,12 +193,13 @@ end ---@param cursor boolean? If true, returns the message closest to the cursor position ---@return CopilotChat.ui.chat.Message? function Chat:get_message(role, cursor) + self:parse() + if cursor then if not self:visible() then return nil end - self:render() local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) local cursor_line = cursor_pos[1] local closest_message = nil @@ -366,7 +384,6 @@ function Chat:open(config) vim.api.nvim_set_hl(ns, '@markup.italic.markdown_inline', {}) -- disable italic messing up glob patterns vim.api.nvim_win_set_hl_ns(self.winnr, ns) vim.api.nvim_win_set_buf(self.winnr, self.bufnr) - self:render() end --- Close the chat window. @@ -446,9 +463,11 @@ function Chat:finish() end --- Add a message to the chat window. ----@param message CopilotChat.client.Message +---@param message CopilotChat.ui.chat.Message ---@param replace boolean? If true, replaces the last message if it has same role function Chat:add_message(message, replace) + self:parse() + local current_message = self.messages[#self.messages] local is_new = not current_message or current_message.role ~= message.role @@ -458,17 +477,15 @@ function Chat:add_message(message, replace) -- Add appropriate header based on role and generate a new ID if not provided message.id = message.id or utils.uuid() local header = self.headers[message.role] + table.insert(self.messages, message) + if current_message then - header = '\n' .. header + self:append('\n') end - - table.insert(self.messages, message) - self:append(header .. '(' .. message.id .. ')' .. self.separator .. '\n\n') + self:append('# ' .. header .. ' (' .. message.id .. ') ' .. self.separator .. '\n\n') self:append(message.content) elseif replace and current_message then -- Replace the content of the current message - self:render() - for k, v in pairs(message) do current_message[k] = v end @@ -503,7 +520,7 @@ function Chat:remove_message(role, cursor) return end - self:render() + self:parse() local message = self:get_message(role, cursor) if not message then return @@ -527,8 +544,6 @@ function Chat:remove_message(role, cursor) break end end - - self:render() end --- Append text to the chat window. @@ -580,9 +595,10 @@ function Chat:create() vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { buffer = bufnr, callback = function() - utils.debounce(self.name, function() + utils.debounce('chat-parse-' .. bufnr, function() + self:parse() self:render() - end, 100) + end, 150) end, }) @@ -599,149 +615,174 @@ function Chat:validate() end end ---- Render the chat window. ----@protected -function Chat:render() +function Chat:parse() self:validate() - local highlight_ns = vim.api.nvim_create_namespace('copilot-chat-headers') - vim.api.nvim_buf_clear_namespace(self.bufnr, highlight_ns, 0, -1) + local changedtick = vim.api.nvim_buf_get_changedtick(self.bufnr) + if self._last_changedtick == changedtick then + return false + end + self._last_changedtick = changedtick - local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) + local parser = vim.treesitter.get_parser(self.bufnr, 'markdown') + if not parser then + return + end + local query = vim.treesitter.query.get('markdown', 'copilotchat') + if not query then + return + end + + local root = parser:parse()[1]:root() local new_messages = {} - local current_message = nil - local current_block = nil - - local function parse_header(header, line) - return line:match('^' .. vim.pesc(header) .. '%(([^)]+)%)' .. vim.pesc(self.separator) .. '$') - end - - for l, line in ipairs(lines) do - -- Detect section header with ID - for header_name, header_value in pairs(self.headers) do - local id = parse_header(header_value, line) - if id then - -- Draw the separator as virtual text over the header line, hiding the id and anything after the header - if self.config.highlight_headers then - local header_width = vim.fn.strwidth(header_value) - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { - end_col = #header_value, - hl_group = 'CopilotChatHeader', - priority = 100, - strict = false, - }) - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, #header_value, { - virt_text = { - { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, + local current_message = { + content = {}, + section = { + blocks = {}, + }, + } + + local current_block = { + content = {}, + } + + for id, node in query:iter_captures(root, self.bufnr, 0, -1) do + local name = query.captures[id] + local start_row, _, end_row, _ = node:range() + + -- Convert 0 based to 1 based indexing + start_row = start_row + 1 + end_row = end_row + 1 + + -- Skip header line at start of the section + start_row = start_row + 1 + + if name == 'section_header' then + local header_text = vim.treesitter.get_node_text(node, self.bufnr) + local id, role = match_section_header(self.headers, self.separator, header_text) + if role and id ~= current_message.id then + current_message.section.end_line = start_row - 2 + + current_message = { + id = id, + role = role, + content = {}, + section = { + blocks = {}, + start_line = start_row, + }, + } + table.insert(new_messages, current_message) + end + elseif name == 'section_content' then + local content = vim.treesitter.get_node_text(node, self.bufnr) + current_message.section.end_line = end_row + table.insert(current_message.content, content) + elseif current_message.role == constants.ROLE.ASSISTANT then + if name == 'block_header' then + local header_text = vim.treesitter.get_node_text(node, self.bufnr) + local filetype, filename, start_line, end_line = match_block_header(header_text) + if filetype then + current_block = { + header = { + filetype = filetype, + filename = filename, + start_line = start_line, + end_line = end_line, }, - virt_text_win_col = header_width, - priority = 200, - strict = false, - }) - end - - -- Finish previous message - if current_message then - current_message.section.end_line = l - 1 - current_message.content = vim.trim( - table.concat( - vim.list_slice(lines, current_message.section.start_line, current_message.section.end_line), - '\n' - ) - ) + start_line = start_row, + content = {}, + } + table.insert(current_message.section.blocks, current_block) end + elseif name == 'block_content' then + local content = vim.treesitter.get_node_text(node, self.bufnr) + current_block.end_line = end_row + table.insert(current_block.content, content) + end + end + end - -- Find existing message by id or create new - local old_msg = nil - for _, msg in ipairs(self.messages) do - if msg.id == id then - old_msg = msg - break - end - end - if not old_msg then - old_msg = { id = id, role = header_name } - end + -- Finish last message + current_message.section.end_line = vim.api.nvim_buf_line_count(self.bufnr) - -- Attach section info - old_msg.section = { - role = header_name, - start_line = l + 1, - blocks = {}, - } - table.insert(new_messages, old_msg) - current_message = old_msg - current_block = nil - break + for _, message in ipairs(new_messages) do + message.content = vim.trim(table.concat(message.content, '\n')) + if message.section then + for _, block in ipairs(message.section.blocks) do + block.content = vim.trim(table.concat(block.content, '\n')) end end + end - -- Code blocks - if current_message and current_message.role == constants.ROLE.ASSISTANT then - local filetype, filename, start_line, end_line = match_header(line) - if filetype and filename and not current_block then - current_block = { - header = { - filename = filename, - start_line = start_line, - end_line = end_line, - filetype = filetype, - }, - start_line = l + 1, - } + self.messages = new_messages +end + +--- Render the chat window. +---@protected +function Chat:render() + self:validate() + + local highlight_ns = vim.api.nvim_create_namespace('copilot-chat-headers') + vim.api.nvim_buf_clear_namespace(self.bufnr, highlight_ns, 0, -1) -- Clear previous highlights + self:show_help() -- Clear previous help + + for i, message in ipairs(self.messages) do + if self.config.highlight_headers then + -- Overlay section header with nice display + local header_value = self.headers[message.role] + local header_line = message.section.start_line - 2 + + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, header_line, 0, { + conceal = '', + virt_text = { + { ' ' .. header_value .. ' ', 'CopilotChatHeader' }, + { string.rep(self.separator, vim.go.columns - #header_value - 1), 'CopilotChatSeparator' }, + }, + virt_text_pos = 'overlay', + priority = 300, + strict = false, + }) + + -- Highlight code block headers and show file info as virtual lines + for _, block in ipairs(message.section.blocks) do + local header = block.header + local filetype = header.filetype + local filename = header.filename local text = string.format('[%s] %s', filetype, filename) - if start_line and end_line then - text = text .. string.format(' lines %d-%d', start_line, end_line) + if header.start_line and header.end_line then + text = text .. string.format(' lines %d-%d', header.start_line, header.end_line) end - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, block.start_line - 1, 0, { virt_lines_above = true, virt_lines = { { { text, 'CopilotChatAnnotationHeader' } } }, priority = 100, strict = false, }) - elseif line == '```' and current_block then - current_block.end_line = l - 1 - current_block.content = - table.concat(vim.list_slice(lines, current_block.start_line, current_block.end_line), '\n') - table.insert(current_message.section.blocks, current_block) - current_block = nil end end - -- If last line, finish last message - if l == #lines and current_message then - current_message.section.end_line = l - current_message.content = vim.trim( - table.concat(vim.list_slice(lines, current_message.section.start_line, current_message.section.end_line), '\n') - ) - end - - -- Highlight response calls - for _, message in ipairs(self.messages) do - for _, tool_call in ipairs(message.tool_calls or {}) do - if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then - vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) - if not utils.empty(tool_call.arguments) then - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { - virt_lines = vim.tbl_map(function(json_line) - return { { json_line, 'CopilotChatAnnotation' } } - end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), - priority = 100, - strict = false, - }) - end - break - end + -- Show reasoning as virtual text above assistant messages + if + message.role == constants.ROLE.ASSISTANT + and not utils.empty(message.reasoning) + and message.section + and message.section.start_line + then + local virt_lines = {} + for _, line in ipairs(vim.split(message.reasoning, '\n')) do + table.insert(virt_lines, { { 'Reasoning: ' .. line, 'CopilotChatAnnotation' } }) end + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, message.section.start_line - 1, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + priority = 100, + strict = false, + }) end - end - - -- Replace self.messages with new_messages (preserving tool_calls, etc.) - self.messages = new_messages - for i, message in ipairs(self.messages) do - -- Show tool call details as virt lines + -- Show tool call details as virt lines in assistant messages if message.tool_calls and #message.tool_calls > 0 then local section = message.section if section and section.end_line then @@ -761,13 +802,14 @@ function Chat:render() end end + -- Highlight tool calls in tool messages if message.tool_call_id then local section = message.section if section and section.start_line then local virt_lines = { { { 'Tool: ' .. message.tool_call_id, 'CopilotChatAnnotationHeader' } }, } - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, section.start_line, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, section.start_line - 1, 0, { virt_lines = virt_lines, virt_lines_above = true, priority = 100, @@ -776,25 +818,41 @@ function Chat:render() end end - -- Show reasoning as virtual text above assistant messages - if - message.role == constants.ROLE.ASSISTANT - and not utils.empty(message.reasoning) - and message.section - and message.section.start_line - then - local virt_lines = {} - for _, line in ipairs(vim.split(message.reasoning, '\n')) do - table.insert(virt_lines, { { 'Reasoning: ' .. line, 'CopilotChatAnnotation' } }) + if i == #self.messages and message.role == constants.ROLE.USER then + -- Highlight tools in the last user message + local assistant_msg = self:get_message(constants.ROLE.ASSISTANT) + if assistant_msg and assistant_msg.tool_calls and #assistant_msg.tool_calls > 0 then + for i, line in ipairs(utils.split_lines(message.content)) do + for _, tool_call in ipairs(assistant_msg.tool_calls) do + if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then + local l = message.section.start_line - 1 + i + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatAnnotationHeader', l, 0, #line) + if not utils.empty(tool_call.arguments) then + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { + virt_lines = vim.tbl_map(function(json_line) + return { { json_line, 'CopilotChatAnnotation' } } + end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), + priority = 100, + strict = false, + }) + end + end + end + end end - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, message.section.start_line - 1, 0, { - virt_lines = virt_lines, - virt_lines_above = true, - priority = 100, - strict = false, - }) + + -- Show help message and token usage below the last user message + local msg = self.config.show_help and self.help or '' + if self.token_count and self.token_max_count then + if msg ~= '' then + msg = msg .. '\n' + end + msg = msg .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' + end + self:show_help(msg, message.section.start_line - 1) end + -- Auto fold non-assistant messages if enabled if self.config.auto_fold and self:visible() then if message.role ~= constants.ROLE.ASSISTANT and message.section and i < #self.messages then vim.api.nvim_win_call(self.winnr, function() @@ -806,21 +864,6 @@ function Chat:render() end end end - - -- Show help as before, using last user message - local last_message = self.messages[#self.messages] - if last_message and last_message.role == constants.ROLE.USER then - local msg = self.config.show_help and self.help or '' - if self.token_count and self.token_max_count then - if msg ~= '' then - msg = msg .. '\n' - end - msg = msg .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' - end - self:show_help(msg, last_message.section.start_line - last_message.section.end_line - 1) - else - self:show_help() - end end --- Get the last line and column of the chat window. diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 298bfcb2..ddaa41a5 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -41,7 +41,7 @@ function Overlay:show(text, winnr, filetype, syntax, on_show, on_hide) 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) + self:show_help(self.help, vim.api.nvim_buf_line_count(self.bufnr)) vim.api.nvim_win_set_cursor(winnr, { 1, 0 }) filetype = filetype or 'markdown' @@ -130,17 +130,16 @@ end --- Show help message in the overlay ---@param msg string? ----@param offset number? +---@param pos number ---@protected -function Overlay:show_help(msg, offset) +function Overlay:show_help(msg, pos) if not msg or msg == '' then vim.api.nvim_buf_del_extmark(self.bufnr, self.help_ns, 1) return end self:validate() - local line = vim.api.nvim_buf_line_count(self.bufnr) + (offset or 0) - vim.api.nvim_buf_set_extmark(self.bufnr, self.help_ns, math.max(0, line - 1), 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, self.help_ns, math.max(0, pos - 1), 0, { id = 1, hl_mode = 'combine', priority = 100, diff --git a/queries/markdown/copilotchat.scm b/queries/markdown/copilotchat.scm new file mode 100644 index 00000000..f4ec8546 --- /dev/null +++ b/queries/markdown/copilotchat.scm @@ -0,0 +1,13 @@ +(section + (atx_heading + (atx_h1_marker) + heading_content: (_) @section_header + ) + (_)? @section_content +) +(section + (fenced_code_block + (info_string) @block_header + (code_fence_content) @block_content + ) +) From 070e3022d8878567a089143e2672e8c06ab83be2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 12 Sep 2025 01:30:16 +0000 Subject: [PATCH 588/728] 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 af834e4c..456e34a1 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 31 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 12 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From f2f523fe3fdb855da1b3dcabf4f2981cdc3b2c2d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 09:12:22 +0200 Subject: [PATCH 589/728] fix(ui): preserve extra fields in chat messages (#1399) Preserves additional fields in chat messages when parsing, ensuring that tool call data and other metadata are not lost between parses. Also fixes annotation highlighting and extmark placement for tool calls in chat rendering. Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index aa657afb..ce7a1c3d 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -618,6 +618,7 @@ end function Chat:parse() self:validate() + -- Skip parsing if buffer hasn't changed local changedtick = vim.api.nvim_buf_get_changedtick(self.bufnr) if self._last_changedtick == changedtick then return false @@ -707,6 +708,15 @@ function Chat:parse() -- Finish last message current_message.section.end_line = vim.api.nvim_buf_line_count(self.bufnr) + -- Build lookup table for previous messages by id + local old_messages_by_id = {} + for _, msg in ipairs(self.messages or {}) do + if msg.id then + old_messages_by_id[msg.id] = msg + end + end + + -- Format new messages and preserve extra fields from old messages for _, message in ipairs(new_messages) do message.content = vim.trim(table.concat(message.content, '\n')) if message.section then @@ -714,6 +724,15 @@ function Chat:parse() block.content = vim.trim(table.concat(block.content, '\n')) end end + + local old = old_messages_by_id[message.id] + if old then + for k, v in pairs(old) do + if message[k] == nil then + message[k] = v + end + end + end end self.messages = new_messages @@ -825,10 +844,10 @@ function Chat:render() for i, line in ipairs(utils.split_lines(message.content)) do for _, tool_call in ipairs(assistant_msg.tool_calls) do if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then - local l = message.section.start_line - 1 + i + local l = message.section.start_line + i vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatAnnotationHeader', l, 0, #line) if not utils.empty(tool_call.arguments) then - vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l, 0, { virt_lines = vim.tbl_map(function(json_line) return { { json_line, 'CopilotChatAnnotation' } } end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), From 62a91c3ad055228e784de640d8c3fa114841c37f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 09:19:45 +0200 Subject: [PATCH 590/728] refactor(ui): improve chat and overlay function signatures (#1400) - Remove unnecessary visibility check in Chat:remove_message - Add @protected annotation to Chat:parse for clarity - Make Overlay:show_help pos parameter optional for flexibility These changes enhance code readability and maintainability by refining function signatures and documentation. --- lua/CopilotChat/ui/chat.lua | 7 +++---- lua/CopilotChat/ui/overlay.lua | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index ce7a1c3d..2f97fe74 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -516,11 +516,8 @@ end ---@param role string? If specified, only considers sections of the given role ---@param cursor boolean? If true, removes the message closest to the cursor position function Chat:remove_message(role, cursor) - if not self:visible() then - return - end - self:parse() + local message = self:get_message(role, cursor) if not message then return @@ -615,6 +612,8 @@ function Chat:validate() end end +--- Parse the chat window buffer into structured messages. +---@protected function Chat:parse() self:validate() diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index ddaa41a5..7a56c33b 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -130,7 +130,7 @@ end --- Show help message in the overlay ---@param msg string? ----@param pos number +---@param pos number? ---@protected function Overlay:show_help(msg, pos) if not msg or msg == '' then From f49df19d5a8925d295ac6472c30b36584bd10d93 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 09:40:30 +0200 Subject: [PATCH 591/728] feat(health): require markdown parser and copilotchat query (#1401) Update health checks to require the markdown treesitter parser and copilotchat query for chat parsing. Errors are now shown if either is missing, with instructions for installation. This ensures proper chat highlighting and parsing functionality. Signed-off-by: Tomas Slusny --- lua/CopilotChat/health.lua | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 1c8bc3b4..0c3bcfe9 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -38,6 +38,15 @@ local function treesitter_parser_available(ft) return res and parser ~= nil end +--- Check if a treesitter query is available +---@param ft string +---@param query_name string +---@return boolean +local function treesitter_query_available(ft, query_name) + local query = vim.treesitter.query.get(ft, query_name) + return query ~= nil +end + function M.check() start('CopilotChat.nvim [core]') @@ -145,8 +154,16 @@ function M.check() if treesitter_parser_available('markdown') then ok('treesitter[markdown]: installed') else - warn( - 'treesitter[markdown]: missing, optional for better chat highlighting. Install `nvim-treesitter/nvim-treesitter` plugin and run `:TSInstall markdown`.' + error( + 'treesitter[markdown]: missing, required for chat parsing. Install `nvim-treesitter/nvim-treesitter` plugin and run `:TSInstall markdown`.' + ) + end + + if treesitter_query_available('markdown', 'copilotchat') then + ok('treesitter[markdown/copilotchat]: found') + else + error( + 'treesitter[markdown/copilotchat]: missing, required for chat parsing. See `:h CopilotChat-installation` for instructions.' ) end From 4a45e69de8ad2b72ef62ede5a554c68c9632e718 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 10:03:50 +0200 Subject: [PATCH 592/728] perf(chat): simplify last line/column calculation (#1402) Replaces Chat:last() method with a standalone last() function for retrieving the last line and column of the chat buffer. Updates references to use the new function and removes redundant logic. This improves code clarity and maintainability. --- lua/CopilotChat/ui/chat.lua | 41 +++++++++++++++---------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 2f97fe74..4681779b 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -52,6 +52,21 @@ local function match_block_header(header) end end +--- Get the last line and column of the chat window. +---@param bufnr number +---@return number, number +---@protected +local function last(bufnr) + local line_count = vim.api.nvim_buf_line_count(bufnr) + if line_count == 0 then + return 0, 0 + end + local last_line = line_count - 1 + local last_line_content = vim.api.nvim_buf_get_lines(bufnr, last_line, last_line + 1, false) + local last_column = last_line_content[1] and #last_line_content[1] or 0 + return last_line, last_column +end + ---@class CopilotChat.ui.chat.Header ---@field filename string ---@field start_line number @@ -426,11 +441,7 @@ function Chat:follow() return end - local last_line, last_column, line_count = self:last() - if line_count == 0 then - return - end - + local last_line, last_column = last(self.bufnr) vim.api.nvim_win_set_cursor(self.winnr, { last_line + 1, last_column }) end @@ -557,7 +568,7 @@ function Chat:append(str) should_follow_cursor = current_pos[1] >= line_count - 1 end - local last_line, last_column, _ = self:last() + local last_line, last_column, _ = last(self.bufnr) local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true @@ -884,22 +895,4 @@ function Chat:render() end end ---- Get the last line and column of the chat window. ----@return number, number, number ----@protected -function Chat:last() - self:validate() - local line_count = vim.api.nvim_buf_line_count(self.bufnr) - local last_line = line_count - 1 - if last_line < 0 then - return 0, 0, line_count - end - local last_line_content = vim.api.nvim_buf_get_lines(self.bufnr, -2, -1, false) - if not last_line_content or #last_line_content == 0 then - return last_line, 0, line_count - end - local last_column = #last_line_content[1] - return last_line, last_column, line_count -end - return Chat From 1041ad0034e65e4a63859172d31e7045c8975d87 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 12:14:08 +0200 Subject: [PATCH 593/728] perf(chat): optimize message storage and access (#1403) Switch chat message storage to OrderedMap for improved performance and consistency. Refactor all message access to use get_messages() and update related logic for adding, removing, and parsing messages. Adds remove() method to OrderedMap utility. This change improves efficiency and prepares for future scalability. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 9 ++-- lua/CopilotChat/init.lua | 6 +-- lua/CopilotChat/ui/chat.lua | 65 +++++++++++++++------------- lua/CopilotChat/utils/orderedmap.lua | 13 ++++++ tests/orderedmap_spec.lua | 9 ++++ 5 files changed, 65 insertions(+), 37 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 46f47e01..23867f61 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -279,9 +279,10 @@ return { normal = 'gqa', callback = function() local items = {} - for i, message in ipairs(copilot.chat.messages) do + local messages = copilot.chat:get_messages() + for i, message in ipairs(messages) do if message.section and message.role == constants.ROLE.ASSISTANT then - local prev_message = copilot.chat.messages[i - 1] + local prev_message = messages[i - 1] local text = '' if prev_message then text = prev_message.content @@ -305,8 +306,8 @@ return { normal = 'gqd', callback = function(source) local items = {} - - for _, message in ipairs(copilot.chat.messages) do + local messages = copilot.chat:get_messages() + for _, message in ipairs(messages) do if message.section then for _, block in ipairs(message.section.blocks) do local diff = get_diff(source.bufnr, block) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d87b90e7..69d6ac77 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -356,7 +356,7 @@ function M.resolve_functions(prompt, config) local resolved_resources = {} local resolved_tools = {} local tool_calls = {} - for _, message in ipairs(M.chat.messages) do + for _, message in ipairs(M.chat:get_messages()) do if message.tool_calls then for _, tool_call in ipairs(message.tool_calls) do table.insert(tool_calls, tool_call) @@ -868,7 +868,7 @@ function M.ask(prompt, config) local ask_response = client.ask(client, prompt, { headless = config.headless, - history = M.chat.messages, + history = M.chat:get_messages(), resources = resolved_resources, tools = selected_tools, system_prompt = system_prompt, @@ -948,7 +948,7 @@ function M.save(name, history_path) return end - local history = vim.deepcopy(M.chat.messages) + local history = vim.deepcopy(M.chat:get_messages()) for _, message in ipairs(history) do message.section = nil end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 4681779b..95d7fc1e 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -4,6 +4,7 @@ local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = require('CopilotChat.utils.class') +local orderedmap = require('CopilotChat.utils.orderedmap') function CopilotChatFoldExpr(lnum, separator) local to_match = separator .. '$' @@ -93,7 +94,7 @@ end ---@field config CopilotChat.config.Shared ---@field token_count number? ---@field token_max_count number? ----@field messages table +---@field private messages OrderedMap ---@field private layout CopilotChat.config.Layout? ---@field private headers table ---@field private separator string @@ -106,7 +107,7 @@ local Chat = class(function(self, config, on_buf_create) self.config = config self.token_count = nil self.token_max_count = nil - self.messages = {} + self.messages = orderedmap() self.layout = nil self.headers = {} @@ -168,6 +169,9 @@ end ---@param cursor boolean? If true, returns the block closest to the cursor position ---@return CopilotChat.ui.chat.Block? function Chat:get_block(role, cursor) + self:parse() + local messages = self:get_messages() + if cursor then if not self:visible() then return nil @@ -178,7 +182,7 @@ function Chat:get_block(role, cursor) local closest_block = nil local max_line_below_cursor = -1 - for _, message in ipairs(self.messages) do + for _, message in ipairs(messages) do local section = message.section local matches_role = not role or message.role == role if matches_role and section and section.blocks then @@ -194,8 +198,8 @@ function Chat:get_block(role, cursor) return closest_block end - for i = #self.messages, 1, -1 do - local message = self.messages[i] + for i = #messages, 1, -1 do + local message = messages[i] local matches_role = not role or message.role == role if matches_role and message.section and message.section.blocks and #message.section.blocks > 0 then return message.section.blocks[#message.section.blocks] @@ -203,12 +207,19 @@ function Chat:get_block(role, cursor) end end +--- Get list of all chat messages +---@return table +function Chat:get_messages() + return self.messages:values() +end + --- Get last message by role in the chat window. ---@param role string? If specified, only considers sections of the given role ---@param cursor boolean? If true, returns the message closest to the cursor position ---@return CopilotChat.ui.chat.Message? function Chat:get_message(role, cursor) self:parse() + local messages = self:get_messages() if cursor then if not self:visible() then @@ -220,7 +231,7 @@ function Chat:get_message(role, cursor) local closest_message = nil local max_line_below_cursor = -1 - for _, message in ipairs(self.messages) do + for _, message in ipairs(messages) do local section = message.section local matches_role = not role or message.role == role if matches_role and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then @@ -232,8 +243,8 @@ function Chat:get_message(role, cursor) return closest_message end - for i = #self.messages, 1, -1 do - local message = self.messages[i] + for i = #messages, 1, -1 do + local message = messages[i] local matches_role = not role or message.role == role if matches_role then return message @@ -479,7 +490,8 @@ end function Chat:add_message(message, replace) self:parse() - local current_message = self.messages[#self.messages] + local messages = self:get_messages() + local current_message = messages[#messages] local is_new = not current_message or current_message.role ~= message.role or (message.id and current_message.id ~= message.id) @@ -488,7 +500,7 @@ function Chat:add_message(message, replace) -- Add appropriate header based on role and generate a new ID if not provided message.id = message.id or utils.uuid() local header = self.headers[message.role] - table.insert(self.messages, message) + self.messages:set(message.id, message) if current_message then self:append('\n') @@ -546,12 +558,7 @@ function Chat:remove_message(role, cursor) vim.bo[self.bufnr].modifiable = modifiable -- Remove the message from the messages list - for i, msg in ipairs(self.messages) do - if msg.id == message.id then - table.remove(self.messages, i) - break - end - end + self.messages:remove(message.id) end --- Append text to the chat window. @@ -585,7 +592,7 @@ function Chat:clear() self:validate() self.token_count = nil self.token_max_count = nil - self.messages = {} + self.messages = orderedmap() local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true @@ -718,15 +725,8 @@ function Chat:parse() -- Finish last message current_message.section.end_line = vim.api.nvim_buf_line_count(self.bufnr) - -- Build lookup table for previous messages by id - local old_messages_by_id = {} - for _, msg in ipairs(self.messages or {}) do - if msg.id then - old_messages_by_id[msg.id] = msg - end - end - -- Format new messages and preserve extra fields from old messages + local messages = orderedmap() for _, message in ipairs(new_messages) do message.content = vim.trim(table.concat(message.content, '\n')) if message.section then @@ -735,7 +735,7 @@ function Chat:parse() end end - local old = old_messages_by_id[message.id] + local old = self.messages:get(message.id) if old then for k, v in pairs(old) do if message[k] == nil then @@ -743,9 +743,12 @@ function Chat:parse() end end end + + messages:set(message.id, message) end - self.messages = new_messages + -- Update messages + self.messages = messages end --- Render the chat window. @@ -757,7 +760,9 @@ function Chat:render() vim.api.nvim_buf_clear_namespace(self.bufnr, highlight_ns, 0, -1) -- Clear previous highlights self:show_help() -- Clear previous help - for i, message in ipairs(self.messages) do + local messages = self:get_messages() + + for i, message in ipairs(messages) do if self.config.highlight_headers then -- Overlay section header with nice display local header_value = self.headers[message.role] @@ -847,7 +852,7 @@ function Chat:render() end end - if i == #self.messages and message.role == constants.ROLE.USER then + if i == #messages and message.role == constants.ROLE.USER then -- Highlight tools in the last user message local assistant_msg = self:get_message(constants.ROLE.ASSISTANT) if assistant_msg and assistant_msg.tool_calls and #assistant_msg.tool_calls > 0 then @@ -883,7 +888,7 @@ function Chat:render() -- Auto fold non-assistant messages if enabled if self.config.auto_fold and self:visible() then - if message.role ~= constants.ROLE.ASSISTANT and message.section and i < #self.messages then + if message.role ~= constants.ROLE.ASSISTANT and message.section and i < #messages then vim.api.nvim_win_call(self.winnr, function() local fold_level = vim.fn.foldlevel(message.section.start_line) if fold_level > 0 and vim.fn.foldclosed(message.section.start_line) == -1 then diff --git a/lua/CopilotChat/utils/orderedmap.lua b/lua/CopilotChat/utils/orderedmap.lua index 778c686d..1907c161 100644 --- a/lua/CopilotChat/utils/orderedmap.lua +++ b/lua/CopilotChat/utils/orderedmap.lua @@ -1,6 +1,7 @@ ---@class OrderedMap ---@field set fun(self:OrderedMap, key:any, value:any) ---@field get fun(self:OrderedMap, key:any):any +---@field remove fun(self:OrderedMap, key:any) ---@field keys fun(self:OrderedMap):table ---@field values fun(self:OrderedMap):table @@ -22,6 +23,18 @@ local function orderedmap() return self._data[key] end, + remove = function(self, key) + if self._data[key] then + self._data[key] = nil + for i, k in ipairs(self._keys) do + if k == key then + table.remove(self._keys, i) + break + end + end + end + end, + keys = function(self) return self._keys end, diff --git a/tests/orderedmap_spec.lua b/tests/orderedmap_spec.lua index 9000915c..b5fa5a37 100644 --- a/tests/orderedmap_spec.lua +++ b/tests/orderedmap_spec.lua @@ -25,4 +25,13 @@ describe('CopilotChat.utils.orderedmap', function() assert.are.same({ 'a' }, map:keys()) assert.are.same({ 2 }, map:values()) end) + + it('removes values and updates order', function() + local map = orderedmap() + map:set('a', 1) + map:set('b', 2) + map:remove('a') + assert.are.same({ 'b' }, map:keys()) + assert.are.same({ 2 }, map:values()) + end) end) From 418562ef83fba5c155a06f27c144fcb28c5ff815 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 12:17:26 +0200 Subject: [PATCH 594/728] refactor(ui): remove redundant spinner checks in chat (#1404) Simplifies Chat:start and Chat:finish by removing unnecessary checks for self.spinner. Assumes spinner is always present, streamlining the code and reducing conditional branches. Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 95d7fc1e..f018d6b7 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -464,19 +464,12 @@ function Chat:start() utils.return_to_normal_mode() end - if self.spinner then - self.spinner:start() - end - + self.spinner:start() vim.bo[self.bufnr].modifiable = false end --- Finish writing to the chat window. function Chat:finish() - if not self.spinner then - return - end - self.spinner:finish() vim.bo[self.bufnr].modifiable = true if self.config.auto_insert_mode and self:focused() then From 9fdf8951efff6ab4f46e06945e5d6425bdbf4f80 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 20:45:26 +0200 Subject: [PATCH 595/728] feat(diff): add experimental unified diff support, refactor handling (#1392) - Introduce experimental unified diff parsing and application utilities - Refactor mappings to support both block and unified diff formats - Add configuration option to select diff format - Update prompts and instructions for diff formats and tool usage - Improve chat UI parsing for diff blocks - Add tests for diff utilities and edge cases Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 2 + lua/CopilotChat/config/mappings.lua | 340 ++++++------------ lua/CopilotChat/config/prompts.lua | 59 +-- lua/CopilotChat/init.lua | 16 +- .../instructions/edit_file_block.lua | 41 +++ .../instructions/edit_file_unified.lua | 68 ++++ lua/CopilotChat/instructions/tool_use.lua | 12 + lua/CopilotChat/ui/chat.lua | 42 ++- lua/CopilotChat/utils/diff.lua | 229 ++++++++++++ tests/diff_spec.lua | 111 ++++++ 10 files changed, 620 insertions(+), 300 deletions(-) create mode 100644 lua/CopilotChat/instructions/edit_file_block.lua create mode 100644 lua/CopilotChat/instructions/edit_file_unified.lua create mode 100644 lua/CopilotChat/instructions/tool_use.lua create mode 100644 lua/CopilotChat/utils/diff.lua create mode 100644 tests/diff_spec.lua diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b525e995..5261c70e 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -19,6 +19,7 @@ ---@field tools string|table|nil ---@field resources string|table|nil ---@field sticky string|table|nil +---@field diff 'block'|'unified'? ---@field language string? ---@field temperature number? ---@field headless boolean? @@ -62,6 +63,7 @@ return { tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). resources = 'selection', -- Default resources to share with LLM (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + diff = 'block', -- Default diff format to use, 'block' or 'unified'. language = 'English', -- Default language to use for answers temperature = 0.1, -- Result temperature diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 23867f61..97a384f8 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -4,103 +4,25 @@ local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') +local diff = require('CopilotChat.utils.diff') local files = require('CopilotChat.utils.files') ----@class CopilotChat.config.mappings.Diff ----@field change string ----@field reference string ----@field filename string ----@field filetype string ----@field start_line number ----@field end_line number ----@field bufnr number? - ---- Get diff data from a block ----@param bufnr number ----@param block CopilotChat.ui.chat.Block? ----@return CopilotChat.config.mappings.Diff? -local function get_diff(bufnr, block) - -- If no block found, return nil - if not block then - return nil - end - - local header = block.header - local selection = select.get(bufnr) - local filename = nil - local filetype = nil - local start_line = nil - local end_line = nil - local reference = nil - local bufnr = nil - - if selection then - -- If we have a selection, use it as default source of truth - filename = selection.filename - filetype = selection.filetype - start_line = selection.start_line - end_line = selection.end_line - reference = selection.content - bufnr = selection.bufnr - end - - -- If we have header info, use it as source of truth - if header.start_line and header.end_line then - filename = files.uri_to_filename(header.filename) - filetype = header.filetype or files.filetype(filename) - start_line = header.start_line - end_line = header.end_line - - -- 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 files.filename_same(vim.api.nvim_buf_get_name(win_buf), header.filename) then - bufnr = win_buf - break - end - end - - -- If we found a valid buffer, get the reference content - if bufnr and utils.buf_valid(bufnr) then - local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false) - reference = table.concat(lines, '\n') - filetype = vim.bo[bufnr].filetype - end - end - - -- 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 '', - filetype = filetype or '', - filename = filename, - start_line = start_line, - end_line = end_line, - bufnr = bufnr, - } -end - --- Prepare a buffer for applying a diff ----@param diff CopilotChat.config.mappings.Diff? ----@param source CopilotChat.source? ----@return CopilotChat.config.mappings.Diff? -local function prepare_diff_buffer(diff, source) - if not diff then - return diff +---@param filename string? +---@param source CopilotChat.source +---@return integer +local function prepare_diff_buffer(filename, source) + if not filename then + filename = vim.api.nvim_buf_get_name(source.bufnr) end - local diff_bufnr = diff.bufnr + local diff_bufnr = nil -- If buffer is not found, try to load it if not diff_bufnr then -- Try to find matching buffer first for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if files.filename_same(vim.api.nvim_buf_get_name(buf), diff.filename) then + if files.filename_same(vim.api.nvim_buf_get_name(buf), filename) then diff_bufnr = buf break end @@ -108,11 +30,9 @@ local function prepare_diff_buffer(diff, source) -- If still not found, create a new buffer if not diff_bufnr then - diff_bufnr = vim.fn.bufadd(diff.filename) + diff_bufnr = vim.fn.bufadd(filename) vim.fn.bufload(diff_bufnr) end - - diff.bufnr = diff_bufnr end -- If source exists, update it to point to the diff buffer @@ -121,7 +41,7 @@ local function prepare_diff_buffer(diff, source) vim.api.nvim_win_set_buf(source.winnr, diff_bufnr) end - return diff + return diff_bufnr end ---@class CopilotChat.config.mapping @@ -132,9 +52,6 @@ end ---@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping ---@field register string? ----@class CopilotChat.config.mapping.show_diff : CopilotChat.config.mapping ----@field full_diff boolean? - ---@class CopilotChat.config.mappings ---@field complete CopilotChat.config.mapping|false|nil ---@field close CopilotChat.config.mapping|false|nil @@ -145,7 +62,7 @@ end ---@field jump_to_diff CopilotChat.config.mapping|false|nil ---@field quickfix_diffs CopilotChat.config.mapping|false|nil ---@field yank_diff CopilotChat.config.mapping.yank_diff|false|nil ----@field show_diff CopilotChat.config.mapping.show_diff|false|nil +---@field show_diff CopilotChat.config.mapping|false|nil ---@field show_info CopilotChat.config.mapping|false|nil ---@field show_help CopilotChat.config.mapping|false|nil return { @@ -248,87 +165,41 @@ return { normal = '', insert = '', callback = function(source) - local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) - diff = prepare_diff_buffer(diff, source) - if not diff then + local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + if not block then return end - local lines = utils.split_lines(diff.change) - vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) - select.set(source.bufnr, source.winnr, diff.start_line, diff.start_line + #lines - 1) - select.highlight(source.bufnr) + local path = block.header.filename + local bufnr = prepare_diff_buffer(path, source) + local new_lines, applied = diff.apply_diff(block, bufnr) + if not applied then + new_lines = utils.split_lines(block.content) + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines) + local first, last = diff.get_diff_region(block, bufnr) + if first and last then + select.set(bufnr, source.winnr, first, last) + select.highlight(bufnr) + end end, }, jump_to_diff = { normal = 'gj', callback = function(source) - local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) - diff = prepare_diff_buffer(diff, source) - if not diff then + local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + if not block then return end - select.set(source.bufnr, source.winnr, diff.start_line, diff.end_line) - select.highlight(source.bufnr) - end, - }, - - quickfix_answers = { - normal = 'gqa', - callback = function() - local items = {} - local messages = copilot.chat:get_messages() - for i, message in ipairs(messages) do - if message.section and message.role == constants.ROLE.ASSISTANT then - local prev_message = messages[i - 1] - local text = '' - if prev_message then - text = prev_message.content - end - - table.insert(items, { - bufnr = copilot.chat.bufnr, - lnum = message.section.start_line, - end_lnum = message.section.end_line, - text = text, - }) - end - end - - vim.fn.setqflist(items) - vim.cmd('copen') - end, - }, - - quickfix_diffs = { - normal = 'gqd', - callback = function(source) - local items = {} - local messages = copilot.chat:get_messages() - for _, message in ipairs(messages) do - if message.section then - for _, block in ipairs(message.section.blocks) do - local diff = get_diff(source.bufnr, block) - if diff then - local text = string.format('%s (%s)', diff.filename, diff.filetype) - if diff.start_line and diff.end_line then - text = text .. string.format(' [lines %d-%d]', diff.start_line, diff.end_line) - end - - table.insert(items, { - bufnr = copilot.chat.bufnr, - lnum = block.start_line, - end_lnum = block.end_line, - text = text, - }) - end - end - end - - vim.fn.setqflist(items) - vim.cmd('copen') + local path = block.header.filename + local bufnr = prepare_diff_buffer(path, source) + local first, last = diff.get_diff_region(block, bufnr) + if first and last and bufnr then + select.set(bufnr, source.winnr, first, last) + select.highlight(bufnr) end end, }, @@ -348,99 +219,96 @@ return { show_diff = { normal = 'gd', - full_diff = false, -- Show full diff instead of unified diff when showing diff window callback = function(source) - local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true)) - diff = prepare_diff_buffer(diff, source) - if not diff then + local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + if not block then return end + local path = block.header.filename + local bufnr = prepare_diff_buffer(path, source) + local new_lines, applied = diff.apply_diff(block, bufnr) + if not applied then + new_lines = utils.split_lines(block.content) + end + local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local opts = { - filetype = diff.filetype, - syntax = 'diff', + filetype = vim.bo[bufnr].filetype, + text = applied and table.concat(new_lines, '\n') or table.concat(original_lines, '\n'), } - if copilot.config.mappings.show_diff.full_diff then - local original = utils.buf_valid(diff.bufnr) and vim.api.nvim_buf_get_lines(diff.bufnr, 0, -1, false) or {} - - if #original > 0 then - -- Find all diffs from the same file in this section - local message = copilot.chat:get_message(constants.ROLE.ASSISTANT, true) - local section = message and message.section - local same_file_diffs = {} - if section then - for _, block in ipairs(section.blocks) do - local block_diff = get_diff(source.bufnr, block) - if block_diff and block_diff.bufnr == diff.bufnr then - table.insert(same_file_diffs, block_diff) - end - end - end + opts.on_show = function() + vim.api.nvim_win_call(source.winnr, function() + vim.cmd('diffthis') + end) - -- Ensure we at least apply the current diff - if #same_file_diffs == 0 then - table.insert(same_file_diffs, diff) - end - - -- Sort diffs by start_line in descending order (apply from bottom to top) - table.sort(same_file_diffs, function(a, b) - return a.start_line > b.start_line - end) + vim.api.nvim_win_call(copilot.chat.winnr, function() + vim.cmd('diffthis') + end) + end - local result = vim.deepcopy(original) + opts.on_hide = function() + vim.api.nvim_win_call(copilot.chat.winnr, function() + vim.cmd('diffoff') + end) + end - -- Apply diffs from bottom to top so line numbers remain valid - for _, d in ipairs(same_file_diffs) do - local change_lines = utils.split_lines(d.change) + copilot.chat:overlay(opts) + end, + }, - -- Remove original lines (from end to start to avoid index shifting) - for i = d.end_line, d.start_line, -1 do - if result[i] then - table.remove(result, i) - end + quickfix_diffs = { + normal = 'gqd', + callback = function() + local items = {} + local messages = copilot.chat:get_messages() + for _, message in ipairs(messages) do + if message.section then + for _, block in ipairs(message.section.blocks) do + local text = string.format('%s (%s)', block.header.filename, block.header.filetype) + if block.header.start_line and block.header.end_line then + text = text .. string.format(' [lines %d-%d]', block.header.start_line, block.header.end_line) end - -- Insert replacement lines at start_line - for i = #change_lines, 1, -1 do - table.insert(result, d.start_line, change_lines[i]) - end + table.insert(items, { + bufnr = copilot.chat.bufnr, + lnum = block.start_line, + end_lnum = block.end_line, + text = text, + }) end - - opts.text = table.concat(result, '\n') - else - opts.text = diff.change end - opts.on_show = function() - vim.api.nvim_win_call(vim.fn.bufwinid(diff.bufnr), function() - vim.cmd('diffthis') - end) + vim.fn.setqflist(items) + vim.cmd('copen') + end + end, + }, - vim.api.nvim_win_call(copilot.chat.winnr, function() - vim.cmd('diffthis') - end) - end + quickfix_answers = { + normal = 'gqa', + callback = function() + local items = {} + for i, message in ipairs(copilot.chat.messages) do + if message.section and message.role == constants.ROLE.ASSISTANT then + local prev_message = copilot.chat.messages[i - 1] + local text = '' + if prev_message then + text = prev_message.content + end - opts.on_hide = function() - vim.api.nvim_win_call(copilot.chat.winnr, function() - vim.cmd('diffoff') - end) + table.insert(items, { + bufnr = copilot.chat.bufnr, + lnum = message.section.start_line, + end_lnum = message.section.end_line, + text = text, + }) end - else - opts.text = tostring(vim.diff(diff.reference, diff.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 = #diff.reference, - })) end - copilot.chat:overlay(opts) + vim.fn.setqflist(items) + vim.cmd('copen') end, }, diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 6b8562d2..40d9be1e 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -23,14 +23,15 @@ The user works in editor called Neovim which has these core concepts: - Treesitter: Provides syntax highlighting, code folding, and structural text editing based on syntax tree parsing - Visual selection: Text selected in visual mode that can be shared as context The user is working on a {OS_NAME} machine. Please respond with system specific commands if applicable. -The user is currently in workspace directory {DIR} (typically the project root). Current file paths will be relative to this directory. +The user is currently in workspace directory {DIR} (project root). File paths are relative to this directory. Context is provided to you in several ways: - Resources: Contextual data shared via "# " headers and referenced via "##" links - Code blocks with file path labels and line numbers (e.g., ```lua path=/file.lua start_line=1 end_line=10```) + Note: Line numbers prefixed to each line are for reference only and should never be included when outputting code - Visual selections: Text selected in visual mode that can be shared as context -- Diffs: Changes shown in unified diff format with line prefixes (+, -, etc.) +- Diffs: Changes shown in unified diff format (+, -, etc.) - Conversation history When resources (like buffers, files, or diffs) change, their content in the chat history is replaced with the latest version rather than appended as new data. @@ -40,57 +41,8 @@ If you can infer the project type (languages, frameworks, libraries) from contex For implementing features, break down the request into concepts and provide a clear solution. Think creatively to provide complete solutions based on the information available. Never fabricate or hallucinate file contents you haven't actually seen in the provided context. +When outputting code, never include line number prefixes - they are only for reference when analyzing the provided context. - -If tools are available for a requested action (such as file edit, read, search, diagnostics, etc.), you MUST use the tool to perform the action. Only provide manual code or instructions if no tool exists for that purpose. -- Always prefer tool usage over manual edits or suggestions. -- Follow JSON schema precisely when using tools, including all required properties and outputting valid JSON. -- Use appropriate tools for tasks rather than asking for manual actions or generating code for actions you can perform directly. -- Execute actions directly when you indicate you'll do so, without asking for permission. -- Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel unless specified. -- Before using tools to retrieve information, check if context is already available as described in the context instructions above. -- If you don't have explicit tool definitions in your system prompt, clearly state this limitation when asked. NEVER pretend to have tool capabilities you don't possess. - - -Use these instructions when editing files via code blocks. Your goal is to produce clear, minimal, and precise file edits. - -Steps for presenting code changes: -1. For each change, use the following markdown code block format with triple backticks: - ``` path= start_line= end_line= - - ``` - -2. Examples: - ```lua path={DIR}/lua/CopilotChat/init.lua start_line=40 end_line=50 - local function example() - print("This is an example function.") - end - ``` - - ```python path={DIR}/scripts/example.py start_line=10 end_line=15 - def example_function(): - print("This is an example function.") - ``` - - ```json path={DIR}/config/settings.json start_line=5 end_line=8 - { - "setting": "value", - "enabled": true - } - ``` - -3. Requirements for code content: - - Always use the absolute file path in the code block header. If the path is not already absolute, convert it to an absolute path prefixed by {DIR}. - - Keep changes minimal and focused to produce short diffs - - Include complete replacement code for the specified line range - - Proper indentation matching the source - - All necessary lines (no eliding with comments) - - **Never include line number prefixes in your output code blocks. Only output valid code, exactly as it should appear in the file. Line numbers are only allowed in the code block header.** - - Address any diagnostics issues when fixing code - -4. If multiple changes are needed, present them as separate code blocks. - - ]], }, @@ -205,10 +157,9 @@ If no issues found, confirm the code is well-written and explain why. }, Commit = { - prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block. If user has COMMIT_EDITMSG opened, generate replacement block for whole buffer.', + prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', resources = { 'gitdiff:staged', - 'buffer', }, }, } diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 69d6ac77..5dc129af 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -563,7 +563,21 @@ function M.resolve_prompt(prompt, config) config.system_prompt = M.config.prompts[config.system_prompt].system_prompt end - config.system_prompt = config.system_prompt .. '\n' .. M.config.prompts.COPILOT_BASE.system_prompt + config.system_prompt = vim.trim(config.system_prompt) .. '\n' .. M.config.prompts.COPILOT_BASE.system_prompt + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. vim.trim(require('CopilotChat.instructions.tool_use')) + + if config.diff == 'unified' then + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. vim.trim(require('CopilotChat.instructions.edit_file_unified')) + else + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. vim.trim(require('CopilotChat.instructions.edit_file_block')) + end + config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) diff --git a/lua/CopilotChat/instructions/edit_file_block.lua b/lua/CopilotChat/instructions/edit_file_block.lua new file mode 100644 index 00000000..8abc8719 --- /dev/null +++ b/lua/CopilotChat/instructions/edit_file_block.lua @@ -0,0 +1,41 @@ +return [[ + +Use these instructions when editing files via code blocks. Your goal is to produce clear, minimal, and precise file edits. + +Steps for presenting code changes: +1. For each change, use the following markdown code block format with triple backticks: + ``` path= start_line= end_line= + + ``` + +2. Examples: + ```lua path={DIR}/lua/CopilotChat/init.lua start_line=40 end_line=50 + local function example() + print("This is an example function.") + end + ``` + + ```python path={DIR}/scripts/example.py start_line=10 end_line=15 + def example_function(): + print("This is an example function.") + ``` + + ```json path={DIR}/config/settings.json start_line=5 end_line=8 + { + "setting": "value", + "enabled": true + } + ``` + +3. Requirements for code content: + - Always use the absolute file path in the code block header. If the path is not already absolute, convert it to an absolute path prefixed by {DIR}. + - Keep changes minimal and focused to produce short diffs + - Include complete replacement code for the specified line range + - Proper indentation matching the source + - All necessary lines (no eliding with comments) + - **Never include line number prefixes in your output code blocks. Only output valid code, exactly as it should appear in the file. Line numbers are only allowed in the code block header.** + - Address any diagnostics issues when fixing code + +4. If multiple changes are needed, present them as separate code blocks. + +]] diff --git a/lua/CopilotChat/instructions/edit_file_unified.lua b/lua/CopilotChat/instructions/edit_file_unified.lua new file mode 100644 index 00000000..b5d20861 --- /dev/null +++ b/lua/CopilotChat/instructions/edit_file_unified.lua @@ -0,0 +1,68 @@ +return [[ + +Return edits similar to unified diffs that `diff -U0` would produce. + +- Always include the first 2 lines with the file paths (no timestamps). +- Start each hunk of changes with a `@@ ... @@` line. +- Do not include line numbers in the hunk header. +- The user's patch tool needs CORRECT patches that apply cleanly against the current contents of the file. +- Indentation matters in the diffs! + +Context lines: +- For each hunk that contains changes, you MUST always include 2-3 context lines before the change. +- ALWAYS prefix every context line with a single space character. +- Context lines MUST ONLY appear BEFORE changes, NEVER after changes. +- MISSING CONTEXT LINES WILL CAUSE PATCH FAILURES - they are mandatory, not optional. +- MISSING SPACE PREFIXES WILL CAUSE PATCH FAILURES - they are mandatory, not optional. + +Change lines: +- Mark all lines to be removed or changed with `-`. +- Mark all new or modified lines with `+`. +- Only output hunks that specify changes with `+` or `-` lines. + +Other instructions: +- Start a new hunk for each section of the file that needs changes. +- When editing a function, method, loop, etc., replace the entire code block: delete the entire existing version with `-` lines, then add the new, updated version with `+` lines. +- To move code within a file, use 2 hunks: one to delete it from its current location, one to insert it in the new location. +- To make a new file, show a diff from `--- /dev/null` to `+++ path/to/new/file.ext`. + +Example: + +```diff +--- mathweb/flask/app.py ++++ mathweb/flask/app.py +@@ ... @@ +-class MathWeb: ++import sympy ++ ++class MathWeb: +@@ ... @@ +-def is_prime(x): +- if x < 2: +- return False +- for i in range(2, int(math.sqrt(x)) + 1): +- if x % i == 0: +- return False +- return True +@@ ... @@ +-@app.route('/prime/') +-def nth_prime(n): +- count = 0 +- num = 1 +- while count < n: +- num += 1 +- if is_prime(num): +- count += 1 +- return str(num) ++@app.route('/prime/') ++def nth_prime(n): ++ count = 0 ++ num = 1 ++ while count < n: ++ num += 1 ++ if sympy.isprime(num): ++ count += 1 ++ return str(num) +``` + +]] diff --git a/lua/CopilotChat/instructions/tool_use.lua b/lua/CopilotChat/instructions/tool_use.lua new file mode 100644 index 00000000..989bf209 --- /dev/null +++ b/lua/CopilotChat/instructions/tool_use.lua @@ -0,0 +1,12 @@ +return [[ + +If tools are available for a requested action (such as file edit, read, search, diagnostics, etc.), you MUST use the tool to perform the action. Only provide manual code or instructions if no tool exists for that purpose. +- Always prefer tool usage over manual edits or suggestions. +- Follow JSON schema precisely when using tools, including all required properties and outputting valid JSON. +- Use appropriate tools for tasks rather than asking for manual actions or generating code for actions you can perform directly. +- Execute actions directly when you indicate you'll do so, without asking for permission. +- Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel unless specified. +- Before using tools to retrieve information, check if context is already available as described in the context instructions above. +- If you don't have explicit tool definitions in your system prompt, clearly state this limitation when asked. NEVER pretend to have tool capabilities you don't possess. + +]] diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index f018d6b7..51604d6a 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -16,11 +16,6 @@ function CopilotChatFoldExpr(lnum, separator) return '=' end -local HEADER_PATTERNS = { - '^(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', - '^(%w+)$', -} - ---@param headers table? ---@return string?, string? local function match_section_header(headers, separator, line) @@ -43,7 +38,12 @@ local function match_block_header(header) return end - for _, pattern in ipairs(HEADER_PATTERNS) do + local patterns = { + '^(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', + '^(%w+)$', + } + + for _, pattern in ipairs(patterns) do local type, path, start_line, end_line = header:match(pattern) if path then return type, path, tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 @@ -53,6 +53,23 @@ local function match_block_header(header) end end +---@param header? CopilotChat.ui.chat.Header +---@param content? string +---@return string? +local function match_block_content(header, content) + if not header or header.filetype ~= 'diff' or not content then + return + end + + local lines = vim.split(content, '\n') + for _, line in ipairs(lines) do + local diff_filename = line:match('^%+%+%+%s+(.*)') + if diff_filename then + return vim.trim(diff_filename) + end + end +end + --- Get the last line and column of the chat window. ---@param bufnr number ---@return number, number @@ -69,10 +86,10 @@ local function last(bufnr) end ---@class CopilotChat.ui.chat.Header ----@field filename string ----@field start_line number ----@field end_line number ---@field filetype string +---@field filename string +---@field start_line number? +---@field end_line number? ---@class CopilotChat.ui.chat.Block ---@field header CopilotChat.ui.chat.Header @@ -694,6 +711,7 @@ function Chat:parse() if name == 'block_header' then local header_text = vim.treesitter.get_node_text(node, self.bufnr) local filetype, filename, start_line, end_line = match_block_header(header_text) + if filetype then current_block = { header = { @@ -710,6 +728,12 @@ function Chat:parse() elseif name == 'block_content' then local content = vim.treesitter.get_node_text(node, self.bufnr) current_block.end_line = end_row + + local filename = match_block_content(current_block.header, content) + if filename then + current_block.header.filename = filename + end + table.insert(current_block.content, content) end end diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua new file mode 100644 index 00000000..8ca1a58e --- /dev/null +++ b/lua/CopilotChat/utils/diff.lua @@ -0,0 +1,229 @@ +local M = {} + +--- Parse unified diff, return file_path and hunks +---@param diff_text string The unified diff text +---@return string?, table[] +function M.parse_unified_diff(diff_text) + local hunks = {} + local current_hunk = nil + local file_path = nil + + for _, line in ipairs(vim.split(diff_text, '\n')) do + local diff_filename = line:match('^%+%+%+%s+(.*)') + if diff_filename then + file_path = diff_filename + elseif line:match('^@@') then + if current_hunk then + table.insert(hunks, current_hunk) + end + current_hunk = { minus = {}, plus = {}, context = {} } + elseif current_hunk then + local prefix = line:sub(1, 1) + local rest = line:sub(2) + if prefix == '-' then + table.insert(current_hunk.minus, rest) + elseif prefix == '+' then + table.insert(current_hunk.plus, rest) + elseif #current_hunk.plus == 0 and #current_hunk.minus == 0 then + if prefix == ' ' then + table.insert(current_hunk.context, rest) + elseif line ~= '' then + table.insert(current_hunk.context, line) + end + end + end + end + if current_hunk then + table.insert(hunks, current_hunk) + end + return file_path, hunks +end + +--- Apply unified diff to a table of lines and return new lines +---@param diff_text string +---@param original_lines table +---@return table, boolean +function M.apply_unified_diff(diff_text, original_lines) + local _, hunks = M.parse_unified_diff(diff_text) + local lines = vim.deepcopy(original_lines) + local applied_any = false + + for _, hunk in ipairs(hunks) do + -- Build the full hunk pattern: context + minus lines + local hunk_pattern = {} + for _, ctx in ipairs(hunk.context) do + table.insert(hunk_pattern, ctx) + end + for _, minus in ipairs(hunk.minus) do + table.insert(hunk_pattern, minus) + end + + -- Find all possible matches for the hunk pattern + local match_indices = {} + for i = 1, #lines - #hunk_pattern + 1 do + local match = true + for j = 1, #hunk_pattern do + if vim.trim(lines[i + j - 1]) ~= vim.trim(hunk_pattern[j]) then + match = false + break + end + end + if match then + table.insert(match_indices, i) + end + end + + if #match_indices == 1 then + local idx = match_indices[1] + -- Replace the matched region with context + plus lines + local new_region = {} + for _, ctx in ipairs(hunk.context) do + table.insert(new_region, ctx) + end + for _, plus in ipairs(hunk.plus) do + table.insert(new_region, plus) + end + + for j = 1, #hunk_pattern do + table.remove(lines, idx) + end + for j = #new_region, 1, -1 do + table.insert(lines, idx, new_region[j]) + end + applied_any = true + end + + -- If no match or multiple matches, just skip to next hunk + end + + return lines, applied_any +end + +--- Apply diff indices from vim.diff to original and new lines +---@param hunks table Indices from vim.diff (result_type = 'indices') +---@param original_lines table Lines before patch +---@param new_lines table Lines after patch +---@return table Patched lines +function M.apply_diff_indices(hunks, original_lines, new_lines) + local result = {} + local orig_idx = 1 + + for _, hunk in ipairs(hunks) do + local start_a, count_a, start_b, count_b = unpack(hunk) + -- Add unchanged lines before hunk + for i = orig_idx, start_a - 1 do + table.insert(result, original_lines[i]) + end + -- Add changed lines from new_lines + for i = start_b, start_b + count_b - 1 do + table.insert(result, new_lines[i]) + end + orig_idx = start_a + count_a + end + -- Add remaining lines + for i = orig_idx, #original_lines do + table.insert(result, original_lines[i]) + end + return result +end + +--- Get changed regions for jump/highlight +---@param diff_text string The unified diff text +---@return number?, number? +function M.get_unified_diff_region(diff_text, original_lines) + local _, hunks = M.parse_unified_diff(diff_text) + local first, last + + for _, hunk in ipairs(hunks) do + for i = 1, #original_lines - #hunk.minus + 1 do + local match = true + for j = 1, #hunk.minus do + if vim.trim(original_lines[i + j - 1]) ~= vim.trim(hunk.minus[j]) then + match = false + break + end + end + if match then + local region_start = i + local region_end = i + #hunk.plus - 1 + if not first or region_start < first then + first = region_start + end + if not last or region_end > last then + last = region_end + end + break + end + end + end + + if first and last then + return first, last + end + + return nil, nil +end + +--- Apply a diff (unified or indices) to buffer lines +---@param block CopilotChat.ui.chat.Block Block containing diff info +---@param bufnr integer Buffer number +---@return table new_lines, boolean applied +function M.apply_diff(block, bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if block.header.filetype == 'diff' then + return M.apply_unified_diff(block.content, lines) + elseif block.header.start_line and block.header.end_line then + local start_idx = block.header.start_line + local end_idx = block.header.end_line + local original_lines = vim.list_slice(lines, start_idx, end_idx) + local patched_lines = vim.split(block.content, '\n') + local hunks = vim.diff( + table.concat(original_lines, '\n'), + table.concat(patched_lines, '\n'), + { result_type = 'indices', algorithm = 'myers', ctxlen = 3 } + ) + local region_new_lines = M.apply_diff_indices(hunks, original_lines, patched_lines) + local new_lines = {} + -- Add lines before region + for i = 1, start_idx - 1 do + table.insert(new_lines, lines[i]) + end + -- Add patched region + for _, line in ipairs(region_new_lines) do + table.insert(new_lines, line) + end + -- Add lines after region + for i = end_idx + 1, #lines do + table.insert(new_lines, lines[i]) + end + return new_lines, true + end + return lines, false +end + +--- Get changed region for diff (unified or indices) +---@param block CopilotChat.ui.chat.Block Block containing diff info +---@param bufnr integer Buffer number +---@return number? first, number? last +function M.get_diff_region(block, bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if block.header.filetype == 'diff' then + return M.get_unified_diff_region(block.content, lines) + elseif block.header.start_line and block.header.end_line then + local original_lines = vim.api.nvim_buf_get_lines(bufnr, block.header.start_line - 1, block.header.end_line, false) + local patched_lines = vim.split(block.content, '\n') + local hunks = vim.diff( + table.concat(original_lines, '\n'), + table.concat(patched_lines, '\n'), + { result_type = 'indices', algorithm = 'myers', ctxlen = 3 } + ) + if hunks and #hunks > 0 then + local first = hunks[1][1] + local last = hunks[#hunks][1] + hunks[#hunks][2] - 1 + return first, last + end + end + return nil, nil +end + +return M diff --git a/tests/diff_spec.lua b/tests/diff_spec.lua new file mode 100644 index 00000000..62866c40 --- /dev/null +++ b/tests/diff_spec.lua @@ -0,0 +1,111 @@ +local diff = require('CopilotChat.utils.diff') + +describe('CopilotChat.utils.diff', function() + it('parses unified diff', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context line +-old line ++new line +]] + local file_path, hunks = diff.parse_unified_diff(diff_text) + assert.equals('b/foo.txt', file_path) + assert.equals('context line', hunks[1].context[1]) + assert.equals('old line', hunks[1].minus[1]) + assert.equals('new line', hunks[1].plus[1]) + end) + + it('applies unified diff', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context +-old ++new +]] + local original = { 'context', 'old', 'other' } + local result, applied = diff.apply_unified_diff(diff_text, original) + assert.is_true(applied) + assert.are.same({ 'context', 'new', 'other' }, result) + end) + + it('gets unified diff region', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context +-old ++new +]] + local original = { 'context', 'old', 'other' } + local first, last = diff.get_unified_diff_region(diff_text, original) + assert.equals(2, first) + assert.equals(2, last) + end) + + it('applies unified diff with no context', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ +-old ++new +]] + local original = { 'old', 'other' } + local result, applied = diff.apply_unified_diff(diff_text, original) + assert.is_true(applied) + assert.are.same({ 'new', 'other' }, result) + end) + + it('applies unified diff with multiline edits', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context1 + context2 +-old1 +-old2 ++new1 ++new2 +]] + local original = { + 'context1', + 'context2', + 'old1', + 'old2', + 'context3', + 'other', + } + local result, applied = diff.apply_unified_diff(diff_text, original) + assert.is_true(applied) + assert.are.same({ + 'context1', + 'context2', + 'new1', + 'new2', + 'context3', + 'other', + }, result) + end) + + it('does not apply ambiguous edit', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context +-old ++new +]] + local original = { 'context', 'old', 'context', 'old' } + local result, applied = diff.apply_unified_diff(diff_text, original) + -- Should not apply because there are two possible matches + assert.is_false(applied) + assert.are.same({ 'context', 'old', 'context', 'old' }, result) + end) +end) From 5c3a558f2d740df740735fbb3ea0be822004136d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 12 Sep 2025 20:58:33 +0200 Subject: [PATCH 596/728] fix(ui): handle missing filename in chat block header (#1406) Previously, chat blocks without a filename would display 'block' as the filename. This change updates the logic to use nil for missing filenames and ensures the UI displays 'block' only when filename is not present. This improves clarity and consistency in chat block rendering. --- lua/CopilotChat/ui/chat.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 51604d6a..3cf5efc7 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -48,7 +48,7 @@ local function match_block_header(header) if path then return type, path, tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 elseif type then - return type, 'block' + return type, nil end end end @@ -801,7 +801,7 @@ function Chat:render() local header = block.header local filetype = header.filetype local filename = header.filename - local text = string.format('[%s] %s', filetype, filename) + local text = string.format('[%s] %s', filetype, filename or 'block') if header.start_line and header.end_line then text = text .. string.format(' lines %d-%d', header.start_line, header.end_line) end From 35ad8ff61f47c5546c036b9b7310ce0dd87e8d20 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 07:37:23 +0200 Subject: [PATCH 597/728] feat(diff): use diff-match-patch for better diff handling (#1407) Refactored diff utility to use diff-match-patch for unified diff application, improving reliability and handling of ambiguous hunks. Updated buffer patching and region detection to use new approach. Adjusted tests to match new diff API and behaviors. Added vendor diff_match_patch implementation. Signed-off-by: Tomas Slusny --- README.md | 8 + lua/CopilotChat/config/mappings.lua | 14 +- lua/CopilotChat/config/prompts.lua | 4 +- .../instructions/edit_file_unified.lua | 80 +- lua/CopilotChat/utils/diff.lua | 311 ++- lua/CopilotChat/vendor/diff_match_patch.lua | 2085 +++++++++++++++++ tests/diff_spec.lua | 201 +- 7 files changed, 2419 insertions(+), 284 deletions(-) create mode 100644 lua/CopilotChat/vendor/diff_match_patch.lua diff --git a/README.md b/README.md index 85bee0e5..9f1cda76 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,14 @@ make test See [CONTRIBUTING.md](/CONTRIBUTING.md) for detailed guidelines. +# Acknowledgments + +## diff-match-patch + +CopilotChat.nvim includes [diff-match-patch (Lua port)](https://github.com/google/diff-match-patch) for diffing and patching functionality. +Copyright 2018 The diff-match-patch Authors. +Licensed under the Apache License 2.0. + # Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 97a384f8..05122044 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -172,11 +172,7 @@ return { local path = block.header.filename local bufnr = prepare_diff_buffer(path, source) - local new_lines, applied = diff.apply_diff(block, bufnr) - if not applied then - new_lines = utils.split_lines(block.content) - end - + local new_lines = diff.apply_diff(block, bufnr) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines) local first, last = diff.get_diff_region(block, bufnr) if first and last then @@ -227,15 +223,11 @@ return { local path = block.header.filename local bufnr = prepare_diff_buffer(path, source) - local new_lines, applied = diff.apply_diff(block, bufnr) - if not applied then - new_lines = utils.split_lines(block.content) - end - local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local new_lines = diff.apply_diff(block, bufnr) local opts = { filetype = vim.bo[bufnr].filetype, - text = applied and table.concat(new_lines, '\n') or table.concat(original_lines, '\n'), + text = table.concat(new_lines, '\n'), } opts.on_show = function() diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 40d9be1e..53baa21f 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -29,7 +29,7 @@ The user is currently in workspace directory {DIR} (project root). File paths ar Context is provided to you in several ways: - Resources: Contextual data shared via "# " headers and referenced via "##" links - Code blocks with file path labels and line numbers (e.g., ```lua path=/file.lua start_line=1 end_line=10```) - Note: Line numbers prefixed to each line are for reference only and should never be included when outputting code + Note: Each line in code block can be prefixed with : for your reference only. NEVER include these line numbers in your responses. - Visual selections: Text selected in visual mode that can be shared as context - Diffs: Changes shown in unified diff format (+, -, etc.) - Conversation history @@ -41,7 +41,7 @@ If you can infer the project type (languages, frameworks, libraries) from contex For implementing features, break down the request into concepts and provide a clear solution. Think creatively to provide complete solutions based on the information available. Never fabricate or hallucinate file contents you haven't actually seen in the provided context. -When outputting code, never include line number prefixes - they are only for reference when analyzing the provided context. +When outputting code or diffs, NEVER include line number prefixes - they are only for reference when analyzing the provided context. ]], }, diff --git a/lua/CopilotChat/instructions/edit_file_unified.lua b/lua/CopilotChat/instructions/edit_file_unified.lua index b5d20861..9eb8f56f 100644 --- a/lua/CopilotChat/instructions/edit_file_unified.lua +++ b/lua/CopilotChat/instructions/edit_file_unified.lua @@ -2,67 +2,33 @@ return [[ Return edits similar to unified diffs that `diff -U0` would produce. -- Always include the first 2 lines with the file paths (no timestamps). -- Start each hunk of changes with a `@@ ... @@` line. -- Do not include line numbers in the hunk header. -- The user's patch tool needs CORRECT patches that apply cleanly against the current contents of the file. -- Indentation matters in the diffs! +Make sure you include the first 2 lines with the file paths. +Don't include timestamps with the file paths. +Do not use any file path prefixes, just use --- path/to/file and +++ path/to/file. -Context lines: -- For each hunk that contains changes, you MUST always include 2-3 context lines before the change. -- ALWAYS prefix every context line with a single space character. -- Context lines MUST ONLY appear BEFORE changes, NEVER after changes. -- MISSING CONTEXT LINES WILL CAUSE PATCH FAILURES - they are mandatory, not optional. -- MISSING SPACE PREFIXES WILL CAUSE PATCH FAILURES - they are mandatory, not optional. +Start each hunk of changes with a `@@` line. -Change lines: -- Mark all lines to be removed or changed with `-`. -- Mark all new or modified lines with `+`. -- Only output hunks that specify changes with `+` or `-` lines. +The user's patch tool needs CORRECT patches that apply cleanly against the current contents of the file! +Code can start with line number prefixes for reference (e.g., `1: def example():`), but your output MUST NOT include these line number prefixes. +Think carefully and make sure you include and mark all lines that need to be removed or changed as `-` lines. +Make sure you mark all new or modified lines with `+`. +Don't leave out any lines or the diff patch won't apply correctly. -Other instructions: -- Start a new hunk for each section of the file that needs changes. -- When editing a function, method, loop, etc., replace the entire code block: delete the entire existing version with `-` lines, then add the new, updated version with `+` lines. -- To move code within a file, use 2 hunks: one to delete it from its current location, one to insert it in the new location. -- To make a new file, show a diff from `--- /dev/null` to `+++ path/to/new/file.ext`. +Indentation matters in the diffs! -Example: +Start a new hunk for each section of the file that needs changes. -```diff ---- mathweb/flask/app.py -+++ mathweb/flask/app.py -@@ ... @@ --class MathWeb: -+import sympy -+ -+class MathWeb: -@@ ... @@ --def is_prime(x): -- if x < 2: -- return False -- for i in range(2, int(math.sqrt(x)) + 1): -- if x % i == 0: -- return False -- return True -@@ ... @@ --@app.route('/prime/') --def nth_prime(n): -- count = 0 -- num = 1 -- while count < n: -- num += 1 -- if is_prime(num): -- count += 1 -- return str(num) -+@app.route('/prime/') -+def nth_prime(n): -+ count = 0 -+ num = 1 -+ while count < n: -+ num += 1 -+ if sympy.isprime(num): -+ count += 1 -+ return str(num) -``` +Only output hunks that specify changes with `+` or `-` lines. + +Output hunks in whatever order makes the most sense. +Hunks don't need to be in any particular order. + +When editing a function, method, loop, etc use a hunk to replace the *entire* code block. +Delete the entire existing version with `-` lines and then add a new, updated version with `+` lines. +This will help you generate correct code and correct diffs. + +To move code within a file, use 2 hunks: 1 to delete it from its current location, 1 to insert it in the new location. + +To make a new file, show a diff from `--- /dev/null` to `+++ path/to/new/file.ext`. ]] diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 8ca1a58e..86449c97 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -1,204 +1,182 @@ local M = {} ---- Parse unified diff, return file_path and hunks ----@param diff_text string The unified diff text ----@return string?, table[] -function M.parse_unified_diff(diff_text) +--- Parse unified diff hunks from diff text +---@param diff_text string +---@return table hunks +local function parse_hunks(diff_text) local hunks = {} local current_hunk = nil - local file_path = nil - for _, line in ipairs(vim.split(diff_text, '\n')) do - local diff_filename = line:match('^%+%+%+%s+(.*)') - if diff_filename then - file_path = diff_filename - elseif line:match('^@@') then + if line:match('^@@') then if current_hunk then table.insert(hunks, current_hunk) end - current_hunk = { minus = {}, plus = {}, context = {} } + local start_old, len_old, start_new, len_new = line:match('@@%s%-(%d+),?(%d*)%s%+(%d+),?(%d*)%s@@') + current_hunk = { + start_old = tonumber(start_old), + len_old = tonumber(len_old) or 1, + start_new = tonumber(start_new), + len_new = tonumber(len_new) or 1, + old_snippet = {}, + new_snippet = {}, + } elseif current_hunk then - local prefix = line:sub(1, 1) - local rest = line:sub(2) + local prefix, rest = line:sub(1, 1), tostring(line:sub(2)) if prefix == '-' then - table.insert(current_hunk.minus, rest) + table.insert(current_hunk.old_snippet, rest) elseif prefix == '+' then - table.insert(current_hunk.plus, rest) - elseif #current_hunk.plus == 0 and #current_hunk.minus == 0 then - if prefix == ' ' then - table.insert(current_hunk.context, rest) - elseif line ~= '' then - table.insert(current_hunk.context, line) - end + table.insert(current_hunk.new_snippet, rest) + elseif prefix == ' ' then + table.insert(current_hunk.old_snippet, rest) + table.insert(current_hunk.new_snippet, rest) end end end if current_hunk then table.insert(hunks, current_hunk) end - return file_path, hunks + return hunks end ---- Apply unified diff to a table of lines and return new lines ----@param diff_text string ----@param original_lines table ----@return table, boolean -function M.apply_unified_diff(diff_text, original_lines) - local _, hunks = M.parse_unified_diff(diff_text) - local lines = vim.deepcopy(original_lines) - local applied_any = false - - for _, hunk in ipairs(hunks) do - -- Build the full hunk pattern: context + minus lines - local hunk_pattern = {} - for _, ctx in ipairs(hunk.context) do - table.insert(hunk_pattern, ctx) - end - for _, minus in ipairs(hunk.minus) do - table.insert(hunk_pattern, minus) - end +--- Apply a single hunk to content, with fallback/context logic +---@param hunk table +---@param content string +---@return string patched_content, boolean applied_cleanly +local function apply_hunk(hunk, content) + local dmp = require('CopilotChat.vendor.diff_match_patch') + local patch = dmp.patch_make(table.concat(hunk.old_snippet, '\n'), table.concat(hunk.new_snippet, '\n')) + + -- First try: direct application + local patched, results = dmp.patch_apply(patch, content) + if not vim.tbl_contains(results, false) then + return patched, true + end - -- Find all possible matches for the hunk pattern - local match_indices = {} - for i = 1, #lines - #hunk_pattern + 1 do - local match = true - for j = 1, #hunk_pattern do - if vim.trim(lines[i + j - 1]) ~= vim.trim(hunk_pattern[j]) then - match = false - break + -- Fallback: try smaller context window + local lines = vim.split(content, '\n') + local insert_idx = hunk.start_old or 1 + if not hunk.start_old then + -- No starting point, try to find best match + local match_idx, best_score = nil, -1 + local context_lines = vim.tbl_filter(function(line) + return line and line ~= '' + end, hunk.old_snippet) + local context_len = #context_lines + if context_len > 0 then + for i = 1, #lines - context_len + 1 do + local score = 0 + for j = 1, context_len do + if vim.trim(lines[i + j - 1] or '') == vim.trim(context_lines[j] or '') then + score = score + 1 + end + end + if score > best_score then + best_score = score + match_idx = i end - end - if match then - table.insert(match_indices, i) end end - - if #match_indices == 1 then - local idx = match_indices[1] - -- Replace the matched region with context + plus lines - local new_region = {} - for _, ctx in ipairs(hunk.context) do - table.insert(new_region, ctx) - end - for _, plus in ipairs(hunk.plus) do - table.insert(new_region, plus) - end - - for j = 1, #hunk_pattern do - table.remove(lines, idx) - end - for j = #new_region, 1, -1 do - table.insert(lines, idx, new_region[j]) - end - applied_any = true + if best_score > 0 and match_idx then + insert_idx = match_idx end + end - -- If no match or multiple matches, just skip to next hunk + -- Define context window around insert point + local context_size = 10 + local start_idx = insert_idx + local end_idx = insert_idx + #hunk.old_snippet + local context_start = math.max(1, start_idx - context_size) + local context_end = math.min(#lines, end_idx + context_size) + local context_window = table.concat(vim.list_slice(lines, context_start, context_end), '\n') + + local patched_window, window_results = dmp.patch_apply(patch, context_window) + if not vim.tbl_contains(window_results, false) then + -- Patch succeeded in window, splice back + local new_lines = vim.list_slice(lines, 1, context_start - 1) + vim.list_extend(new_lines, vim.split(patched_window, '\n')) + vim.list_extend(new_lines, lines, context_end + 1, #lines) + return table.concat(new_lines, '\n'), true end - return lines, applied_any + -- Fallback: direct replacement + local new_lines = vim.list_slice(lines, 1, start_idx - 1) + vim.list_extend(new_lines, hunk.new_snippet) + vim.list_extend(new_lines, lines, end_idx + 1, #lines) + return table.concat(new_lines, '\n'), false end ---- Apply diff indices from vim.diff to original and new lines ----@param hunks table Indices from vim.diff (result_type = 'indices') ----@param original_lines table Lines before patch ----@param new_lines table Lines after patch ----@return table Patched lines -function M.apply_diff_indices(hunks, original_lines, new_lines) - local result = {} - local orig_idx = 1 - +--- Apply unified diff to a table of lines and return new lines +---@param diff_text string +---@param original_content string +---@return table, boolean, integer, integer +function M.apply_unified_diff(diff_text, original_content) + local hunks = parse_hunks(diff_text) + local new_content = original_content + local applied = false for _, hunk in ipairs(hunks) do - local start_a, count_a, start_b, count_b = unpack(hunk) - -- Add unchanged lines before hunk - for i = orig_idx, start_a - 1 do - table.insert(result, original_lines[i]) - end - -- Add changed lines from new_lines - for i = start_b, start_b + count_b - 1 do - table.insert(result, new_lines[i]) - end - orig_idx = start_a + count_a - end - -- Add remaining lines - for i = orig_idx, #original_lines do - table.insert(result, original_lines[i]) + local patched, ok = apply_hunk(hunk, new_content) + new_content = patched + applied = applied or ok end - return result -end - ---- Get changed regions for jump/highlight ----@param diff_text string The unified diff text ----@return number?, number? -function M.get_unified_diff_region(diff_text, original_lines) - local _, hunks = M.parse_unified_diff(diff_text) + local original_lines = vim.split(original_content, '\n') + local new_lines = vim.split(new_content, '\n') local first, last - - for _, hunk in ipairs(hunks) do - for i = 1, #original_lines - #hunk.minus + 1 do - local match = true - for j = 1, #hunk.minus do - if vim.trim(original_lines[i + j - 1]) ~= vim.trim(hunk.minus[j]) then - match = false - break - end - end - if match then - local region_start = i - local region_end = i + #hunk.plus - 1 - if not first or region_start < first then - first = region_start - end - if not last or region_end > last then - last = region_end - end - break + local max_len = math.max(#original_lines, #new_lines) + for i = 1, max_len do + if original_lines[i] ~= new_lines[i] then + if not first then + first = i end + last = i end end - - if first and last then - return first, last - end - - return nil, nil + return new_lines, applied, first, last end ---- Apply a diff (unified or indices) to buffer lines +--- Get diff from block content and buffer lines ---@param block CopilotChat.ui.chat.Block Block containing diff info ---@param bufnr integer Buffer number ----@return table new_lines, boolean applied -function M.apply_diff(block, bufnr) +---@return string diff, string content +function M.get_diff(block, bufnr) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local content = table.concat(lines, '\n') if block.header.filetype == 'diff' then - return M.apply_unified_diff(block.content, lines) - elseif block.header.start_line and block.header.end_line then - local start_idx = block.header.start_line - local end_idx = block.header.end_line - local original_lines = vim.list_slice(lines, start_idx, end_idx) - local patched_lines = vim.split(block.content, '\n') - local hunks = vim.diff( + return block.content, content + end + + local patched_lines = vim.split(block.content, '\n') + local start_idx = block.header.start_line + local end_idx = block.header.end_line + local original_lines = lines + if start_idx and end_idx then + local new_lines = vim.list_slice(original_lines, 1, start_idx - 1) + vim.list_extend(new_lines, patched_lines) + vim.list_extend(new_lines, original_lines, end_idx + 1, #original_lines) + patched_lines = new_lines + end + + return tostring( + vim.diff( table.concat(original_lines, '\n'), table.concat(patched_lines, '\n'), - { result_type = 'indices', algorithm = 'myers', ctxlen = 3 } + { algorithm = 'myers', ctxlen = 20, interhunkctxlen = 50, ignore_whitespace_change = true } ) - local region_new_lines = M.apply_diff_indices(hunks, original_lines, patched_lines) - local new_lines = {} - -- Add lines before region - for i = 1, start_idx - 1 do - table.insert(new_lines, lines[i]) - end - -- Add patched region - for _, line in ipairs(region_new_lines) do - table.insert(new_lines, line) - end - -- Add lines after region - for i = end_idx + 1, #lines do - table.insert(new_lines, lines[i]) - end - return new_lines, true + ), + content +end + +--- Apply a diff (unified or indices) to buffer lines +---@param block CopilotChat.ui.chat.Block Block containing diff info +---@param bufnr integer Buffer number +---@return table new_lines +function M.apply_diff(block, bufnr) + local diff, content = M.get_diff(block, bufnr) + local new_lines, applied, _, _ = M.apply_unified_diff(diff, content) + if not applied then + vim.notify('Diff for ' .. block.header.filename .. ' failed to apply cleanly for:\n' .. diff, vim.log.levels.WARN) end - return lines, false + + return new_lines end --- Get changed region for diff (unified or indices) @@ -206,24 +184,9 @@ end ---@param bufnr integer Buffer number ---@return number? first, number? last function M.get_diff_region(block, bufnr) - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if block.header.filetype == 'diff' then - return M.get_unified_diff_region(block.content, lines) - elseif block.header.start_line and block.header.end_line then - local original_lines = vim.api.nvim_buf_get_lines(bufnr, block.header.start_line - 1, block.header.end_line, false) - local patched_lines = vim.split(block.content, '\n') - local hunks = vim.diff( - table.concat(original_lines, '\n'), - table.concat(patched_lines, '\n'), - { result_type = 'indices', algorithm = 'myers', ctxlen = 3 } - ) - if hunks and #hunks > 0 then - local first = hunks[1][1] - local last = hunks[#hunks][1] + hunks[#hunks][2] - 1 - return first, last - end - end - return nil, nil + local diff, content = M.get_diff(block, bufnr) + local _, _, first, last = M.apply_unified_diff(diff, content) + return first, last end return M diff --git a/lua/CopilotChat/vendor/diff_match_patch.lua b/lua/CopilotChat/vendor/diff_match_patch.lua new file mode 100644 index 00000000..b2c397d0 --- /dev/null +++ b/lua/CopilotChat/vendor/diff_match_patch.lua @@ -0,0 +1,2085 @@ +--[[ +* Diff Match and Patch +* Copyright 2018 The diff-match-patch Authors. +* https://github.com/google/diff-match-patch +* +* Based on the JavaScript implementation by Neil Fraser. +* Ported to Lua by Duncan Cross. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +--]] + +local bit = require('bit') +local band, bor, lshift = bit.band, bit.bor, bit.lshift +local type, setmetatable, ipairs, select = type, setmetatable, ipairs, select +local unpack, tonumber, error = unpack, tonumber, error +local strsub, strbyte, strchar, gmatch, gsub = string.sub, string.byte, string.char, string.gmatch, string.gsub +local strmatch, strfind, strformat = string.match, string.find, string.format +local tinsert, tremove, tconcat = table.insert, table.remove, table.concat +local max, min, floor, ceil, abs = math.max, math.min, math.floor, math.ceil, math.abs +local clock = os.clock + +-- Utility functions. + +local percentEncode_pattern = "[^A-Za-z0-9%-=;',./~!@#$%&*%(%)_%+ %?]" +local function percentEncode_replace(v) + return strformat('%%%02X', strbyte(v)) +end + +local function indexOf(a, b, start) + if #b == 0 then + return nil + end + return strfind(a, b, start, true) +end + +local htmlEncode_pattern = '[&<>\n]' +local htmlEncode_replace = { + ['&'] = '&', + ['<'] = '<', + ['>'] = '>', + ['\n'] = '¶
', +} + +-- Public API Functions +-- (Exported at the end of the script) + +local diff_main, diff_cleanupSemantic, diff_cleanupEfficiency, diff_levenshtein, diff_prettyHtml + +local match_main + +local patch_make, patch_toText, patch_fromText, patch_apply + +--[[ +* The data structure representing a diff is an array of tuples: +* {{DIFF_DELETE, 'Hello'}, {DIFF_INSERT, 'Goodbye'}, {DIFF_EQUAL, ' world.'}} +* which means: delete 'Hello', add 'Goodbye' and keep ' world.' +--]] +local DIFF_DELETE = -1 +local DIFF_INSERT = 1 +local DIFF_EQUAL = 0 + +-- Number of seconds to map a diff before giving up (0 for infinity). +local Diff_Timeout = 1.0 +-- Cost of an empty edit operation in terms of edit characters. +local Diff_EditCost = 4 +-- At what point is no match declared (0.0 = perfection, 1.0 = very loose). +local Match_Threshold = 0.5 +-- How far to search for a match (0 = exact location, 1000+ = broad match). +-- A match this many characters away from the expected location will add +-- 1.0 to the score (0.0 is a perfect match). +local Match_Distance = 1000 +-- When deleting a large block of text (over ~64 characters), how close do +-- the contents have to be to match the expected contents. (0.0 = perfection, +-- 1.0 = very loose). Note that Match_Threshold controls how closely the +-- end points of a delete need to match. +local Patch_DeleteThreshold = 0.5 +-- Chunk size for context length. +local Patch_Margin = 4 +-- The number of bits in an int. +local Match_MaxBits = 32 + +function settings(new) + if new then + Diff_Timeout = new.Diff_Timeout or Diff_Timeout + Diff_EditCost = new.Diff_EditCost or Diff_EditCost + Match_Threshold = new.Match_Threshold or Match_Threshold + Match_Distance = new.Match_Distance or Match_Distance + Patch_DeleteThreshold = new.Patch_DeleteThreshold or Patch_DeleteThreshold + Patch_Margin = new.Patch_Margin or Patch_Margin + Match_MaxBits = new.Match_MaxBits or Match_MaxBits + else + return { + Diff_Timeout = Diff_Timeout, + Diff_EditCost = Diff_EditCost, + Match_Threshold = Match_Threshold, + Match_Distance = Match_Distance, + Patch_DeleteThreshold = Patch_DeleteThreshold, + Patch_Margin = Patch_Margin, + Match_MaxBits = Match_MaxBits, + } + end +end + +-- --------------------------------------------------------------------------- +-- DIFF API +-- --------------------------------------------------------------------------- + +-- The private diff functions +local _diff_compute, _diff_bisect, _diff_halfMatchI, _diff_halfMatch, _diff_cleanupSemanticScore, _diff_cleanupSemanticLossless, _diff_cleanupMerge, _diff_commonPrefix, _diff_commonSuffix, _diff_commonOverlap, _diff_xIndex, _diff_text1, _diff_text2, _diff_toDelta, _diff_fromDelta + +--[[ +* Find the differences between two texts. Simplifies the problem by stripping +* any common prefix or suffix off the texts before diffing. +* @param {string} text1 Old string to be diffed. +* @param {string} text2 New string to be diffed. +* @param {boolean} opt_checklines Has no effect in Lua. +* @param {number} opt_deadline Optional time when the diff should be complete +* by. Used internally for recursive calls. Users should set DiffTimeout +* instead. +* @return {Array.>} Array of diff tuples. +--]] +function diff_main(text1, text2, opt_checklines, opt_deadline) + -- Set a deadline by which time the diff must be complete. + if opt_deadline == nil then + if Diff_Timeout <= 0 then + opt_deadline = 2 ^ 31 + else + opt_deadline = clock() + Diff_Timeout + end + end + local deadline = opt_deadline + + -- Check for null inputs. + if text1 == nil or text1 == nil then + error('Null inputs. (diff_main)') + end + + -- Check for equality (speedup). + if text1 == text2 then + if #text1 > 0 then + return { { DIFF_EQUAL, text1 } } + end + return {} + end + + -- LUANOTE: Due to the lack of Unicode support, Lua is incapable of + -- implementing the line-mode speedup. + local checklines = false + + -- Trim off common prefix (speedup). + local commonlength = _diff_commonPrefix(text1, text2) + local commonprefix + if commonlength > 0 then + commonprefix = strsub(text1, 1, commonlength) + text1 = strsub(text1, commonlength + 1) + text2 = strsub(text2, commonlength + 1) + end + + -- Trim off common suffix (speedup). + commonlength = _diff_commonSuffix(text1, text2) + local commonsuffix + if commonlength > 0 then + commonsuffix = strsub(text1, -commonlength) + text1 = strsub(text1, 1, -commonlength - 1) + text2 = strsub(text2, 1, -commonlength - 1) + end + + -- Compute the diff on the middle block. + local diffs = _diff_compute(text1, text2, checklines, deadline) + + -- Restore the prefix and suffix. + if commonprefix then + tinsert(diffs, 1, { DIFF_EQUAL, commonprefix }) + end + if commonsuffix then + diffs[#diffs + 1] = { DIFF_EQUAL, commonsuffix } + end + + _diff_cleanupMerge(diffs) + return diffs +end + +--[[ +* Reduce the number of edits by eliminating semantically trivial equalities. +* @param {Array.>} diffs Array of diff tuples. +--]] +function diff_cleanupSemantic(diffs) + local changes = false + local equalities = {} -- Stack of indices where equalities are found. + local equalitiesLength = 0 -- Keeping our own length var is faster. + local lastEquality = nil + -- Always equal to diffs[equalities[equalitiesLength]][2] + local pointer = 1 -- Index of current position. + -- Number of characters that changed prior to the equality. + local length_insertions1 = 0 + local length_deletions1 = 0 + -- Number of characters that changed after the equality. + local length_insertions2 = 0 + local length_deletions2 = 0 + + while diffs[pointer] do + if diffs[pointer][1] == DIFF_EQUAL then -- Equality found. + equalitiesLength = equalitiesLength + 1 + equalities[equalitiesLength] = pointer + length_insertions1 = length_insertions2 + length_deletions1 = length_deletions2 + length_insertions2 = 0 + length_deletions2 = 0 + lastEquality = diffs[pointer][2] + else -- An insertion or deletion. + if diffs[pointer][1] == DIFF_INSERT then + length_insertions2 = length_insertions2 + #diffs[pointer][2] + else + length_deletions2 = length_deletions2 + #diffs[pointer][2] + end + -- Eliminate an equality that is smaller or equal to the edits on both + -- sides of it. + if + lastEquality + and (#lastEquality <= max(length_insertions1, length_deletions1)) + and (#lastEquality <= max(length_insertions2, length_deletions2)) + then + -- Duplicate record. + tinsert(diffs, equalities[equalitiesLength], { DIFF_DELETE, lastEquality }) + -- Change second copy to insert. + diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT + -- Throw away the equality we just deleted. + equalitiesLength = equalitiesLength - 1 + -- Throw away the previous equality (it needs to be reevaluated). + equalitiesLength = equalitiesLength - 1 + pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 + length_insertions1, length_deletions1 = 0, 0 -- Reset the counters. + length_insertions2, length_deletions2 = 0, 0 + lastEquality = nil + changes = true + end + end + pointer = pointer + 1 + end + + -- Normalize the diff. + if changes then + _diff_cleanupMerge(diffs) + end + _diff_cleanupSemanticLossless(diffs) + + -- Find any overlaps between deletions and insertions. + -- e.g: abcxxxxxxdef + -- -> abcxxxdef + -- e.g: xxxabcdefxxx + -- -> defxxxabc + -- Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 2 + while diffs[pointer] do + if diffs[pointer - 1][1] == DIFF_DELETE and diffs[pointer][1] == DIFF_INSERT then + local deletion = diffs[pointer - 1][2] + local insertion = diffs[pointer][2] + local overlap_length1 = _diff_commonOverlap(deletion, insertion) + local overlap_length2 = _diff_commonOverlap(insertion, deletion) + if overlap_length1 >= overlap_length2 then + if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then + -- Overlap found. Insert an equality and trim the surrounding edits. + tinsert(diffs, pointer, { DIFF_EQUAL, strsub(insertion, 1, overlap_length1) }) + diffs[pointer - 1][2] = strsub(deletion, 1, #deletion - overlap_length1) + diffs[pointer + 1][2] = strsub(insertion, overlap_length1 + 1) + pointer = pointer + 1 + end + else + if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then + -- Reverse overlap found. + -- Insert an equality and swap and trim the surrounding edits. + tinsert(diffs, pointer, { DIFF_EQUAL, strsub(deletion, 1, overlap_length2) }) + diffs[pointer - 1] = { DIFF_INSERT, strsub(insertion, 1, #insertion - overlap_length2) } + diffs[pointer + 1] = { DIFF_DELETE, strsub(deletion, overlap_length2 + 1) } + pointer = pointer + 1 + end + end + pointer = pointer + 1 + end + pointer = pointer + 1 + end +end + +--[[ +* Reduce the number of edits by eliminating operationally trivial equalities. +* @param {Array.>} diffs Array of diff tuples. +--]] +function diff_cleanupEfficiency(diffs) + local changes = false + -- Stack of indices where equalities are found. + local equalities = {} + -- Keeping our own length var is faster. + local equalitiesLength = 0 + -- Always equal to diffs[equalities[equalitiesLength]][2] + local lastEquality = nil + -- Index of current position. + local pointer = 1 + + -- The following four are really booleans but are stored as numbers because + -- they are used at one point like this: + -- + -- (pre_ins + pre_del + post_ins + post_del) == 3 + -- + -- ...i.e. checking that 3 of them are true and 1 of them is false. + + -- Is there an insertion operation before the last equality. + local pre_ins = 0 + -- Is there a deletion operation before the last equality. + local pre_del = 0 + -- Is there an insertion operation after the last equality. + local post_ins = 0 + -- Is there a deletion operation after the last equality. + local post_del = 0 + + while diffs[pointer] do + if diffs[pointer][1] == DIFF_EQUAL then -- Equality found. + local diffText = diffs[pointer][2] + if (#diffText < Diff_EditCost) and (post_ins == 1 or post_del == 1) then + -- Candidate found. + equalitiesLength = equalitiesLength + 1 + equalities[equalitiesLength] = pointer + pre_ins, pre_del = post_ins, post_del + lastEquality = diffText + else + -- Not a candidate, and can never become one. + equalitiesLength = 0 + lastEquality = nil + end + post_ins, post_del = 0, 0 + else -- An insertion or deletion. + if diffs[pointer][1] == DIFF_DELETE then + post_del = 1 + else + post_ins = 1 + end + --[[ + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + --]] + if + lastEquality + and ( + (pre_ins + pre_del + post_ins + post_del == 4) + or ((#lastEquality < Diff_EditCost / 2) and (pre_ins + pre_del + post_ins + post_del == 3)) + ) + then + -- Duplicate record. + tinsert(diffs, equalities[equalitiesLength], { DIFF_DELETE, lastEquality }) + -- Change second copy to insert. + diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT + -- Throw away the equality we just deleted. + equalitiesLength = equalitiesLength - 1 + lastEquality = nil + if (pre_ins == 1) and (pre_del == 1) then + -- No changes made which could affect previous entry, keep going. + post_ins, post_del = 1, 1 + equalitiesLength = 0 + else + -- Throw away the previous equality. + equalitiesLength = equalitiesLength - 1 + pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 + post_ins, post_del = 0, 0 + end + changes = true + end + end + pointer = pointer + 1 + end + + if changes then + _diff_cleanupMerge(diffs) + end +end + +--[[ +* Compute the Levenshtein distance; the number of inserted, deleted or +* substituted characters. +* @param {Array.>} diffs Array of diff tuples. +* @return {number} Number of changes. +--]] +function diff_levenshtein(diffs) + local levenshtein = 0 + local insertions, deletions = 0, 0 + for x, diff in ipairs(diffs) do + local op, data = diff[1], diff[2] + if op == DIFF_INSERT then + insertions = insertions + #data + elseif op == DIFF_DELETE then + deletions = deletions + #data + elseif op == DIFF_EQUAL then + -- A deletion and an insertion is one substitution. + levenshtein = levenshtein + max(insertions, deletions) + insertions = 0 + deletions = 0 + end + end + levenshtein = levenshtein + max(insertions, deletions) + return levenshtein +end + +--[[ +* Convert a diff array into a pretty HTML report. +* @param {Array.>} diffs Array of diff tuples. +* @return {string} HTML representation. +--]] +function diff_prettyHtml(diffs) + local html = {} + for x, diff in ipairs(diffs) do + local op = diff[1] -- Operation (insert, delete, equal) + local data = diff[2] -- Text of change. + local text = gsub(data, htmlEncode_pattern, htmlEncode_replace) + if op == DIFF_INSERT then + html[x] = '' .. text .. '' + elseif op == DIFF_DELETE then + html[x] = '' .. text .. '' + elseif op == DIFF_EQUAL then + html[x] = '' .. text .. '' + end + end + return tconcat(html) +end + +-- --------------------------------------------------------------------------- +-- UNOFFICIAL/PRIVATE DIFF FUNCTIONS +-- --------------------------------------------------------------------------- + +--[[ +* Find the differences between two texts. Assumes that the texts do not +* have any common prefix or suffix. +* @param {string} text1 Old string to be diffed. +* @param {string} text2 New string to be diffed. +* @param {boolean} checklines Has no effect in Lua. +* @param {number} deadline Time when the diff should be complete by. +* @return {Array.>} Array of diff tuples. +* @private +--]] +function _diff_compute(text1, text2, checklines, deadline) + if #text1 == 0 then + -- Just add some text (speedup). + return { { DIFF_INSERT, text2 } } + end + + if #text2 == 0 then + -- Just delete some text (speedup). + return { { DIFF_DELETE, text1 } } + end + + local diffs + + local longtext = (#text1 > #text2) and text1 or text2 + local shorttext = (#text1 > #text2) and text2 or text1 + local i = indexOf(longtext, shorttext) + + if i ~= nil then + -- Shorter text is inside the longer text (speedup). + diffs = { + { DIFF_INSERT, strsub(longtext, 1, i - 1) }, + { DIFF_EQUAL, shorttext }, + { DIFF_INSERT, strsub(longtext, i + #shorttext) }, + } + -- Swap insertions for deletions if diff is reversed. + if #text1 > #text2 then + diffs[1][1], diffs[3][1] = DIFF_DELETE, DIFF_DELETE + end + return diffs + end + + if #shorttext == 1 then + -- Single character string. + -- After the previous speedup, the character can't be an equality. + return { { DIFF_DELETE, text1 }, { DIFF_INSERT, text2 } } + end + + -- Check to see if the problem can be split in two. + do + local text1_a, text1_b, text2_a, text2_b, mid_common = _diff_halfMatch(text1, text2) + + if text1_a then + -- A half-match was found, sort out the return data. + -- Send both pairs off for separate processing. + local diffs_a = diff_main(text1_a, text2_a, checklines, deadline) + local diffs_b = diff_main(text1_b, text2_b, checklines, deadline) + -- Merge the results. + local diffs_a_len = #diffs_a + diffs = diffs_a + diffs[diffs_a_len + 1] = { DIFF_EQUAL, mid_common } + for i, b_diff in ipairs(diffs_b) do + diffs[diffs_a_len + 1 + i] = b_diff + end + return diffs + end + end + + return _diff_bisect(text1, text2, deadline) +end + +--[[ +* Find the 'middle snake' of a diff, split the problem in two +* and return the recursively constructed diff. +* See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. +* @param {string} text1 Old string to be diffed. +* @param {string} text2 New string to be diffed. +* @param {number} deadline Time at which to bail if not yet complete. +* @return {Array.>} Array of diff tuples. +* @private +--]] +function _diff_bisect(text1, text2, deadline) + -- Cache the text lengths to prevent multiple calls. + local text1_length = #text1 + local text2_length = #text2 + local _sub, _element + local max_d = ceil((text1_length + text2_length) / 2) + local v_offset = max_d + local v_length = 2 * max_d + local v1 = {} + local v2 = {} + -- Setting all elements to -1 is faster in Lua than mixing integers and nil. + for x = 0, v_length - 1 do + v1[x] = -1 + v2[x] = -1 + end + v1[v_offset + 1] = 0 + v2[v_offset + 1] = 0 + local delta = text1_length - text2_length + -- If the total number of characters is odd, then + -- the front path will collide with the reverse path. + local front = (delta % 2 ~= 0) + -- Offsets for start and end of k loop. + -- Prevents mapping of space beyond the grid. + local k1start = 0 + local k1end = 0 + local k2start = 0 + local k2end = 0 + for d = 0, max_d - 1 do + -- Bail out if deadline is reached. + if clock() > deadline then + break + end + + -- Walk the front path one step. + for k1 = -d + k1start, d - k1end, 2 do + local k1_offset = v_offset + k1 + local x1 + if (k1 == -d) or ((k1 ~= d) and (v1[k1_offset - 1] < v1[k1_offset + 1])) then + x1 = v1[k1_offset + 1] + else + x1 = v1[k1_offset - 1] + 1 + end + local y1 = x1 - k1 + while (x1 <= text1_length) and (y1 <= text2_length) and (strsub(text1, x1, x1) == strsub(text2, y1, y1)) do + x1 = x1 + 1 + y1 = y1 + 1 + end + v1[k1_offset] = x1 + if x1 > text1_length + 1 then + -- Ran off the right of the graph. + k1end = k1end + 2 + elseif y1 > text2_length + 1 then + -- Ran off the bottom of the graph. + k1start = k1start + 2 + elseif front then + local k2_offset = v_offset + delta - k1 + if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] ~= -1 then + -- Mirror x2 onto top-left coordinate system. + local x2 = text1_length - v2[k2_offset] + 1 + if x1 > x2 then + -- Overlap detected. + return _diff_bisectSplit(text1, text2, x1, y1, deadline) + end + end + end + end + + -- Walk the reverse path one step. + for k2 = -d + k2start, d - k2end, 2 do + local k2_offset = v_offset + k2 + local x2 + if (k2 == -d) or ((k2 ~= d) and (v2[k2_offset - 1] < v2[k2_offset + 1])) then + x2 = v2[k2_offset + 1] + else + x2 = v2[k2_offset - 1] + 1 + end + local y2 = x2 - k2 + while (x2 <= text1_length) and (y2 <= text2_length) and (strsub(text1, -x2, -x2) == strsub(text2, -y2, -y2)) do + x2 = x2 + 1 + y2 = y2 + 1 + end + v2[k2_offset] = x2 + if x2 > text1_length + 1 then + -- Ran off the left of the graph. + k2end = k2end + 2 + elseif y2 > text2_length + 1 then + -- Ran off the top of the graph. + k2start = k2start + 2 + elseif not front then + local k1_offset = v_offset + delta - k2 + if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] ~= -1 then + local x1 = v1[k1_offset] + local y1 = v_offset + x1 - k1_offset + -- Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2 + 1 + if x1 > x2 then + -- Overlap detected. + return _diff_bisectSplit(text1, text2, x1, y1, deadline) + end + end + end + end + end + -- Diff took too long and hit the deadline or + -- number of diffs equals number of characters, no commonality at all. + return { { DIFF_DELETE, text1 }, { DIFF_INSERT, text2 } } +end + +--[[ + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {Array.>} Array of diff tuples. + * @private +--]] +function _diff_bisectSplit(text1, text2, x, y, deadline) + local text1a = strsub(text1, 1, x - 1) + local text2a = strsub(text2, 1, y - 1) + local text1b = strsub(text1, x) + local text2b = strsub(text2, y) + + -- Compute both diffs serially. + local diffs = diff_main(text1a, text2a, false, deadline) + local diffsb = diff_main(text1b, text2b, false, deadline) + + local diffs_len = #diffs + for i, v in ipairs(diffsb) do + diffs[diffs_len + i] = v + end + return diffs +end + +--[[ +* Determine the common prefix of two strings. +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {number} The number of characters common to the start of each +* string. +--]] +function _diff_commonPrefix(text1, text2) + -- Quick check for common null cases. + if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, 1) ~= strbyte(text2, 1)) then + return 0 + end + -- Binary search. + -- Performance analysis: https://neil.fraser.name/news/2007/10/09/ + local pointermin = 1 + local pointermax = min(#text1, #text2) + local pointermid = pointermax + local pointerstart = 1 + while pointermin < pointermid do + if strsub(text1, pointerstart, pointermid) == strsub(text2, pointerstart, pointermid) then + pointermin = pointermid + pointerstart = pointermin + else + pointermax = pointermid + end + pointermid = floor(pointermin + (pointermax - pointermin) / 2) + end + return pointermid +end + +--[[ +* Determine the common suffix of two strings. +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {number} The number of characters common to the end of each string. +--]] +function _diff_commonSuffix(text1, text2) + -- Quick check for common null cases. + if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, -1) ~= strbyte(text2, -1)) then + return 0 + end + -- Binary search. + -- Performance analysis: https://neil.fraser.name/news/2007/10/09/ + local pointermin = 1 + local pointermax = min(#text1, #text2) + local pointermid = pointermax + local pointerend = 1 + while pointermin < pointermid do + if strsub(text1, -pointermid, -pointerend) == strsub(text2, -pointermid, -pointerend) then + pointermin = pointermid + pointerend = pointermin + else + pointermax = pointermid + end + pointermid = floor(pointermin + (pointermax - pointermin) / 2) + end + return pointermid +end + +--[[ +* Determine if the suffix of one string is the prefix of another. +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {number} The number of characters common to the end of the first +* string and the start of the second string. +* @private +--]] +function _diff_commonOverlap(text1, text2) + -- Cache the text lengths to prevent multiple calls. + local text1_length = #text1 + local text2_length = #text2 + -- Eliminate the null case. + if text1_length == 0 or text2_length == 0 then + return 0 + end + -- Truncate the longer string. + if text1_length > text2_length then + text1 = strsub(text1, text1_length - text2_length + 1) + elseif text1_length < text2_length then + text2 = strsub(text2, 1, text1_length) + end + local text_length = min(text1_length, text2_length) + -- Quick check for the worst case. + if text1 == text2 then + return text_length + end + + -- Start by looking for a single character match + -- and increase length until no match is found. + -- Performance analysis: https://neil.fraser.name/news/2010/11/04/ + local best = 0 + local length = 1 + while true do + local pattern = strsub(text1, text_length - length + 1) + local found = strfind(text2, pattern, 1, true) + if found == nil then + return best + end + length = length + found - 1 + if found == 1 or strsub(text1, text_length - length + 1) == strsub(text2, 1, length) then + best = length + length = length + 1 + end + end +end + +--[[ +* Does a substring of shorttext exist within longtext such that the substring +* is at least half the length of longtext? +* This speedup can produce non-minimal diffs. +* Closure, but does not reference any external variables. +* @param {string} longtext Longer string. +* @param {string} shorttext Shorter string. +* @param {number} i Start index of quarter length substring within longtext. +* @return {?Array.} Five element Array, containing the prefix of +* longtext, the suffix of longtext, the prefix of shorttext, the suffix +* of shorttext and the common middle. Or nil if there was no match. +* @private +--]] +function _diff_halfMatchI(longtext, shorttext, i) + -- Start with a 1/4 length substring at position i as a seed. + local seed = strsub(longtext, i, i + floor(#longtext / 4)) + local j = 0 -- LUANOTE: do not change to 1, was originally -1 + local best_common = '' + local best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b + while true do + j = indexOf(shorttext, seed, j + 1) + if j == nil then + break + end + local prefixLength = _diff_commonPrefix(strsub(longtext, i), strsub(shorttext, j)) + local suffixLength = _diff_commonSuffix(strsub(longtext, 1, i - 1), strsub(shorttext, 1, j - 1)) + if #best_common < suffixLength + prefixLength then + best_common = strsub(shorttext, j - suffixLength, j - 1) .. strsub(shorttext, j, j + prefixLength - 1) + best_longtext_a = strsub(longtext, 1, i - suffixLength - 1) + best_longtext_b = strsub(longtext, i + prefixLength) + best_shorttext_a = strsub(shorttext, 1, j - suffixLength - 1) + best_shorttext_b = strsub(shorttext, j + prefixLength) + end + end + if #best_common * 2 >= #longtext then + return { best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common } + else + return nil + end +end + +--[[ +* Do the two texts share a substring which is at least half the length of the +* longer text? +* @param {string} text1 First string. +* @param {string} text2 Second string. +* @return {?Array.} Five element Array, containing the prefix of +* text1, the suffix of text1, the prefix of text2, the suffix of +* text2 and the common middle. Or nil if there was no match. +* @private +--]] +function _diff_halfMatch(text1, text2) + if Diff_Timeout <= 0 then + -- Don't risk returning a non-optimal diff if we have unlimited time. + return nil + end + local longtext = (#text1 > #text2) and text1 or text2 + local shorttext = (#text1 > #text2) and text2 or text1 + if (#longtext < 4) or (#shorttext * 2 < #longtext) then + return nil -- Pointless. + end + + -- First check if the second quarter is the seed for a half-match. + local hm1 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 4)) + -- Check again based on the third quarter. + local hm2 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 2)) + local hm + if not hm1 and not hm2 then + return nil + elseif not hm2 then + hm = hm1 + elseif not hm1 then + hm = hm2 + else + -- Both matched. Select the longest. + hm = (#hm1[5] > #hm2[5]) and hm1 or hm2 + end + + -- A half-match was found, sort out the return data. + local text1_a, text1_b, text2_a, text2_b + if #text1 > #text2 then + text1_a, text1_b = hm[1], hm[2] + text2_a, text2_b = hm[3], hm[4] + else + text2_a, text2_b = hm[1], hm[2] + text1_a, text1_b = hm[3], hm[4] + end + local mid_common = hm[5] + return text1_a, text1_b, text2_a, text2_b, mid_common +end + +--[[ +* Given two strings, compute a score representing whether the internal +* boundary falls on logical boundaries. +* Scores range from 6 (best) to 0 (worst). +* @param {string} one First string. +* @param {string} two Second string. +* @return {number} The score. +* @private +--]] +function _diff_cleanupSemanticScore(one, two) + if (#one == 0) or (#two == 0) then + -- Edges are the best. + return 6 + end + + -- Each port of this function behaves slightly differently due to + -- subtle differences in each language's definition of things like + -- 'whitespace'. Since this function's purpose is largely cosmetic, + -- the choice has been made to use each language's native features + -- rather than force total conformity. + local char1 = strsub(one, -1) + local char2 = strsub(two, 1, 1) + local nonAlphaNumeric1 = strmatch(char1, '%W') + local nonAlphaNumeric2 = strmatch(char2, '%W') + local whitespace1 = nonAlphaNumeric1 and strmatch(char1, '%s') + local whitespace2 = nonAlphaNumeric2 and strmatch(char2, '%s') + local lineBreak1 = whitespace1 and strmatch(char1, '%c') + local lineBreak2 = whitespace2 and strmatch(char2, '%c') + local blankLine1 = lineBreak1 and strmatch(one, '\n\r?\n$') + local blankLine2 = lineBreak2 and strmatch(two, '^\r?\n\r?\n') + + if blankLine1 or blankLine2 then + -- Five points for blank lines. + return 5 + elseif lineBreak1 or lineBreak2 then + -- Four points for line breaks. + return 4 + elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then + -- Three points for end of sentences. + return 3 + elseif whitespace1 or whitespace2 then + -- Two points for whitespace. + return 2 + elseif nonAlphaNumeric1 or nonAlphaNumeric2 then + -- One point for non-alphanumeric. + return 1 + end + return 0 +end + +--[[ +* Look for single edits surrounded on both sides by equalities +* which can be shifted sideways to align the edit to a word boundary. +* e.g: The cat came. -> The cat came. +* @param {Array.>} diffs Array of diff tuples. +--]] +function _diff_cleanupSemanticLossless(diffs) + local pointer = 2 + -- Intentionally ignore the first and last element (don't need checking). + while diffs[pointer + 1] do + local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] + if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then + -- This is a single edit surrounded by equalities. + local diff = diffs[pointer] + + local equality1 = prevDiff[2] + local edit = diff[2] + local equality2 = nextDiff[2] + + -- First, shift the edit as far left as possible. + local commonOffset = _diff_commonSuffix(equality1, edit) + if commonOffset > 0 then + local commonString = strsub(edit, -commonOffset) + equality1 = strsub(equality1, 1, -commonOffset - 1) + edit = commonString .. strsub(edit, 1, -commonOffset - 1) + equality2 = commonString .. equality2 + end + + -- Second, step character by character right, looking for the best fit. + local bestEquality1 = equality1 + local bestEdit = edit + local bestEquality2 = equality2 + local bestScore = _diff_cleanupSemanticScore(equality1, edit) + _diff_cleanupSemanticScore(edit, equality2) + + while strbyte(edit, 1) == strbyte(equality2, 1) do + equality1 = equality1 .. strsub(edit, 1, 1) + edit = strsub(edit, 2) .. strsub(equality2, 1, 1) + equality2 = strsub(equality2, 2) + local score = _diff_cleanupSemanticScore(equality1, edit) + _diff_cleanupSemanticScore(edit, equality2) + -- The >= encourages trailing rather than leading whitespace on edits. + if score >= bestScore then + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + end + end + if prevDiff[2] ~= bestEquality1 then + -- We have an improvement, save it back to the diff. + if #bestEquality1 > 0 then + diffs[pointer - 1][2] = bestEquality1 + else + tremove(diffs, pointer - 1) + pointer = pointer - 1 + end + diffs[pointer][2] = bestEdit + if #bestEquality2 > 0 then + diffs[pointer + 1][2] = bestEquality2 + else + tremove(diffs, pointer + 1, 1) + pointer = pointer - 1 + end + end + end + pointer = pointer + 1 + end +end + +--[[ +* Reorder and merge like edit sections. Merge equalities. +* Any edit section can move as long as it doesn't cross an equality. +* @param {Array.>} diffs Array of diff tuples. +--]] +function _diff_cleanupMerge(diffs) + diffs[#diffs + 1] = { DIFF_EQUAL, '' } -- Add a dummy entry at the end. + local pointer = 1 + local count_delete, count_insert = 0, 0 + local text_delete, text_insert = '', '' + local commonlength + while diffs[pointer] do + local diff_type = diffs[pointer][1] + if diff_type == DIFF_INSERT then + count_insert = count_insert + 1 + text_insert = text_insert .. diffs[pointer][2] + pointer = pointer + 1 + elseif diff_type == DIFF_DELETE then + count_delete = count_delete + 1 + text_delete = text_delete .. diffs[pointer][2] + pointer = pointer + 1 + elseif diff_type == DIFF_EQUAL then + -- Upon reaching an equality, check for prior redundancies. + if count_delete + count_insert > 1 then + if (count_delete > 0) and (count_insert > 0) then + -- Factor out any common prefixies. + commonlength = _diff_commonPrefix(text_insert, text_delete) + if commonlength > 0 then + local back_pointer = pointer - count_delete - count_insert + if (back_pointer > 1) and (diffs[back_pointer - 1][1] == DIFF_EQUAL) then + diffs[back_pointer - 1][2] = diffs[back_pointer - 1][2] .. strsub(text_insert, 1, commonlength) + else + tinsert(diffs, 1, { DIFF_EQUAL, strsub(text_insert, 1, commonlength) }) + pointer = pointer + 1 + end + text_insert = strsub(text_insert, commonlength + 1) + text_delete = strsub(text_delete, commonlength + 1) + end + -- Factor out any common suffixies. + commonlength = _diff_commonSuffix(text_insert, text_delete) + if commonlength ~= 0 then + diffs[pointer][2] = strsub(text_insert, -commonlength) .. diffs[pointer][2] + text_insert = strsub(text_insert, 1, -commonlength - 1) + text_delete = strsub(text_delete, 1, -commonlength - 1) + end + end + -- Delete the offending records and add the merged ones. + pointer = pointer - count_delete - count_insert + for i = 1, count_delete + count_insert do + tremove(diffs, pointer) + end + if #text_delete > 0 then + tinsert(diffs, pointer, { DIFF_DELETE, text_delete }) + pointer = pointer + 1 + end + if #text_insert > 0 then + tinsert(diffs, pointer, { DIFF_INSERT, text_insert }) + pointer = pointer + 1 + end + pointer = pointer + 1 + elseif (pointer > 1) and (diffs[pointer - 1][1] == DIFF_EQUAL) then + -- Merge this equality with the previous one. + diffs[pointer - 1][2] = diffs[pointer - 1][2] .. diffs[pointer][2] + tremove(diffs, pointer) + else + pointer = pointer + 1 + end + count_insert, count_delete = 0, 0 + text_delete, text_insert = '', '' + end + end + if diffs[#diffs][2] == '' then + diffs[#diffs] = nil -- Remove the dummy entry at the end. + end + + -- Second pass: look for single edits surrounded on both sides by equalities + -- which can be shifted sideways to eliminate an equality. + -- e.g: ABAC -> ABAC + local changes = false + pointer = 2 + -- Intentionally ignore the first and last element (don't need checking). + while pointer < #diffs do + local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] + if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then + -- This is a single edit surrounded by equalities. + local diff = diffs[pointer] + local currentText = diff[2] + local prevText = prevDiff[2] + local nextText = nextDiff[2] + if #prevText == 0 then + tremove(diffs, pointer - 1) + changes = true + elseif strsub(currentText, -#prevText) == prevText then + -- Shift the edit over the previous equality. + diff[2] = prevText .. strsub(currentText, 1, -#prevText - 1) + nextDiff[2] = prevText .. nextDiff[2] + tremove(diffs, pointer - 1) + changes = true + elseif strsub(currentText, 1, #nextText) == nextText then + -- Shift the edit over the next equality. + prevDiff[2] = prevText .. nextText + diff[2] = strsub(currentText, #nextText + 1) .. nextText + tremove(diffs, pointer + 1) + changes = true + end + end + pointer = pointer + 1 + end + -- If shifts were made, the diff needs reordering and another shift sweep. + if changes then + -- LUANOTE: no return value, but necessary to use 'return' to get + -- tail calls. + return _diff_cleanupMerge(diffs) + end +end + +--[[ +* loc is a location in text1, compute and return the equivalent location in +* text2. +* e.g. 'The cat' vs 'The big cat', 1->1, 5->8 +* @param {Array.>} diffs Array of diff tuples. +* @param {number} loc Location within text1. +* @return {number} Location within text2. +--]] +function _diff_xIndex(diffs, loc) + local chars1 = 1 + local chars2 = 1 + local last_chars1 = 1 + local last_chars2 = 1 + local x + for _x, diff in ipairs(diffs) do + x = _x + if diff[1] ~= DIFF_INSERT then -- Equality or deletion. + chars1 = chars1 + #diff[2] + end + if diff[1] ~= DIFF_DELETE then -- Equality or insertion. + chars2 = chars2 + #diff[2] + end + if chars1 > loc then -- Overshot the location. + break + end + last_chars1 = chars1 + last_chars2 = chars2 + end + -- Was the location deleted? + if diffs[x + 1] and (diffs[x][1] == DIFF_DELETE) then + return last_chars2 + end + -- Add the remaining character length. + return last_chars2 + (loc - last_chars1) +end + +--[[ +* Compute and return the source text (all equalities and deletions). +* @param {Array.>} diffs Array of diff tuples. +* @return {string} Source text. +--]] +function _diff_text1(diffs) + local text = {} + for x, diff in ipairs(diffs) do + if diff[1] ~= DIFF_INSERT then + text[#text + 1] = diff[2] + end + end + return tconcat(text) +end + +--[[ +* Compute and return the destination text (all equalities and insertions). +* @param {Array.>} diffs Array of diff tuples. +* @return {string} Destination text. +--]] +function _diff_text2(diffs) + local text = {} + for x, diff in ipairs(diffs) do + if diff[1] ~= DIFF_DELETE then + text[#text + 1] = diff[2] + end + end + return tconcat(text) +end + +--[[ +* Crush the diff into an encoded string which describes the operations +* required to transform text1 into text2. +* E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. +* Operations are tab-separated. Inserted text is escaped using %xx notation. +* @param {Array.>} diffs Array of diff tuples. +* @return {string} Delta text. +--]] +function _diff_toDelta(diffs) + local text = {} + for x, diff in ipairs(diffs) do + local op, data = diff[1], diff[2] + if op == DIFF_INSERT then + text[x] = '+' .. gsub(data, percentEncode_pattern, percentEncode_replace) + elseif op == DIFF_DELETE then + text[x] = '-' .. #data + elseif op == DIFF_EQUAL then + text[x] = '=' .. #data + end + end + return tconcat(text, '\t') +end + +--[[ +* Given the original text1, and an encoded string which describes the +* operations required to transform text1 into text2, compute the full diff. +* @param {string} text1 Source string for the diff. +* @param {string} delta Delta text. +* @return {Array.>} Array of diff tuples. +* @throws {Errorend If invalid input. +--]] +function _diff_fromDelta(text1, delta) + local diffs = {} + local diffsLength = 0 -- Keeping our own length var is faster + local pointer = 1 -- Cursor in text1 + for token in gmatch(delta, '[^\t]+') do + -- Each token begins with a one character parameter which specifies the + -- operation of this token (delete, insert, equality). + local tokenchar, param = strsub(token, 1, 1), strsub(token, 2) + if tokenchar == '+' then + local invalidDecode = false + local decoded = gsub(param, '%%(.?.?)', function(c) + local n = tonumber(c, 16) + if (#c ~= 2) or (n == nil) then + invalidDecode = true + return '' + end + return strchar(n) + end) + if invalidDecode then + -- Malformed URI sequence. + error('Illegal escape in _diff_fromDelta: ' .. param) + end + diffsLength = diffsLength + 1 + diffs[diffsLength] = { DIFF_INSERT, decoded } + elseif (tokenchar == '-') or (tokenchar == '=') then + local n = tonumber(param) + if (n == nil) or (n < 0) then + error('Invalid number in _diff_fromDelta: ' .. param) + end + local text = strsub(text1, pointer, pointer + n - 1) + pointer = pointer + n + if tokenchar == '=' then + diffsLength = diffsLength + 1 + diffs[diffsLength] = { DIFF_EQUAL, text } + else + diffsLength = diffsLength + 1 + diffs[diffsLength] = { DIFF_DELETE, text } + end + else + error('Invalid diff operation in _diff_fromDelta: ' .. token) + end + end + if pointer ~= #text1 + 1 then + error('Delta length (' .. (pointer - 1) .. ') does not equal source text length (' .. #text1 .. ').') + end + return diffs +end + +-- --------------------------------------------------------------------------- +-- MATCH API +-- --------------------------------------------------------------------------- + +local _match_bitap, _match_alphabet + +--[[ +* Locate the best instance of 'pattern' in 'text' near 'loc'. +* @param {string} text The text to search. +* @param {string} pattern The pattern to search for. +* @param {number} loc The location to search around. +* @return {number} Best match index or -1. +--]] +function match_main(text, pattern, loc) + -- Check for null inputs. + if text == nil or pattern == nil or loc == nil then + error('Null inputs. (match_main)') + end + + if text == pattern then + -- Shortcut (potentially not guaranteed by the algorithm) + return 1 + elseif #text == 0 then + -- Nothing to match. + return -1 + end + loc = max(1, min(loc, #text)) + if strsub(text, loc, loc + #pattern - 1) == pattern then + -- Perfect match at the perfect spot! (Includes case of null pattern) + return loc + else + -- Do a fuzzy compare. + return _match_bitap(text, pattern, loc) + end +end + +-- --------------------------------------------------------------------------- +-- UNOFFICIAL/PRIVATE MATCH FUNCTIONS +-- --------------------------------------------------------------------------- + +--[[ +* Initialise the alphabet for the Bitap algorithm. +* @param {string} pattern The text to encode. +* @return {Object} Hash of character locations. +* @private +--]] +function _match_alphabet(pattern) + local s = {} + local i = 0 + for c in gmatch(pattern, '.') do + s[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1)) + i = i + 1 + end + return s +end + +--[[ +* Locate the best instance of 'pattern' in 'text' near 'loc' using the +* Bitap algorithm. +* @param {string} text The text to search. +* @param {string} pattern The pattern to search for. +* @param {number} loc The location to search around. +* @return {number} Best match index or -1. +* @private +--]] +function _match_bitap(text, pattern, loc) + if #pattern > Match_MaxBits then + error('Pattern too long.') + end + + -- Initialise the alphabet. + local s = _match_alphabet(pattern) + + --[[ + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + --]] + local function _match_bitapScore(e, x) + local accuracy = e / #pattern + local proximity = abs(loc - x) + if Match_Distance == 0 then + -- Dodge divide by zero error. + return (proximity == 0) and 1 or accuracy + end + return accuracy + (proximity / Match_Distance) + end + + -- Highest score beyond which we give up. + local score_threshold = Match_Threshold + -- Is there a nearby exact match? (speedup) + local best_loc = indexOf(text, pattern, loc) + if best_loc then + score_threshold = min(_match_bitapScore(0, best_loc), score_threshold) + -- LUANOTE: Ideally we'd also check from the other direction, but Lua + -- doesn't have an efficent lastIndexOf function. + end + + -- Initialise the bit arrays. + local matchmask = lshift(1, #pattern - 1) + best_loc = -1 + + local bin_min, bin_mid + local bin_max = #pattern + #text + local last_rd + for d = 0, #pattern - 1, 1 do + -- Scan for the best match; each iteration allows for one more error. + -- Run a binary search to determine how far from 'loc' we can stray at this + -- error level. + bin_min = 0 + bin_mid = bin_max + while bin_min < bin_mid do + if _match_bitapScore(d, loc + bin_mid) <= score_threshold then + bin_min = bin_mid + else + bin_max = bin_mid + end + bin_mid = floor(bin_min + (bin_max - bin_min) / 2) + end + -- Use the result from this iteration as the maximum for the next. + bin_max = bin_mid + local start = max(1, loc - bin_mid + 1) + local finish = min(loc + bin_mid, #text) + #pattern + + local rd = {} + for j = start, finish do + rd[j] = 0 + end + rd[finish + 1] = lshift(1, d) - 1 + for j = finish, start, -1 do + local charMatch = s[strsub(text, j - 1, j - 1)] or 0 + if d == 0 then -- First pass: exact match. + rd[j] = band(bor((rd[j + 1] * 2), 1), charMatch) + else + -- Subsequent passes: fuzzy match. + -- Functions instead of operators make this hella messy. + rd[j] = bor( + band(bor(lshift(rd[j + 1], 1), 1), charMatch), + bor(bor(lshift(bor(last_rd[j + 1], last_rd[j]), 1), 1), last_rd[j + 1]) + ) + end + if band(rd[j], matchmask) ~= 0 then + local score = _match_bitapScore(d, j - 1) + -- This match will almost certainly be better than any existing match. + -- But check anyway. + if score <= score_threshold then + -- Told you so. + score_threshold = score + best_loc = j - 1 + if best_loc > loc then + -- When passing loc, don't exceed our current distance from loc. + start = max(1, loc * 2 - best_loc) + else + -- Already passed loc, downhill from here on in. + break + end + end + end + end + -- No hope for a (better) match at greater error levels. + if _match_bitapScore(d + 1, loc) > score_threshold then + break + end + last_rd = rd + end + return best_loc +end + +-- ----------------------------------------------------------------------------- +-- PATCH API +-- ----------------------------------------------------------------------------- + +local _patch_addContext, _patch_deepCopy, _patch_addPadding, _patch_splitMax, _patch_appendText, _new_patch_obj + +--[[ +* Compute a list of patches to turn text1 into text2. +* Use diffs if provided, otherwise compute it ourselves. +* There are four ways to call this function, depending on what data is +* available to the caller: +* Method 1: +* a = text1, b = text2 +* Method 2: +* a = diffs +* Method 3 (optimal): +* a = text1, b = diffs +* Method 4 (deprecated, use method 3): +* a = text1, b = text2, c = diffs +* +* @param {string|Array.>} a text1 (methods 1,3,4) or +* Array of diff tuples for text1 to text2 (method 2). +* @param {string|Array.>} opt_b text2 (methods 1,4) or +* Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). +* @param {string|Array.>} opt_c Array of diff tuples for +* text1 to text2 (method 4) or undefined (methods 1,2,3). +* @return {Array.<_new_patch_obj>} Array of patch objects. +--]] +function patch_make(a, opt_b, opt_c) + local text1, diffs + local type_a, type_b, type_c = type(a), type(opt_b), type(opt_c) + if (type_a == 'string') and (type_b == 'string') and (type_c == 'nil') then + -- Method 1: text1, text2 + -- Compute diffs from text1 and text2. + text1 = a + diffs = diff_main(text1, opt_b, true) + if #diffs > 2 then + diff_cleanupSemantic(diffs) + diff_cleanupEfficiency(diffs) + end + elseif (type_a == 'table') and (type_b == 'nil') and (type_c == 'nil') then + -- Method 2: diffs + -- Compute text1 from diffs. + diffs = a + text1 = _diff_text1(diffs) + elseif (type_a == 'string') and (type_b == 'table') and (type_c == 'nil') then + -- Method 3: text1, diffs + text1 = a + diffs = opt_b + elseif (type_a == 'string') and (type_b == 'string') and (type_c == 'table') then + -- Method 4: text1, text2, diffs + -- text2 is not used. + text1 = a + diffs = opt_c + else + error('Unknown call format to patch_make.') + end + + if diffs[1] == nil then + return {} -- Get rid of the null case. + end + + local patches = {} + local patch = _new_patch_obj() + local patchDiffLength = 0 -- Keeping our own length var is faster. + local char_count1 = 0 -- Number of characters into the text1 string. + local char_count2 = 0 -- Number of characters into the text2 string. + -- Start with text1 (prepatch_text) and apply the diffs until we arrive at + -- text2 (postpatch_text). We recreate the patches one by one to determine + -- context info. + local prepatch_text, postpatch_text = text1, text1 + for x, diff in ipairs(diffs) do + local diff_type, diff_text = diff[1], diff[2] + + if (patchDiffLength == 0) and (diff_type ~= DIFF_EQUAL) then + -- A new patch starts here. + patch.start1 = char_count1 + 1 + patch.start2 = char_count2 + 1 + end + + if diff_type == DIFF_INSERT then + patchDiffLength = patchDiffLength + 1 + patch.diffs[patchDiffLength] = diff + patch.length2 = patch.length2 + #diff_text + postpatch_text = strsub(postpatch_text, 1, char_count2) .. diff_text .. strsub(postpatch_text, char_count2 + 1) + elseif diff_type == DIFF_DELETE then + patch.length1 = patch.length1 + #diff_text + patchDiffLength = patchDiffLength + 1 + patch.diffs[patchDiffLength] = diff + postpatch_text = strsub(postpatch_text, 1, char_count2) .. strsub(postpatch_text, char_count2 + #diff_text + 1) + elseif diff_type == DIFF_EQUAL then + if (#diff_text <= Patch_Margin * 2) and (patchDiffLength ~= 0) and (#diffs ~= x) then + -- Small equality inside a patch. + patchDiffLength = patchDiffLength + 1 + patch.diffs[patchDiffLength] = diff + patch.length1 = patch.length1 + #diff_text + patch.length2 = patch.length2 + #diff_text + elseif #diff_text >= Patch_Margin * 2 then + -- Time for a new patch. + if patchDiffLength ~= 0 then + _patch_addContext(patch, prepatch_text) + patches[#patches + 1] = patch + patch = _new_patch_obj() + patchDiffLength = 0 + -- Unlike Unidiff, our patch lists have a rolling context. + -- https://github.com/google/diff-match-patch/wiki/Unidiff + -- Update prepatch text & pos to reflect the application of the + -- just completed patch. + prepatch_text = postpatch_text + char_count1 = char_count2 + end + end + end + + -- Update the current character count. + if diff_type ~= DIFF_INSERT then + char_count1 = char_count1 + #diff_text + end + if diff_type ~= DIFF_DELETE then + char_count2 = char_count2 + #diff_text + end + end + + -- Pick up the leftover patch if not empty. + if patchDiffLength > 0 then + _patch_addContext(patch, prepatch_text) + patches[#patches + 1] = patch + end + + return patches +end + +--[[ +* Merge a set of patches onto the text. Return a patched text, as well +* as a list of true/false values indicating which patches were applied. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @param {string} text Old text. +* @return {Array.>} Two return values, the +* new text and an array of boolean values. +--]] +function patch_apply(patches, text) + if patches[1] == nil then + return text, {} + end + + -- Deep copy the patches so that no changes are made to originals. + patches = _patch_deepCopy(patches) + + local nullPadding = _patch_addPadding(patches) + text = nullPadding .. text .. nullPadding + + _patch_splitMax(patches) + -- delta keeps track of the offset between the expected and actual location + -- of the previous patch. If there are patches expected at positions 10 and + -- 20, but the first patch was found at 12, delta is 2 and the second patch + -- has an effective expected position of 22. + local delta = 0 + local results = {} + for x, patch in ipairs(patches) do + local expected_loc = patch.start2 + delta + local text1 = _diff_text1(patch.diffs) + local start_loc + local end_loc = -1 + if #text1 > Match_MaxBits then + -- _patch_splitMax will only provide an oversized pattern in + -- the case of a monster delete. + start_loc = match_main(text, strsub(text1, 1, Match_MaxBits), expected_loc) + if start_loc ~= -1 then + end_loc = match_main(text, strsub(text1, -Match_MaxBits), expected_loc + #text1 - Match_MaxBits) + if end_loc == -1 or start_loc >= end_loc then + -- Can't find valid trailing context. Drop this patch. + start_loc = -1 + end + end + else + start_loc = match_main(text, text1, expected_loc) + end + if start_loc == -1 then + -- No match found. :( + results[x] = false + -- Subtract the delta for this failed patch from subsequent patches. + delta = delta - patch.length2 - patch.length1 + else + -- Found a match. :) + results[x] = true + delta = start_loc - expected_loc + local text2 + if end_loc == -1 then + text2 = strsub(text, start_loc, start_loc + #text1 - 1) + else + text2 = strsub(text, start_loc, end_loc + Match_MaxBits - 1) + end + if text1 == text2 then + -- Perfect match, just shove the replacement text in. + text = strsub(text, 1, start_loc - 1) .. _diff_text2(patch.diffs) .. strsub(text, start_loc + #text1) + else + -- Imperfect match. Run a diff to get a framework of equivalent + -- indices. + local diffs = diff_main(text1, text2, false) + if (#text1 > Match_MaxBits) and (diff_levenshtein(diffs) / #text1 > Patch_DeleteThreshold) then + -- The end points match, but the content is unacceptably bad. + results[x] = false + else + _diff_cleanupSemanticLossless(diffs) + local index1 = 1 + local index2 + for y, mod in ipairs(patch.diffs) do + if mod[1] ~= DIFF_EQUAL then + index2 = _diff_xIndex(diffs, index1) + end + if mod[1] == DIFF_INSERT then + text = strsub(text, 1, start_loc + index2 - 2) .. mod[2] .. strsub(text, start_loc + index2 - 1) + elseif mod[1] == DIFF_DELETE then + text = strsub(text, 1, start_loc + index2 - 2) + .. strsub(text, start_loc + _diff_xIndex(diffs, index1 + #mod[2] - 1)) + end + if mod[1] ~= DIFF_DELETE then + index1 = index1 + #mod[2] + end + end + end + end + end + end + -- Strip the padding off. + text = strsub(text, #nullPadding + 1, -#nullPadding - 1) + return text, results +end + +--[[ +* Take a list of patches and return a textual representation. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @return {string} Text representation of patches. +--]] +function patch_toText(patches) + local text = {} + for x, patch in ipairs(patches) do + _patch_appendText(patch, text) + end + return tconcat(text) +end + +--[[ +* Parse a textual representation of patches and return a list of patch objects. +* @param {string} textline Text representation of patches. +* @return {Array.<_new_patch_obj>} Array of patch objects. +* @throws {Error} If invalid input. +--]] +function patch_fromText(textline) + local patches = {} + if #textline == 0 then + return patches + end + local text = {} + for line in gmatch(textline, '([^\n]*)') do + text[#text + 1] = line + end + local textPointer = 1 + while textPointer <= #text do + local start1, length1, start2, length2 = strmatch(text[textPointer], '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$') + if start1 == nil then + error('Invalid patch string: "' .. text[textPointer] .. '"') + end + local patch = _new_patch_obj() + patches[#patches + 1] = patch + + start1 = tonumber(start1) + length1 = tonumber(length1) or 1 + if length1 == 0 then + start1 = start1 + 1 + end + patch.start1 = start1 + patch.length1 = length1 + + start2 = tonumber(start2) + length2 = tonumber(length2) or 1 + if length2 == 0 then + start2 = start2 + 1 + end + patch.start2 = start2 + patch.length2 = length2 + + textPointer = textPointer + 1 + + while true do + local line = text[textPointer] + if line == nil then + break + end + local sign + sign, line = strsub(line, 1, 1), strsub(line, 2) + + local invalidDecode = false + local decoded = gsub(line, '%%(.?.?)', function(c) + local n = tonumber(c, 16) + if (#c ~= 2) or (n == nil) then + invalidDecode = true + return '' + end + return strchar(n) + end) + if invalidDecode then + -- Malformed URI sequence. + error('Illegal escape in patch_fromText: ' .. line) + end + + line = decoded + + if sign == '-' then + -- Deletion. + patch.diffs[#patch.diffs + 1] = { DIFF_DELETE, line } + elseif sign == '+' then + -- Insertion. + patch.diffs[#patch.diffs + 1] = { DIFF_INSERT, line } + elseif sign == ' ' then + -- Minor equality. + patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, line } + elseif sign == '@' then + -- Start of next patch. + break + elseif sign == '' then + -- Blank line? Whatever. + else + -- WTF? + error('Invalid patch mode "' .. sign .. '" in: ' .. line) + end + textPointer = textPointer + 1 + end + end + return patches +end + +-- --------------------------------------------------------------------------- +-- UNOFFICIAL/PRIVATE PATCH FUNCTIONS +-- --------------------------------------------------------------------------- + +local patch_meta = { + __tostring = function(patch) + local buf = {} + _patch_appendText(patch, buf) + return tconcat(buf) + end, +} + +--[[ +* Class representing one patch operation. +* @constructor +--]] +function _new_patch_obj() + return setmetatable({ + --[[ @type {Array.>} ]] + diffs = {}, + --[[ @type {?number} ]] + start1 = 1, -- nil; + --[[ @type {?number} ]] + start2 = 1, -- nil; + --[[ @type {number} ]] + length1 = 0, + --[[ @type {number} ]] + length2 = 0, + }, patch_meta) +end + +--[[ +* Increase the context until it is unique, +* but don't let the pattern expand beyond Match_MaxBits. +* @param {_new_patch_obj} patch The patch to grow. +* @param {string} text Source text. +* @private +--]] +function _patch_addContext(patch, text) + if #text == 0 then + return + end + local pattern = strsub(text, patch.start2, patch.start2 + patch.length1 - 1) + local padding = 0 + + -- LUANOTE: Lua's lack of a lastIndexOf function results in slightly + -- different logic here than in other language ports. + -- Look for the first two matches of pattern in text. If two are found, + -- increase the pattern length. + local firstMatch = indexOf(text, pattern) + local secondMatch = nil + if firstMatch ~= nil then + secondMatch = indexOf(text, pattern, firstMatch + 1) + end + while (#pattern == 0 or secondMatch ~= nil) and (#pattern < Match_MaxBits - Patch_Margin - Patch_Margin) do + padding = padding + Patch_Margin + pattern = strsub(text, max(1, patch.start2 - padding), patch.start2 + patch.length1 - 1 + padding) + firstMatch = indexOf(text, pattern) + if firstMatch ~= nil then + secondMatch = indexOf(text, pattern, firstMatch + 1) + else + secondMatch = nil + end + end + -- Add one chunk for good luck. + padding = padding + Patch_Margin + + -- Add the prefix. + local prefix = strsub(text, max(1, patch.start2 - padding), patch.start2 - 1) + if #prefix > 0 then + tinsert(patch.diffs, 1, { DIFF_EQUAL, prefix }) + end + -- Add the suffix. + local suffix = strsub(text, patch.start2 + patch.length1, patch.start2 + patch.length1 - 1 + padding) + if #suffix > 0 then + patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, suffix } + end + + -- Roll back the start points. + patch.start1 = patch.start1 - #prefix + patch.start2 = patch.start2 - #prefix + -- Extend the lengths. + patch.length1 = patch.length1 + #prefix + #suffix + patch.length2 = patch.length2 + #prefix + #suffix +end + +--[[ +* Given an array of patches, return another array that is identical. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @return {Array.<_new_patch_obj>} Array of patch objects. +--]] +function _patch_deepCopy(patches) + local patchesCopy = {} + for x, patch in ipairs(patches) do + local patchCopy = _new_patch_obj() + local diffsCopy = {} + for i, diff in ipairs(patch.diffs) do + diffsCopy[i] = { diff[1], diff[2] } + end + patchCopy.diffs = diffsCopy + patchCopy.start1 = patch.start1 + patchCopy.start2 = patch.start2 + patchCopy.length1 = patch.length1 + patchCopy.length2 = patch.length2 + patchesCopy[x] = patchCopy + end + return patchesCopy +end + +--[[ +* Add some padding on text start and end so that edges can match something. +* Intended to be called only from within patch_apply. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +* @return {string} The padding string added to each side. +--]] +function _patch_addPadding(patches) + local paddingLength = Patch_Margin + local nullPadding = '' + for x = 1, paddingLength do + nullPadding = nullPadding .. strchar(x) + end + + -- Bump all the patches forward. + for x, patch in ipairs(patches) do + patch.start1 = patch.start1 + paddingLength + patch.start2 = patch.start2 + paddingLength + end + + -- Add some padding on start of first diff. + local patch = patches[1] + local diffs = patch.diffs + local firstDiff = diffs[1] + if (firstDiff == nil) or (firstDiff[1] ~= DIFF_EQUAL) then + -- Add nullPadding equality. + tinsert(diffs, 1, { DIFF_EQUAL, nullPadding }) + patch.start1 = patch.start1 - paddingLength -- Should be 0. + patch.start2 = patch.start2 - paddingLength -- Should be 0. + patch.length1 = patch.length1 + paddingLength + patch.length2 = patch.length2 + paddingLength + elseif paddingLength > #firstDiff[2] then + -- Grow first equality. + local extraLength = paddingLength - #firstDiff[2] + firstDiff[2] = strsub(nullPadding, #firstDiff[2] + 1) .. firstDiff[2] + patch.start1 = patch.start1 - extraLength + patch.start2 = patch.start2 - extraLength + patch.length1 = patch.length1 + extraLength + patch.length2 = patch.length2 + extraLength + end + + -- Add some padding on end of last diff. + patch = patches[#patches] + diffs = patch.diffs + local lastDiff = diffs[#diffs] + if (lastDiff == nil) or (lastDiff[1] ~= DIFF_EQUAL) then + -- Add nullPadding equality. + diffs[#diffs + 1] = { DIFF_EQUAL, nullPadding } + patch.length1 = patch.length1 + paddingLength + patch.length2 = patch.length2 + paddingLength + elseif paddingLength > #lastDiff[2] then + -- Grow last equality. + local extraLength = paddingLength - #lastDiff[2] + lastDiff[2] = lastDiff[2] .. strsub(nullPadding, 1, extraLength) + patch.length1 = patch.length1 + extraLength + patch.length2 = patch.length2 + extraLength + end + + return nullPadding +end + +--[[ +* Look through the patches and break up any which are longer than the maximum +* limit of the match algorithm. +* Intended to be called only from within patch_apply. +* @param {Array.<_new_patch_obj>} patches Array of patch objects. +--]] +function _patch_splitMax(patches) + local patch_size = Match_MaxBits + local x = 1 + while true do + local patch = patches[x] + if patch == nil then + return + end + if patch.length1 > patch_size then + local bigpatch = patch + -- Remove the big old patch. + tremove(patches, x) + x = x - 1 + local start1 = bigpatch.start1 + local start2 = bigpatch.start2 + local precontext = '' + while bigpatch.diffs[1] do + -- Create one of several smaller patches. + local patch = _new_patch_obj() + local empty = true + patch.start1 = start1 - #precontext + patch.start2 = start2 - #precontext + if precontext ~= '' then + patch.length1, patch.length2 = #precontext, #precontext + patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, precontext } + end + while bigpatch.diffs[1] and (patch.length1 < patch_size - Patch_Margin) do + local diff_type = bigpatch.diffs[1][1] + local diff_text = bigpatch.diffs[1][2] + if diff_type == DIFF_INSERT then + -- Insertions are harmless. + patch.length2 = patch.length2 + #diff_text + start2 = start2 + #diff_text + patch.diffs[#patch.diffs + 1] = bigpatch.diffs[1] + tremove(bigpatch.diffs, 1) + empty = false + elseif + (diff_type == DIFF_DELETE) + and (#patch.diffs == 1) + and (patch.diffs[1][1] == DIFF_EQUAL) + and (#diff_text > 2 * patch_size) + then + -- This is a large deletion. Let it pass in one chunk. + patch.length1 = patch.length1 + #diff_text + start1 = start1 + #diff_text + empty = false + patch.diffs[#patch.diffs + 1] = { diff_type, diff_text } + tremove(bigpatch.diffs, 1) + else + -- Deletion or equality. + -- Only take as much as we can stomach. + diff_text = strsub(diff_text, 1, patch_size - patch.length1 - Patch_Margin) + patch.length1 = patch.length1 + #diff_text + start1 = start1 + #diff_text + if diff_type == DIFF_EQUAL then + patch.length2 = patch.length2 + #diff_text + start2 = start2 + #diff_text + else + empty = false + end + patch.diffs[#patch.diffs + 1] = { diff_type, diff_text } + if diff_text == bigpatch.diffs[1][2] then + tremove(bigpatch.diffs, 1) + else + bigpatch.diffs[1][2] = strsub(bigpatch.diffs[1][2], #diff_text + 1) + end + end + end + -- Compute the head context for the next patch. + precontext = _diff_text2(patch.diffs) + precontext = strsub(precontext, -Patch_Margin) + -- Append the end context for this patch. + local postcontext = strsub(_diff_text1(bigpatch.diffs), 1, Patch_Margin) + if postcontext ~= '' then + patch.length1 = patch.length1 + #postcontext + patch.length2 = patch.length2 + #postcontext + if patch.diffs[1] and (patch.diffs[#patch.diffs][1] == DIFF_EQUAL) then + patch.diffs[#patch.diffs][2] = patch.diffs[#patch.diffs][2] .. postcontext + else + patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, postcontext } + end + end + if not empty then + x = x + 1 + tinsert(patches, x, patch) + end + end + end + x = x + 1 + end +end + +--[[ +* Emulate GNU diff's format. +* Header: @@ -382,8 +481,9 @@ +* @return {string} The GNU diff string. +--]] +function _patch_appendText(patch, text) + local coords1, coords2 + local length1, length2 = patch.length1, patch.length2 + local start1, start2 = patch.start1, patch.start2 + local diffs = patch.diffs + + if length1 == 1 then + coords1 = start1 + else + coords1 = ((length1 == 0) and (start1 - 1) or start1) .. ',' .. length1 + end + + if length2 == 1 then + coords2 = start2 + else + coords2 = ((length2 == 0) and (start2 - 1) or start2) .. ',' .. length2 + end + text[#text + 1] = '@@ -' .. coords1 .. ' +' .. coords2 .. ' @@\n' + + local op + -- Escape the body of the patch with %xx notation. + for x, diff in ipairs(patch.diffs) do + local diff_type = diff[1] + if diff_type == DIFF_INSERT then + op = '+' + elseif diff_type == DIFF_DELETE then + op = '-' + elseif diff_type == DIFF_EQUAL then + op = ' ' + end + text[#text + 1] = op .. gsub(diffs[x][2], percentEncode_pattern, percentEncode_replace) .. '\n' + end + + return text +end + +-- Expose the API +local _M = {} + +_M.DIFF_DELETE = DIFF_DELETE +_M.DIFF_INSERT = DIFF_INSERT +_M.DIFF_EQUAL = DIFF_EQUAL + +_M.diff_main = diff_main +_M.diff_cleanupSemantic = diff_cleanupSemantic +_M.diff_cleanupEfficiency = diff_cleanupEfficiency +_M.diff_levenshtein = diff_levenshtein +_M.diff_prettyHtml = diff_prettyHtml + +_M.match_main = match_main + +_M.patch_make = patch_make +_M.patch_toText = patch_toText +_M.patch_fromText = patch_fromText +_M.patch_apply = patch_apply + +-- Expose some non-API functions as well, for testing purposes etc. +_M.diff_commonPrefix = _diff_commonPrefix +_M.diff_commonSuffix = _diff_commonSuffix +_M.diff_commonOverlap = _diff_commonOverlap +_M.diff_halfMatch = _diff_halfMatch +_M.diff_bisect = _diff_bisect +_M.diff_cleanupMerge = _diff_cleanupMerge +_M.diff_cleanupSemanticLossless = _diff_cleanupSemanticLossless +_M.diff_text1 = _diff_text1 +_M.diff_text2 = _diff_text2 +_M.diff_toDelta = _diff_toDelta +_M.diff_fromDelta = _diff_fromDelta +_M.diff_xIndex = _diff_xIndex +_M.match_alphabet = _match_alphabet +_M.match_bitap = _match_bitap +_M.new_patch_obj = _new_patch_obj +_M.patch_addContext = _patch_addContext +_M.patch_splitMax = _patch_splitMax +_M.patch_addPadding = _patch_addPadding +_M.settings = settings + +return _M diff --git a/tests/diff_spec.lua b/tests/diff_spec.lua index 62866c40..cdea4c1b 100644 --- a/tests/diff_spec.lua +++ b/tests/diff_spec.lua @@ -1,22 +1,6 @@ local diff = require('CopilotChat.utils.diff') describe('CopilotChat.utils.diff', function() - it('parses unified diff', function() - local diff_text = [[ ---- a/foo.txt -+++ b/foo.txt -@@ ... @@ - context line --old line -+new line -]] - local file_path, hunks = diff.parse_unified_diff(diff_text) - assert.equals('b/foo.txt', file_path) - assert.equals('context line', hunks[1].context[1]) - assert.equals('old line', hunks[1].minus[1]) - assert.equals('new line', hunks[1].plus[1]) - end) - it('applies unified diff', function() local diff_text = [[ --- a/foo.txt @@ -27,26 +11,12 @@ describe('CopilotChat.utils.diff', function() +new ]] local original = { 'context', 'old', 'other' } - local result, applied = diff.apply_unified_diff(diff_text, original) + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) assert.is_true(applied) assert.are.same({ 'context', 'new', 'other' }, result) end) - it('gets unified diff region', function() - local diff_text = [[ ---- a/foo.txt -+++ b/foo.txt -@@ ... @@ - context --old -+new -]] - local original = { 'context', 'old', 'other' } - local first, last = diff.get_unified_diff_region(diff_text, original) - assert.equals(2, first) - assert.equals(2, last) - end) - it('applies unified diff with no context', function() local diff_text = [[ --- a/foo.txt @@ -56,7 +26,8 @@ describe('CopilotChat.utils.diff', function() +new ]] local original = { 'old', 'other' } - local result, applied = diff.apply_unified_diff(diff_text, original) + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) assert.is_true(applied) assert.are.same({ 'new', 'other' }, result) end) @@ -81,7 +52,8 @@ describe('CopilotChat.utils.diff', function() 'context3', 'other', } - local result, applied = diff.apply_unified_diff(diff_text, original) + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) assert.is_true(applied) assert.are.same({ 'context1', @@ -93,7 +65,7 @@ describe('CopilotChat.utils.diff', function() }, result) end) - it('does not apply ambiguous edit', function() + it('gets unified diff region', function() local diff_text = [[ --- a/foo.txt +++ b/foo.txt @@ -102,10 +74,159 @@ describe('CopilotChat.utils.diff', function() -old +new ]] - local original = { 'context', 'old', 'context', 'old' } - local result, applied = diff.apply_unified_diff(diff_text, original) - -- Should not apply because there are two possible matches - assert.is_false(applied) - assert.are.same({ 'context', 'old', 'context', 'old' }, result) + local original = { 'context', 'old', 'other' } + local original_content = table.concat(original, '\n') + local _, _, first, last = diff.apply_unified_diff(diff_text, original_content) + assert.equals(2, first) + assert.equals(2, last) + end) + + it('applies unified diff with only additions', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context ++added1 ++added2 +]] + local original = { 'context', 'other' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'context', 'added1', 'added2', 'other' }, result) + end) + + it('applies unified diff with only deletions', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context +-old1 +-old2 +]] + local original = { 'context', 'old1', 'old2', 'other' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'context', 'other' }, result) + end) + + it('applies unified diff with changes at start and end', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ +-oldstart ++newstart + context +-oldend ++newend +]] + local original = { 'oldstart', 'context', 'oldend' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'newstart', 'context', 'newend' }, result) + end) + + it('applies unified diff with multiple hunks', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context1 +-old1 ++new1 +@@ ... @@ + context2 +-old2 ++new2 +]] + local original = { 'context1', 'old1', 'context2', 'old2', 'other' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'context1', 'new1', 'context2', 'new2', 'other' }, result) + end) + + it('applies unified diff with no changes', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context + unchanged +]] + local original = { 'context', 'unchanged' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same(original, result) + end) + + it('applies unified diff with all lines deleted', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ +-old1 +-old2 +-old3 +]] + local original = { 'old1', 'old2', 'old3' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ '' }, result) + end) + + it('applies unified diff with all lines added to empty file', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ ++new1 ++new2 ++new3 +]] + local original = {} + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'new1', 'new2', 'new3' }, result) + end) + + it('applies unified diff with changes at end of file', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ + context +-oldend ++newend +]] + local original = { 'context', 'oldend' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'context', 'newend' }, result) + end) + + it('applies unified diff with changes at start of file', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ ... @@ +-oldstart ++newstart + context +]] + local original = { 'oldstart', 'context' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'newstart', 'context' }, result) end) end) From 06af2b3a6fd89892a80ebd62d4c1d1a64031d8ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Sep 2025 05:37:42 +0000 Subject: [PATCH 598/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 456e34a1..d1a712ba 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 12 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 13 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -31,9 +31,11 @@ Table of Contents *CopilotChat-table-of-contents* 6. Development |CopilotChat-development| - Setup |CopilotChat-setup| - Contributing |CopilotChat-contributing| -7. Contributors |CopilotChat-contributors| -8. Stargazers |CopilotChat-stargazers| -9. Links |CopilotChat-links| +7. Acknowledgments |CopilotChat-acknowledgments| + - diff-match-patch |CopilotChat-diff-match-patch| +8. Contributors |CopilotChat-contributors| +9. Stargazers |CopilotChat-stargazers| +10. Links |CopilotChat-links| CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. @@ -597,7 +599,19 @@ See CONTRIBUTING.md for detailed guidelines. ============================================================================== -7. Contributors *CopilotChat-contributors* +7. Acknowledgments *CopilotChat-acknowledgments* + + +DIFF-MATCH-PATCH *CopilotChat-diff-match-patch* + +CopilotChat.nvim includes diff-match-patch (Lua port) + for diffing and patching +functionality. Copyright 2018 The diff-match-patch Authors. Licensed under the +Apache License 2.0. + + +============================================================================== +8. Contributors *CopilotChat-contributors* Thanks goes to these wonderful people (emoji key ): @@ -608,12 +622,12 @@ Contributions of any kind are welcome! ============================================================================== -8. Stargazers *CopilotChat-stargazers* +9. Stargazers *CopilotChat-stargazers* ============================================================================== -9. Links *CopilotChat-links* +10. Links *CopilotChat-links* 1. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg?variant=adaptive From 19a4d2990fe1f8c0697636059500d521793cede0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 07:41:11 +0200 Subject: [PATCH 599/728] refactor(diff): use plenary.log for debug logging (#1408) Replace vim.notify with plenary.log.debug in diff apply function to avoid user-facing warnings and improve logging granularity. This change helps keep the UI clean while still providing debug information for failed diff applications. --- lua/CopilotChat/utils/diff.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 86449c97..7e862c54 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -1,3 +1,5 @@ +local log = require('plenary.log') + local M = {} --- Parse unified diff hunks from diff text @@ -173,7 +175,7 @@ function M.apply_diff(block, bufnr) local diff, content = M.get_diff(block, bufnr) local new_lines, applied, _, _ = M.apply_unified_diff(diff, content) if not applied then - vim.notify('Diff for ' .. block.header.filename .. ' failed to apply cleanly for:\n' .. diff, vim.log.levels.WARN) + log.debug('Diff for ' .. block.header.filename .. ' failed to apply cleanly for:\n' .. diff) end return new_lines From a88874ef3663aea6bc09eb09c1df4a46ae8577f5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 14:12:58 +0200 Subject: [PATCH 600/728] feat(diff): apply all code blocks for a file at once when showing diff (#1409) Refactored diff preview logic to process all code blocks for a file in one pass, improving consistency and correctness. Updated diff utility functions to accept lines instead of buffer numbers, and adjusted context lengths for more accurate region detection. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 28 ++++++++++++++++++++++++---- lua/CopilotChat/utils/diff.lua | 23 +++++++++++------------ tests/diff_spec.lua | 2 +- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 05122044..74429e33 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -172,9 +172,10 @@ return { local path = block.header.filename local bufnr = prepare_diff_buffer(path, source) - local new_lines = diff.apply_diff(block, bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local new_lines = diff.apply_diff(block, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines) - local first, last = diff.get_diff_region(block, bufnr) + local first, last = diff.get_diff_region(block, lines) if first and last then select.set(bufnr, source.winnr, first, last) select.highlight(bufnr) @@ -192,7 +193,8 @@ return { local path = block.header.filename local bufnr = prepare_diff_buffer(path, source) - local first, last = diff.get_diff_region(block, bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local first, last = diff.get_diff_region(block, lines) if first and last and bufnr then select.set(bufnr, source.winnr, first, last) select.highlight(bufnr) @@ -223,7 +225,25 @@ return { local path = block.header.filename local bufnr = prepare_diff_buffer(path, source) - local new_lines = diff.apply_diff(block, bufnr) + + -- Collect all blocks for the same filename + local message = copilot.chat:get_message(constants.ROLE.ASSISTANT, true) + local blocks = {} + if message and message.section and message.section.blocks then + for _, b in ipairs(message.section.blocks) do + if b.header.filename == path then + table.insert(blocks, b) + end + end + else + blocks = { block } + end + + -- Apply all diffs for the filename + local new_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + for i = #blocks, 1, -1 do + new_lines = diff.apply_diff(blocks[i], new_lines) + end local opts = { filetype = vim.bo[bufnr].filetype, diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 7e862c54..6ff56bac 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -120,8 +120,8 @@ function M.apply_unified_diff(diff_text, original_content) new_content = patched applied = applied or ok end - local original_lines = vim.split(original_content, '\n') - local new_lines = vim.split(new_content, '\n') + local original_lines = vim.split(original_content, '\n', { trimempty = true }) + local new_lines = vim.split(new_content, '\n', { trimempty = true }) local first, last local max_len = math.max(#original_lines, #new_lines) for i = 1, max_len do @@ -137,10 +137,9 @@ end --- Get diff from block content and buffer lines ---@param block CopilotChat.ui.chat.Block Block containing diff info ----@param bufnr integer Buffer number +---@param lines table table of lines ---@return string diff, string content -function M.get_diff(block, bufnr) - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) +function M.get_diff(block, lines) local content = table.concat(lines, '\n') if block.header.filetype == 'diff' then return block.content, content @@ -161,7 +160,7 @@ function M.get_diff(block, bufnr) vim.diff( table.concat(original_lines, '\n'), table.concat(patched_lines, '\n'), - { algorithm = 'myers', ctxlen = 20, interhunkctxlen = 50, ignore_whitespace_change = true } + { algorithm = 'myers', ctxlen = 10, interhunkctxlen = 10, ignore_whitespace_change = true } ) ), content @@ -169,10 +168,10 @@ end --- Apply a diff (unified or indices) to buffer lines ---@param block CopilotChat.ui.chat.Block Block containing diff info ----@param bufnr integer Buffer number +---@param lines table table of lines ---@return table new_lines -function M.apply_diff(block, bufnr) - local diff, content = M.get_diff(block, bufnr) +function M.apply_diff(block, lines) + local diff, content = M.get_diff(block, lines) local new_lines, applied, _, _ = M.apply_unified_diff(diff, content) if not applied then log.debug('Diff for ' .. block.header.filename .. ' failed to apply cleanly for:\n' .. diff) @@ -183,10 +182,10 @@ end --- Get changed region for diff (unified or indices) ---@param block CopilotChat.ui.chat.Block Block containing diff info ----@param bufnr integer Buffer number +---@param lines table table of lines ---@return number? first, number? last -function M.get_diff_region(block, bufnr) - local diff, content = M.get_diff(block, bufnr) +function M.get_diff_region(block, lines) + local diff, content = M.get_diff(block, lines) local _, _, first, last = M.apply_unified_diff(diff, content) return first, last end diff --git a/tests/diff_spec.lua b/tests/diff_spec.lua index cdea4c1b..58e2f4c9 100644 --- a/tests/diff_spec.lua +++ b/tests/diff_spec.lua @@ -179,7 +179,7 @@ describe('CopilotChat.utils.diff', function() local original_content = table.concat(original, '\n') local result, applied = diff.apply_unified_diff(diff_text, original_content) assert.is_true(applied) - assert.are.same({ '' }, result) + assert.are.same({}, result) end) it('applies unified diff with all lines added to empty file', function() From 00d0fb310ad364e76e306a6626a40b85fc5bbd98 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 14:41:37 +0200 Subject: [PATCH 601/728] fix(chat): automatically start treesitter if not started (#1410) As treesitter is now a requirement, it needs to be started automatically. nvim-treesitter does not auto start it anymore on latest branch so we need to do it ourselves. Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 3cf5efc7..3389ea5f 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -117,6 +117,7 @@ end ---@field private separator string ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay +---@field private last_changedtick number? local Chat = class(function(self, config, on_buf_create) Overlay.init(self, 'copilot-chat', utils.key_to_info('show_help', config.mappings.show_help), on_buf_create) @@ -616,6 +617,12 @@ function Chat:create() local bufnr = Overlay.create(self) vim.bo[bufnr].syntax = 'markdown' vim.bo[bufnr].textwidth = 0 + vim.bo[bufnr].undolevels = 10 + self.spinner.bufnr = bufnr + + vim.schedule(function() + pcall(vim.treesitter.start, bufnr) + end) vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { buffer = bufnr, @@ -627,7 +634,6 @@ function Chat:create() end, }) - self.spinner.bufnr = bufnr return bufnr end @@ -647,10 +653,10 @@ function Chat:parse() -- Skip parsing if buffer hasn't changed local changedtick = vim.api.nvim_buf_get_changedtick(self.bufnr) - if self._last_changedtick == changedtick then + if self.last_changedtick == changedtick then return false end - self._last_changedtick = changedtick + self.last_changedtick = changedtick local parser = vim.treesitter.get_parser(self.bufnr, 'markdown') if not parser then From 559e75423774b3a291a58d33a1144c94444e52ac Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 15:05:16 +0200 Subject: [PATCH 602/728] fix(ui): improve help rendering and treesitter usage (#1411) - Avoid starting treesitter if markdown parser already exists - Fix help message line positioning in chat - Render help virtual lines above for better visibility --- lua/CopilotChat/ui/chat.lua | 6 ++++-- lua/CopilotChat/ui/overlay.lua | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 3389ea5f..99bc7fbf 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -621,7 +621,9 @@ function Chat:create() self.spinner.bufnr = bufnr vim.schedule(function() - pcall(vim.treesitter.start, bufnr) + if not vim.treesitter.get_parser(bufnr, 'markdown', {}) then + pcall(vim.treesitter.start, bufnr) + end end) vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { @@ -906,7 +908,7 @@ function Chat:render() end msg = msg .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' end - self:show_help(msg, message.section.start_line - 1) + self:show_help(msg, message.section.start_line) end -- Auto fold non-assistant messages if enabled diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 7a56c33b..547394b8 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -143,6 +143,7 @@ function Overlay:show_help(msg, pos) id = 1, hl_mode = 'combine', priority = 100, + virt_lines_above = true, virt_lines = vim.tbl_map(function(t) return { { t, 'CopilotChatHelp' } } end, vim.split(msg, '\n')), From 0514e7d1944f0caddca3fcb111a8d20c52a6dd18 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 16:35:23 +0200 Subject: [PATCH 603/728] refactor(instructions): simplify edit file block format (#1412) Streamlined instructions for presenting file edits in code blocks. Reduced redundancy, clarified formatting, and emphasized minimal, precise changes. Multiple changes are now presented as separate code blocks for clarity. --- .../instructions/edit_file_block.lua | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/lua/CopilotChat/instructions/edit_file_block.lua b/lua/CopilotChat/instructions/edit_file_block.lua index 8abc8719..f5f9bf9e 100644 --- a/lua/CopilotChat/instructions/edit_file_block.lua +++ b/lua/CopilotChat/instructions/edit_file_block.lua @@ -1,41 +1,26 @@ return [[ -Use these instructions when editing files via code blocks. Your goal is to produce clear, minimal, and precise file edits. +Use these instructions when editing files via code blocks. Present changes as clear, minimal, and precise file edits. -Steps for presenting code changes: -1. For each change, use the following markdown code block format with triple backticks: - ``` path= start_line= end_line= - - ``` +For each change, use this markdown code block format: +``` path= start_line= end_line= + +``` -2. Examples: - ```lua path={DIR}/lua/CopilotChat/init.lua start_line=40 end_line=50 - local function example() - print("This is an example function.") - end - ``` +Example: +```lua path={DIR}/lua/CopilotChat/init.lua start_line=40 end_line=50 +local function example() + print("This is an example function.") +end +``` - ```python path={DIR}/scripts/example.py start_line=10 end_line=15 - def example_function(): - print("This is an example function.") - ``` +Code content requirements: +Always use absolute file paths in headers. Convert relative paths to absolute by prefixing with {DIR}. +Keep changes minimal and focused. Include complete replacement code for the specified line range. +Use proper indentation matching the source file. Include all necessary lines without eliding code. +NEVER include line number prefixes in output code blocks - output only valid code as it should appear in the file. +Address any diagnostics issues when fixing code. - ```json path={DIR}/config/settings.json start_line=5 end_line=8 - { - "setting": "value", - "enabled": true - } - ``` - -3. Requirements for code content: - - Always use the absolute file path in the code block header. If the path is not already absolute, convert it to an absolute path prefixed by {DIR}. - - Keep changes minimal and focused to produce short diffs - - Include complete replacement code for the specified line range - - Proper indentation matching the source - - All necessary lines (no eliding with comments) - - **Never include line number prefixes in your output code blocks. Only output valid code, exactly as it should appear in the file. Line numbers are only allowed in the code block header.** - - Address any diagnostics issues when fixing code - -4. If multiple changes are needed, present them as separate code blocks. +Present multiple changes as separate code blocks. ]] From c15f65e5dc5151230c97f9fd4d386e513fc47c63 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 19:01:08 +0200 Subject: [PATCH 604/728] perf(core): do not require calling setup(), add lazy initialization (#1413) Refactored plugin initialization to use lazy loading via metatable, setting an `initialized` flag and moving setup logic to `init()`. Replaced `add_providers` with `set_providers` for clarity. Improved health check to verify initialization. Cleaned up chat state handling and logging logic for better reliability and maintainability. Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 2 +- lua/CopilotChat/health.lua | 8 +- lua/CopilotChat/init.lua | 143 ++++++++++++++++++++--------------- lua/CopilotChat/tiktoken.lua | 1 - 4 files changed, 87 insertions(+), 67 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 5cd45ffc..cf240a55 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -211,7 +211,7 @@ end --- Set a provider resolver on the client ---@param resolver function: A function that returns a table of providers -function Client:add_providers(resolver) +function Client:set_providers(resolver) self.provider_resolver = resolver end diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 0c3bcfe9..3a67e706 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -57,11 +57,11 @@ function M.check() error('nvim: unsupported, please upgrade to 0.10.0 or later. See "https://neovim.io/".') end - local setup_called = require('CopilotChat').config ~= nil - if setup_called then - ok('setup: called') + local initialized = require('CopilotChat').initialized + if initialized then + ok('initialized: true') else - error('setup: not called, required for plugin to work. See `:h CopilotChat-installation`.') + error('initialized: false, something went wrong. See `:h CopilotChat-installation`.') end local testfile = os.tmpname() diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5dc129af..f473750a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -23,6 +23,14 @@ local M = setmetatable({}, { if key == 'config' then return require('CopilotChat.config') end + + -- Lazy initialize + local initialized = rawget(t, 'initialized') + if not initialized then + rawset(t, 'initialized', true) + rawget(t, 'init')() + end + return rawget(t, key) end, }) @@ -33,8 +41,8 @@ local M = setmetatable({}, { --- @field cwd fun():string --- @class CopilotChat.state ---- @field source CopilotChat.source? ---- @field sticky string[]? +--- @field source CopilotChat.source +--- @field sticky string[] local state = { source = { bufnr = nil, @@ -44,7 +52,7 @@ local state = { end, }, - sticky = nil, + sticky = {}, } --- Insert sticky values from config into prompt @@ -1023,28 +1031,32 @@ function M.log_level(level) M.config.log_level = level M.config.debug = level == 'debug' - log.new({ - plugin = constants.PLUGIN_NAME, - level = level, - outfile = M.config.log_path, - fmt_msg = function(is_console, mode_name, src_path, src_line, msg) - local nameupper = mode_name:upper() - if is_console then - return string.format('[%s] %s', nameupper, msg) - else - local lineinfo = src_path .. ':' .. src_line - return string.format('[%-6s%s] %s: %s\n', nameupper, os.date(), lineinfo, msg) - end - end, - }, true) + if level ~= log.level then + log.new({ + plugin = constants.PLUGIN_NAME, + level = level, + outfile = M.config.log_path, + fmt_msg = function(is_console, mode_name, src_path, src_line, msg) + local nameupper = mode_name:upper() + if is_console then + return string.format('[%s] %s', nameupper, msg) + else + local lineinfo = src_path .. ':' .. src_line + return string.format('[%-6s%s] %s: %s\n', nameupper, os.date(), lineinfo, msg) + end + end, + }, true) + log.level = level + end end ---- Set up the plugin ----@param config CopilotChat.config.Config? -function M.setup(config) - -- Little bit of update magic - for k, v in pairs(vim.tbl_deep_extend('force', M.config, config or {})) do - M.config[k] = v +--- Initialize the plugin if not already initialized. +function M.init() + -- Set log level + if M.config.debug then + M.log_level('debug') + else + M.log_level(M.config.log_level) end -- Save proxy and insecure settings @@ -1054,15 +1066,53 @@ function M.setup(config) }) -- Load the providers - client:stop() - client:add_providers(function() + client:set_providers(function() return M.config.providers end) - if M.config.debug then - M.log_level('debug') - else - M.log_level(M.config.log_level) + -- Initialize chat + if not M.chat then + M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) + for name, _ in pairs(M.config.mappings) do + map_key(name, bufnr) + end + + require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) + + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { + buffer = bufnr, + callback = function(ev) + if ev.event == 'BufEnter' then + update_source() + end + + vim.schedule(function() + select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) + end) + end, + }) + + if M.config.insert_at_end then + vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + buffer = bufnr, + callback = function() + vim.cmd('normal! 0') + vim.cmd('normal! G$') + vim.v.char = 'x' + end, + }) + end + + finish(true) + end) + end +end + +--- Set up the plugin +---@param config CopilotChat.config.Config? +function M.setup(config) + for k, v in pairs(vim.tbl_deep_extend('force', M.config, config or {})) do + M.config[k] = v end if not M.config.separator or M.config.separator == '' then @@ -1073,42 +1123,13 @@ function M.setup(config) end if M.chat then + client:stop() M.chat:close(state.source.bufnr) M.chat:delete() + M.chat = nil end - M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) - for name, _ in pairs(M.config.mappings) do - map_key(name, bufnr) - end - - require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) - - vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { - buffer = bufnr, - callback = function(ev) - if ev.event == 'BufEnter' then - update_source() - end - - vim.schedule(function() - select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) - end) - end, - }) - if M.config.insert_at_end then - vim.api.nvim_create_autocmd({ 'InsertEnter' }, { - buffer = bufnr, - callback = function() - vim.cmd('normal! 0') - vim.cmd('normal! G$') - vim.v.char = 'x' - end, - }) - end - - finish(true) - end) + M.init() for name, prompt in pairs(list_prompts()) do if prompt.prompt then diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 09ccaf37..2dd2914d 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,4 +1,3 @@ -local log = require('plenary.log') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') From e9e426d6f9199138513f6fc2bc9f1058690e97f5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 13 Sep 2025 20:49:58 +0200 Subject: [PATCH 605/728] refactor(chat): streamline message parsing logic (#1414) Move message parsing to get_messages, removing redundant parse calls from get_block, get_message, add_message, and remove_message. This simplifies the code and ensures messages are always parsed when retrieved, reducing unnecessary parsing and improving performance. Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 99bc7fbf..85b69d90 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -187,7 +187,6 @@ end ---@param cursor boolean? If true, returns the block closest to the cursor position ---@return CopilotChat.ui.chat.Block? function Chat:get_block(role, cursor) - self:parse() local messages = self:get_messages() if cursor then @@ -228,6 +227,7 @@ end --- Get list of all chat messages ---@return table function Chat:get_messages() + self:parse() return self.messages:values() end @@ -236,7 +236,6 @@ end ---@param cursor boolean? If true, returns the message closest to the cursor position ---@return CopilotChat.ui.chat.Message? function Chat:get_message(role, cursor) - self:parse() local messages = self:get_messages() if cursor then @@ -499,10 +498,7 @@ end ---@param message CopilotChat.ui.chat.Message ---@param replace boolean? If true, replaces the last message if it has same role function Chat:add_message(message, replace) - self:parse() - - local messages = self:get_messages() - local current_message = messages[#messages] + local current_message = self:get_message() local is_new = not current_message or current_message.role ~= message.role or (message.id and current_message.id ~= message.id) @@ -550,8 +546,6 @@ end ---@param role string? If specified, only considers sections of the given role ---@param cursor boolean? If true, removes the message closest to the cursor position function Chat:remove_message(role, cursor) - self:parse() - local message = self:get_message(role, cursor) if not message then return From d5ea51d3f55dc1941c13cf0c44440de0a7f8019f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 15 Sep 2025 17:31:20 +0200 Subject: [PATCH 606/728] fix(client): correct history handling for headless ask (#1416) Refactors ask request construction to properly handle history in headless mode. Removes prompt parameter from internal request generation and ensures the prompt is included as a user message only when appropriate. Also updates token counting and history trimming logic to avoid removing the current prompt in headless scenarios. Resolves: #1415 --- lua/CopilotChat/client.lua | 28 ++++++++-------------------- lua/CopilotChat/init.lua | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index cf240a55..93e1c91d 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -142,11 +142,10 @@ local function generate_resource_messages(resources) end --- Generate ask request ---- @param prompt string --- @param system_prompt string --- @param history table --- @param generated_messages table -local function generate_ask_request(prompt, system_prompt, history, generated_messages) +local function generate_ask_request(system_prompt, history, generated_messages) local messages = {} system_prompt = vim.trim(system_prompt) @@ -162,15 +161,6 @@ local function generate_ask_request(prompt, system_prompt, history, generated_me -- Include generated messages and history vim.list_extend(messages, generated_messages) vim.list_extend(messages, history) - - -- Include user prompt if we have no history - if not utils.empty(prompt) and utils.empty(history) then - table.insert(messages, { - content = prompt, - role = constants.ROLE.USER, - }) - end - return messages end @@ -303,10 +293,9 @@ function Client:info() end --- Ask a question to Copilot ----@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.client.AskOptions: Options for the request ---@return CopilotChat.client.AskResponse? -function Client:ask(prompt, opts) +function Client:ask(opts) opts = opts or {} local job_id = utils.uuid() @@ -350,20 +339,20 @@ function Client:ask(prompt, opts) notify.publish(notify.STATUS, 'Generating request') end - local history = not opts.headless and vim.deepcopy(opts.history) or {} + local history = vim.deepcopy(opts.history) local tool_calls = orderedmap() local generated_messages = {} local resource_messages = generate_resource_messages(opts.resources) if max_tokens then -- Count required tokens that we cannot reduce - local prompt_tokens = tiktoken:count(prompt) local system_tokens = tiktoken:count(opts.system_prompt) + local prompt_tokens = #history > 0 and tiktoken:count(history[#history].content) or 0 local resource_tokens = #resource_messages > 0 and tiktoken:count(resource_messages[1].content) or 0 local required_tokens = prompt_tokens + system_tokens + resource_tokens - log.debug('Prompt tokens:', prompt_tokens) log.debug('System tokens:', system_tokens) + log.debug('Prompt tokens:', prompt_tokens) log.debug('Resource tokens:', resource_tokens) -- Calculate how many tokens we can use for history @@ -373,8 +362,8 @@ function Client:ask(prompt, opts) history_tokens = history_tokens + tiktoken:count(msg.content) end - -- Remove history messages until we are under the limit - while history_tokens > history_limit and #history > 0 do + -- Remove history messages except prompt until we are under the limit + while history_tokens > history_limit and #history > 1 do local entry = table.remove(history, 1) history_tokens = history_tokens - tiktoken:count(entry.content) end @@ -522,8 +511,7 @@ function Client:ask(prompt, opts) end local headers = self:authenticate(provider_name) - local request = - provider.prepare_input(generate_ask_request(prompt, opts.system_prompt, history, generated_messages), options) + local request = provider.prepare_input(generate_ask_request(opts.system_prompt, history, generated_messages), options) local is_stream = request.stream local args = { diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f473750a..d50251f1 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -888,9 +888,20 @@ function M.ask(prompt, config) return end - local ask_response = client.ask(client, prompt, { + -- Build history, when in headless mode its just current prompt + local history + if not config.headless then + history = M.chat:get_messages() + else + history = { + content = prompt, + role = constants.ROLE.USER, + } + end + + local ask_response = client:ask({ headless = config.headless, - history = M.chat:get_messages(), + history = history, resources = resolved_resources, tools = selected_tools, system_prompt = system_prompt, From 87615648ff4dc852d1cf7ec099f0a7c37b1b2c87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Sep 2025 15:31:40 +0000 Subject: [PATCH 607/728] 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 d1a712ba..cadb7816 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 13 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 15 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From ed86374a1733601dfe79858e05033d62468f60e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 02:40:39 +0200 Subject: [PATCH 608/728] [pre-commit.ci] pre-commit autoupdate (#1418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.1.0 → v2.2.0](https://github.com/JohnnyMorganz/StyLua/compare/v2.1.0...v2.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 a90c2791..242a4e01 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: v2.1.0 + rev: v2.2.0 hooks: - id: stylua-github From 1bbe03470c52feed9eaccb5af4a64c1f2972e4a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 00:40:56 +0000 Subject: [PATCH 609/728] 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 cadb7816..bf35ba50 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 15 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 2279dbe42702397c969aeaa5aebae475a16bcaa9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 16 Sep 2025 02:51:00 +0200 Subject: [PATCH 610/728] fix(provider): safely call curl.post for model policy (#1419) Wrap curl.post in pcall to prevent errors when enabling model policy. This avoids potential crashes if the request fails. --- lua/CopilotChat/config/providers.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 3a4c7d24..5d0126e6 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -333,7 +333,7 @@ M.copilot = { for _, model in ipairs(models) do if not model.policy then - curl.post('https://api.githubcopilot.com/models/' .. model.id .. '/policy', { + pcall(curl.post, 'https://api.githubcopilot.com/models/' .. model.id .. '/policy', { headers = headers, json_request = true, body = { state = 'enabled' }, From 0c68c653061677320dc263f966db89bebb7621d7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 16 Sep 2025 02:53:44 +0200 Subject: [PATCH 611/728] refactor(config): simplify chat headers formatting (#1420) Remove redundant markdown-style prefixes from chat headers in config. This makes the headers cleaner and easier to customize. --- lua/CopilotChat/config.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 5261c70e..e43b1838 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -112,9 +112,9 @@ return { history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history headers = { - user = '## User ', -- Header to use for user questions - assistant = '## Copilot ', -- Header to use for AI answers - tool = '## Tool ', -- Header to use for tool calls + user = 'User', -- Header to use for user questions + assistant = 'Copilot', -- Header to use for AI answers + tool = 'Tool', -- Header to use for tool calls }, separator = '───', -- Separator to use in chat From b784122eddd127bf26dc4d3a375f5b1ed6848182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 02:57:51 +0200 Subject: [PATCH 612/728] chore(main): release 4.7.0 (#1398) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ version.txt | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ea1e6e..3cbea449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [4.7.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.6.0...v4.7.0) (2025-09-16) + + +### Features + +* **chat:** switch to treesitter based chat parsing ([#1394](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1394)) ([ba364fe](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/ba364fe04b36121a594435c3f54261c7a8e450a6)) +* **diff:** add experimental unified diff support, refactor handling ([#1392](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1392)) ([9fdf895](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9fdf8951efff6ab4f46e06945e5d6425bdbf4f80)) +* **diff:** apply all code blocks for a file at once when showing diff ([#1409](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1409)) ([a88874e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a88874ef3663aea6bc09eb09c1df4a46ae8577f5)) +* **diff:** use diff-match-patch for better diff handling ([#1407](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1407)) ([35ad8ff](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/35ad8ff61f47c5546c036b9b7310ce0dd87e8d20)) +* **health:** require markdown parser and copilotchat query ([#1401](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1401)) ([f49df19](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f49df19d5a8925d295ac6472c30b36584bd10d93)) + + +### Bug Fixes + +* **chat:** automatically start treesitter if not started ([#1410](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1410)) ([00d0fb3](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/00d0fb310ad364e76e306a6626a40b85fc5bbd98)) +* **client:** correct history handling for headless ask ([#1416](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1416)) ([d5ea51d](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d5ea51d3f55dc1941c13cf0c44440de0a7f8019f)), closes [#1415](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1415) +* **provider:** safely call curl.post for model policy ([#1419](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1419)) ([2279dbe](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/2279dbe42702397c969aeaa5aebae475a16bcaa9)) +* **ui:** handle missing filename in chat block header ([#1406](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1406)) ([5c3a558](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/5c3a558f2d740df740735fbb3ea0be822004136d)) +* **ui:** improve help rendering and treesitter usage ([#1411](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1411)) ([559e754](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/559e75423774b3a291a58d33a1144c94444e52ac)) +* **ui:** preserve extra fields in chat messages ([#1399](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1399)) ([f2f523f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f2f523fe3fdb855da1b3dcabf4f2981cdc3b2c2d)) + + +### Performance Improvements + +* **chat:** optimize message storage and access ([#1403](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1403)) ([1041ad0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1041ad0034e65e4a63859172d31e7045c8975d87)) +* **chat:** simplify last line/column calculation ([#1402](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1402)) ([4a45e69](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/4a45e69de8ad2b72ef62ede5a554c68c9632e718)) +* **core:** do not require calling setup(), add lazy initialization ([#1413](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1413)) ([c15f65e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c15f65e5dc5151230c97f9fd4d386e513fc47c63)) + ## [4.6.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.5.1...v4.6.0) (2025-08-31) diff --git a/version.txt b/version.txt index 6016e8ad..f6cdf409 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.6.0 +4.7.0 From 1b176f209a9ff85905fa5952898b1f9d4ad676cc Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 16 Sep 2025 09:18:49 +0200 Subject: [PATCH 613/728] refactor(init): merge init logic into setup for simplicity (#1422) Simplifies plugin initialization by merging the init logic into the setup function. Removes the separate init function and ensures all configuration, provider setup, and chat initialization are handled in setup. This reduces redundancy and makes the initialization flow clearer. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 104 ++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 56 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d50251f1..d8a3e378 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -28,7 +28,7 @@ local M = setmetatable({}, { local initialized = rawget(t, 'initialized') if not initialized then rawset(t, 'initialized', true) - rawget(t, 'init')() + rawget(t, 'setup')() end return rawget(t, key) @@ -1061,8 +1061,20 @@ function M.log_level(level) end end ---- Initialize the plugin if not already initialized. -function M.init() +--- Set up the plugin +---@param config CopilotChat.config.Config? +function M.setup(config) + for k, v in pairs(vim.tbl_deep_extend('force', M.config, config or {})) do + M.config[k] = v + end + + if not M.config.separator or M.config.separator == '' then + log.warn( + 'Empty separator is not allowed, using default separator instead. Set `separator` in config to change this.' + ) + M.config.separator = '---' + end + -- Set log level if M.config.debug then M.log_level('debug') @@ -1077,70 +1089,50 @@ function M.init() }) -- Load the providers + client:stop() client:set_providers(function() return M.config.providers end) -- Initialize chat - if not M.chat then - M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) - for name, _ in pairs(M.config.mappings) do - map_key(name, bufnr) - end - - require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) - - vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { - buffer = bufnr, - callback = function(ev) - if ev.event == 'BufEnter' then - update_source() - end - - vim.schedule(function() - select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) - end) - end, - }) + if M.chat then + M.chat:close(state.source.bufnr) + M.chat:delete() + end - if M.config.insert_at_end then - vim.api.nvim_create_autocmd({ 'InsertEnter' }, { - buffer = bufnr, - callback = function() - vim.cmd('normal! 0') - vim.cmd('normal! G$') - vim.v.char = 'x' - end, - }) - end + M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) + for name, _ in pairs(M.config.mappings) do + map_key(name, bufnr) + end - finish(true) - end) - end -end + require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) ---- Set up the plugin ----@param config CopilotChat.config.Config? -function M.setup(config) - for k, v in pairs(vim.tbl_deep_extend('force', M.config, config or {})) do - M.config[k] = v - end + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { + buffer = bufnr, + callback = function(ev) + if ev.event == 'BufEnter' then + update_source() + end - if not M.config.separator or M.config.separator == '' then - log.warn( - 'Empty separator is not allowed, using default separator instead. Set `separator` in config to change this.' - ) - M.config.separator = '---' - end + vim.schedule(function() + select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) + end) + end, + }) - if M.chat then - client:stop() - M.chat:close(state.source.bufnr) - M.chat:delete() - M.chat = nil - end + if M.config.insert_at_end then + vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + buffer = bufnr, + callback = function() + vim.cmd('normal! 0') + vim.cmd('normal! G$') + vim.v.char = 'x' + end, + }) + end - M.init() + finish(true) + end) for name, prompt in pairs(list_prompts()) do if prompt.prompt then From 9a63e83b9fade8e7fa50deb414d58b703352b13a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 16 Sep 2025 10:17:12 +0200 Subject: [PATCH 614/728] fix(ui): increase separator virt_text priority (#1424) Set the separator virtual text priority to 2000 to ensure it overrides other plugins' virtual text overlays in the chat UI (render-markdown.nvim uses 1000) --- lua/CopilotChat/ui/chat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 85b69d90..267b8aed 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -794,7 +794,7 @@ function Chat:render() { string.rep(self.separator, vim.go.columns - #header_value - 1), 'CopilotChatSeparator' }, }, virt_text_pos = 'overlay', - priority = 300, + priority = 2000, -- High priority to override other plugins if enabled strict = false, }) From 92dceb4ece955deea39fd1d7a57c26e66d5ce38d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 16 Sep 2025 16:53:10 +0200 Subject: [PATCH 615/728] fix(chat): ensure user prompt is wrapped in a list (#1427) Previously, the user prompt was not wrapped in a table when initializing the chat history, which could cause issues when the code expects a list of messages. This change ensures the prompt is always provided as a list with the correct structure. Closes #1426 --- 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 d8a3e378..98c1c1e6 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -894,8 +894,10 @@ function M.ask(prompt, config) history = M.chat:get_messages() else history = { - content = prompt, - role = constants.ROLE.USER, + { + content = prompt, + role = constants.ROLE.USER, + }, } end From 62426536f86dd617e9d72c7086f68ed8d5111e45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 09:18:51 +0200 Subject: [PATCH 616/728] chore(main): release 4.7.1 (#1425) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ version.txt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cbea449..894a8113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [4.7.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.0...v4.7.1) (2025-09-16) + + +### Bug Fixes + +* **chat:** ensure user prompt is wrapped in a list ([#1427](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1427)) ([92dceb4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/92dceb4ece955deea39fd1d7a57c26e66d5ce38d)), closes [#1426](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1426) +* **ui:** increase separator virt_text priority ([#1424](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1424)) ([9a63e83](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9a63e83b9fade8e7fa50deb414d58b703352b13a)) + ## [4.7.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.6.0...v4.7.0) (2025-09-16) diff --git a/version.txt b/version.txt index f6cdf409..7c66fca5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.7.0 +4.7.1 From e91c806f6fa23bd1ac455fb68e81b99e197f072f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Sep 2025 07:19:12 +0000 Subject: [PATCH 617/728] 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 bf35ba50..5f96c503 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 16 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From d3ddd226fc08a146412cc28c6c380372a086847d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 17 Sep 2025 10:30:14 +0200 Subject: [PATCH 618/728] docs(readme): update feature list for clarity and accuracy (#1431) Refined feature descriptions in README: - Clarified tool calling and privacy features - Updated interactive chat and token efficiency wording - Added scriptable Lua API highlight Improves transparency and better communicates capabilities. Signed-off-by: Tomas Slusny --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9f1cda76..8da6be23 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,12 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. - 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your [GitHub Copilot settings](https://github.com/settings/copilot/features) and the models provided by GitHub's API. -- 🔧 **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval -- 🔒 **Explicit Control** - Only shares what you specifically request - no background data collection -- 📝 **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration +- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with your explicit approval +- 🔒 **Privacy First** - Only shares what you explicitly request - no background data collection +- 📝 **Interactive Chat** - Interactive UI with completion, diffs, and quickfix integration - 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context -- ⚡ **Efficient** - Smart token usage with tiktoken counting and history management +- ⚡ **Token Efficient** - Resource replacement prevents duplicate context, history management via tiktoken counting +- 🔗 **Scriptable** - Comprehensive Lua API for automation and headless mode operation - 🔌 **Extensible** - [Custom functions](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions) and [providers](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/providers), plus integrations like [mcphub.nvim](https://github.com/ravitemer/mcphub.nvim) # Installation From 386b51b8e5d1d3a664da625ebb5be6ba8cc1a0e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Sep 2025 08:30:34 +0000 Subject: [PATCH 619/728] 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 5f96c503..b86efef6 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -41,11 +41,12 @@ CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. - 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your GitHub Copilot settings and the models provided by GitHub’s API. -- 🔧 **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval -- 🔒 **Explicit Control** - Only shares what you specifically request - no background data collection -- 📝 **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration +- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with your explicit approval +- 🔒 **Privacy First** - Only shares what you explicitly request - no background data collection +- 📝 **Interactive Chat** - Interactive UI with completion, diffs, and quickfix integration - 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context -- ⚡ **Efficient** - Smart token usage with tiktoken counting and history management +- ⚡ **Token Efficient** - Resource replacement prevents duplicate context, history management via tiktoken counting +- 🔗 **Scriptable** - Comprehensive Lua API for automation and headless mode operation - 🔌 **Extensible** - Custom functions and providers , plus integrations like mcphub.nvim From 74611b56e813f50e905122387b92fb832ac9616c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 17 Sep 2025 18:51:28 +0200 Subject: [PATCH 620/728] fix(chat): do not create multiple chat isntances (#1432) this breaks notification listeners Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 58 ++++++++++++++++++------------------- lua/CopilotChat/ui/chat.lua | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 98c1c1e6..268405bd 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1100,41 +1100,41 @@ function M.setup(config) if M.chat then M.chat:close(state.source.bufnr) M.chat:delete() - end - - M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) - for name, _ in pairs(M.config.mappings) do - map_key(name, bufnr) - end - - require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) + else + M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) + for name, _ in pairs(M.config.mappings) do + map_key(name, bufnr) + end - vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { - buffer = bufnr, - callback = function(ev) - if ev.event == 'BufEnter' then - update_source() - end + require('CopilotChat.completion').enable(bufnr, M.config.chat_autocomplete) - vim.schedule(function() - select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) - end) - end, - }) - - if M.config.insert_at_end then - vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { buffer = bufnr, - callback = function() - vim.cmd('normal! 0') - vim.cmd('normal! G$') - vim.v.char = 'x' + callback = function(ev) + if ev.event == 'BufEnter' then + update_source() + end + + vim.schedule(function() + select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) + end) end, }) - end - finish(true) - end) + if M.config.insert_at_end then + vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + buffer = bufnr, + callback = function() + vim.cmd('normal! 0') + vim.cmd('normal! G$') + vim.v.char = 'x' + end, + }) + end + + finish(true) + end) + end for name, prompt in pairs(list_prompts()) do if prompt.prompt then diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 267b8aed..8cd08c11 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -164,7 +164,7 @@ local Chat = class(function(self, config, on_buf_create) if not msg or msg == '' then self.chat_overlay:restore(self.winnr, self.bufnr) else - self:overlay({ text = msg }) + self.chat_overlay:show(msg, self.winnr) end end) end, Overlay) From 181a523f5bf6b8f277e6a994907c23f9e84d2445 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 20:14:43 +0200 Subject: [PATCH 621/728] chore(main): release 4.7.2 (#1433) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894a8113..a7d125ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [4.7.2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.1...v4.7.2) (2025-09-17) + + +### Bug Fixes + +* **chat:** do not create multiple chat isntances ([#1432](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1432)) ([74611b5](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/74611b56e813f50e905122387b92fb832ac9616c)) + ## [4.7.1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.0...v4.7.1) (2025-09-16) diff --git a/version.txt b/version.txt index 7c66fca5..af9764a5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.7.1 +4.7.2 From 16aa92419d48957319a3f6b06c9d74ebdcead80c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 21 Sep 2025 06:18:40 +0200 Subject: [PATCH 622/728] fix(mappings): make sure function resolution is not ran in fast context (#1436) functions call get_messages and those use vim.api functions. So move it at start of show_info block Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 4 ++-- lua/CopilotChat/init.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 74429e33..bcd9590e 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -337,10 +337,10 @@ return { local system_prompt = config.system_prompt async.run(function() - local infos = client:info() + local resolved_resources = copilot.resolve_functions(prompt, config) local selected_tools = copilot.resolve_tools(prompt, config) local selected_model = copilot.resolve_model(prompt, config) - local resolved_resources = copilot.resolve_functions(prompt, config) + local infos = client:info() selected_tools = vim.tbl_map(function(tool) return tool.name diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 268405bd..35eec19f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -515,7 +515,7 @@ end ---@param config CopilotChat.config.Shared? ---@return CopilotChat.config.prompts.Prompt, string function M.resolve_prompt(prompt, config) - if not prompt then + if prompt == nil then local message = M.chat:get_message(constants.ROLE.USER) if message then prompt = message.content From acd1e6d38d216289cd0748808da25dfc66015612 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Sep 2025 04:19:02 +0000 Subject: [PATCH 623/728] 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 b86efef6..7b80dd0c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 21 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From df8efe9d2368c876d607b513bb384eaa8daf1d12 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 28 Sep 2025 11:37:00 +0200 Subject: [PATCH 624/728] fix(os): use vim.uv.os_uname for OS detection (#1449) Replace usage of jit.os with vim.uv.os_uname().sysname to not rely on neovim builds with LuaJIT --- lua/CopilotChat/init.lua | 2 +- lua/CopilotChat/tiktoken.lua | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 35eec19f..34f564ae 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -586,7 +586,7 @@ function M.resolve_prompt(prompt, config) .. vim.trim(require('CopilotChat.instructions.edit_file_block')) end - config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) + config.system_prompt = config.system_prompt:gsub('{OS_NAME}', vim.uv.os_uname().sysname) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 2dd2914d..abe1ce1d 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -6,13 +6,14 @@ local class = require('CopilotChat.utils.class') --- Get the library extension based on the operating system --- @return string local function get_lib_extension() - if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then + local os_name = vim.uv.os_uname().sysname:lower() + if os_name:find('darwin') then return '.dylib' - end - if jit.os:lower() == 'windows' then + elseif os_name:find('windows') then return '.dll' + else + return '.so' end - return '.so' end --- Load tiktoken data from cache or download it From d574851bebfe94b46ee37814d0f12fcfb7109de2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 28 Sep 2025 09:37:16 +0000 Subject: [PATCH 625/728] 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 7b80dd0c..a93e7ad5 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 21 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 2f3e07398ba81ccc0cf3d1fca9805aaca126264e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 28 Sep 2025 11:51:21 +0200 Subject: [PATCH 626/728] chore(main): release 4.7.3 (#1437) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ version.txt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d125ec..6c5ede94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [4.7.3](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.2...v4.7.3) (2025-09-28) + + +### Bug Fixes + +* **mappings:** make sure function resolution is not ran in fast context ([#1436](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1436)) ([16aa924](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/16aa92419d48957319a3f6b06c9d74ebdcead80c)) +* **os:** use vim.uv.os_uname for OS detection ([#1449](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1449)) ([df8efe9](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/df8efe9d2368c876d607b513bb384eaa8daf1d12)) + ## [4.7.2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.1...v4.7.2) (2025-09-17) diff --git a/version.txt b/version.txt index af9764a5..87b18a56 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.7.2 +4.7.3 From 60ad5121bbfe3785ff11b6a137a17106a0ded564 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 22:04:30 +0200 Subject: [PATCH 627/728] [pre-commit.ci] pre-commit autoupdate (#1450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.2.0 → v2.3.0](https://github.com/JohnnyMorganz/StyLua/compare/v2.2.0...v2.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 242a4e01..7602f97a 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: v2.2.0 + rev: v2.3.0 hooks: - id: stylua-github From 481db772c752fa029329194a2bc2c2073356a05a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Sep 2025 20:04:54 +0000 Subject: [PATCH 628/728] 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 a93e7ad5..f7eabd6e 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 7a8e238e36ea9e1df9d6309434a37bcdc15a9fae Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 2 Oct 2025 00:05:06 +0200 Subject: [PATCH 629/728] fix(url): ensure main thread scheduling before fetching (#1453) Call utils.schedule_main() before fetching URL data to ensure that subsequent operations run on the main thread. This prevents potential issues with asynchronous execution and improves reliability when accessing resources. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 2f085705..ca0ba2f2 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -545,6 +545,7 @@ return { input.url = 'https://' .. input.url end + utils.schedule_main() local data, mimetype = resources.get_url(input.url) if not data then error('URL not found: ' .. input.url) From 77cfadb3884581d018d8299f83b925097ac35c3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Oct 2025 22:05:23 +0000 Subject: [PATCH 630/728] 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 f7eabd6e..70b1a375 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 September 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 1e06be97df058fa0bb4af54659c39918a4999c86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 00:07:28 +0200 Subject: [PATCH 631/728] chore(main): release 4.7.4 (#1454) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5ede94..529ec198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [4.7.4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.3...v4.7.4) (2025-10-01) + + +### Bug Fixes + +* **url:** ensure main thread scheduling before fetching ([#1453](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1453)) ([7a8e238](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7a8e238e36ea9e1df9d6309434a37bcdc15a9fae)) + ## [4.7.3](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v4.7.2...v4.7.3) (2025-09-28) diff --git a/version.txt b/version.txt index 87b18a56..b48b2de9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.7.3 +4.7.4 From cf4f7a58a0e65be6ccbdbc83142e189f96cd9fb5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 6 Oct 2025 03:52:11 +0200 Subject: [PATCH 632/728] refactor(functions)!: unify buffer/selection/diagnostics resources (#1456) * refactor(functions)!: unify buffer/selection/diagnostics resources - Replace separate `buffer`, `buffers`, `selection`, `diagnostics` and 'quickfix' resources with a unified `buffer` and `selection` resource that automatically includes diagnostics in the output. - Replace `register` resource with simplified `clipboard` resource - Add new `bash` and `edit` tools for LLM-only usage. - Update README to reflect new resource/tool types and usage. - Remove unused/duplicated mappings and code for old resources. - Improve enum handling and selection UI for resource schemas. BREAKING CHANGE: Removes `buffers`, `diagnostics`, `register`, and `quickfix` resources. Use `buffer`, `selection`, or `clipboard` instead. 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 | 36 +- lua/CopilotChat/config/functions.lua | 573 ++++++++++++--------------- lua/CopilotChat/config/mappings.lua | 29 +- lua/CopilotChat/functions.lua | 36 +- 4 files changed, 303 insertions(+), 371 deletions(-) diff --git a/README.md b/README.md index 8da6be23..bcc84655 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,11 @@ EOF # Sticky prompt that persists -> #buffer:current +> #buffer:active > You are a helpful coding assistant ``` -When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdiff` etc. You'll see the proposed function call and can approve/reject it before execution. +When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`, `glob`, `grep`, `gitdiff` etc. You'll see the proposed function call and can approve/reject it before execution. # Usage @@ -143,7 +143,6 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `` | `` | Reset and clear the chat window | | `` | `` | Submit the current prompt | | - | `grr` | Toggle sticky prompt for line under cursor | -| - | `grx` | Clear all sticky prompts in prompt | | `` | `` | Accept nearest diff | | - | `gj` | Jump to section of nearest diff | | - | `gqa` | Add all answers from chat to quickfix list | @@ -168,20 +167,23 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif All predefined functions belong to the `copilot` group. -| Function | Description | Example Usage | -| ------------- | ------------------------------------------------ | ---------------------- | -| `buffer` | Retrieves content from a specific buffer | `#buffer` | -| `buffers` | Fetches content from multiple buffers | `#buffers:visible` | -| `diagnostics` | Collects code diagnostics (errors, warnings) | `#diagnostics:current` | -| `file` | Reads content from a specified file path | `#file:path/to/file` | -| `gitdiff` | Retrieves git diff information | `#gitdiff:staged` | -| `gitstatus` | Retrieves git status information | `#gitstatus` | -| `glob` | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | -| `grep` | Searches for a pattern across files in workspace | `#grep:TODO` | -| `quickfix` | Includes content of files in quickfix list | `#quickfix` | -| `register` | Provides access to specified Vim register | `#register:+` | -| `selection` | Includes the current visual selection | `#selection` | -| `url` | Fetches content from a specified URL | `#url:https://...` | +| Function | Type | Description | Example Usage | +| ----------- | -------- | ------------------------------------------------------ | -------------------- | +| `bash` | Tool | Executes a bash command and returns output | `@copilot` only | +| `buffer` | Resource | Retrieves content from buffer(s) with diagnostics | `#buffer:active` | +| `clipboard` | Resource | Provides access to system clipboard content | `#clipboard` | +| `edit` | Tool | Applies a unified diff to a file | `@copilot` only | +| `file` | Resource | Reads content from a specified file path | `#file:path/to/file` | +| `gitdiff` | Resource | Retrieves git diff information | `#gitdiff:staged` | +| `glob` | Resource | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | +| `grep` | Resource | Searches for a pattern across files in workspace | `#grep:TODO` | +| `selection` | Resource | Includes the current visual selection with diagnostics | `#selection` | +| `url` | Resource | Fetches content from a specified URL | `#url:https://...` | + +**Type Legend:** + +- **Resource**: Can be used manually via `#function` syntax +- **Tool**: Can only be called by LLM via `@copilot` (for safety/complexity reasons) ## Predefined Prompts diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index ca0ba2f2..da97beb1 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -2,6 +2,44 @@ local resources = require('CopilotChat.resources') local utils = require('CopilotChat.utils') local files = require('CopilotChat.utils.files') +--- Get diagnostics for a buffer and format them as text +---@param bufnr number +---@param start_line number? +---@param end_line number? +---@return string +local function get_diagnostics_text(bufnr, start_line, end_line) + local diagnostics = vim.diagnostic.get(bufnr, { + severity = { min = vim.diagnostic.severity.HINT }, + }) + + if #diagnostics == 0 then + return '' + end + + local diag_lines = { '\n--- Diagnostics ---' } + for _, diag in ipairs(diagnostics) do + local diag_lnum = diag.lnum + 1 + -- If range is specified, filter diagnostics within range + if not start_line or (diag_lnum >= start_line and diag_lnum <= end_line) then + local severity = vim.diagnostic.severity[diag.severity] or 'UNKNOWN' + local line_text = vim.api.nvim_buf_get_lines(bufnr, diag.lnum, diag.lnum + 1, false)[1] or '' + table.insert( + diag_lines, + string.format( + '%s line=%d-%d: %s\n > %s', + severity, + diag.lnum + 1, + diag.end_lnum and (diag.end_lnum + 1) or (diag.lnum + 1), + diag.message, + line_text + ) + ) + end + end + + return #diag_lines > 1 and table.concat(diag_lines, '\n') or '' +end + ---@class CopilotChat.config.functions.Function ---@field description string? ---@field schema table? @@ -50,64 +88,38 @@ return { end, }, - glob = { + url = { group = 'copilot', - uri = 'files://glob/{pattern}', - description = 'Lists filenames matching a pattern in your workspace. Useful for discovering relevant files or understanding the project structure.', + uri = 'https://{url}', + description = 'Fetches content from a specified URL. Useful for referencing documentation, examples, or other online resources.', schema = { type = 'object', - required = { 'pattern' }, + required = { 'url' }, properties = { - pattern = { + url = { type = 'string', - description = 'Glob pattern to match files.', - default = '**/*', + description = 'URL to include in chat context.', }, }, }, - resolve = function(input, source) - local out = files.glob(source.cwd(), { - pattern = input.pattern, - }) - - return { - { - uri = 'files://glob/' .. input.pattern, - mimetype = 'text/plain', - data = table.concat(out, '\n'), - }, - } - end, - }, - - grep = { - group = 'copilot', - uri = 'files://grep/{pattern}', - description = 'Searches for a pattern across files in your workspace. Helpful for finding specific code elements or patterns.', - - schema = { - type = 'object', - required = { 'pattern' }, - properties = { - pattern = { - type = 'string', - description = 'Pattern to search for.', - }, - }, - }, + resolve = function(input) + if not input.url:match('^https?://') then + input.url = 'https://' .. input.url + end - resolve = function(input, source) - local out = files.grep(source.cwd(), { - pattern = input.pattern, - }) + utils.schedule_main() + local data, mimetype = resources.get_url(input.url) + if not data then + error('URL not found: ' .. input.url) + end return { { - uri = 'files://grep/' .. input.pattern, - mimetype = 'text/plain', - data = table.concat(out, '\n'), + uri = input.url, + mimetype = mimetype, + data = data, }, } end, @@ -115,124 +127,128 @@ return { buffer = { group = 'copilot', - uri = 'buffer://{name}', - description = 'Retrieves content from a specific buffer. Useful for discussing or analyzing code from a particular file that is currently loaded.', + uri = 'neovim://buffer/{scope}', + description = 'Retrieves content from buffer(s) with diagnostics. Scope can be a buffer number, filename, or one of: active, visible, listed, quickfix.', schema = { type = 'object', - required = { 'name' }, + required = { 'scope' }, properties = { - name = { + scope = { type = 'string', - description = 'Buffer filename to include in chat context.', + description = 'Buffer scope: active (current), visible (shown in windows), listed (all listed buffers), quickfix (buffers in quickfix list), or a specific buffer number/filename.', enum = function() - return vim - .iter(vim.api.nvim_list_bufs()) - :filter(function(buf) - return buf and utils.buf_valid(buf) and vim.fn.buflisted(buf) == 1 - end) - :map(function(buf) - return vim.api.nvim_buf_get_name(buf) - end) - :totable() + local opts = { + { display = 'active (current buffer)', value = 'active' }, + { display = 'visible (all visible buffers)', value = 'visible' }, + { display = 'listed (all listed buffers)', value = 'listed' }, + { display = 'quickfix (buffers in quickfix)', value = 'quickfix' }, + } + + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if utils.buf_valid(buf) and vim.fn.buflisted(buf) == 1 then + local name = vim.api.nvim_buf_get_name(buf) + if name and name ~= '' then + local display_name = vim.fn.fnamemodify(name, ':~:.') + table.insert(opts, { display = display_name, value = name }) + end + end + end + return opts end, + default = 'active', }, }, }, resolve = function(input, source) utils.schedule_main() - local name = input.name or vim.api.nvim_buf_get_name(source.bufnr) - local found_buf = nil - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if vim.api.nvim_buf_get_name(buf) == name then - found_buf = buf - break + local scope = input.scope or 'active' + local buffers = {} + + -- Determine which buffers to include based on scope + if scope == 'active' then + if source and source.bufnr and utils.buf_valid(source.bufnr) then + buffers = { source.bufnr } + end + elseif scope == 'visible' then + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 and #vim.fn.win_findbuf(b) > 0 + end, vim.api.nvim_list_bufs()) + elseif scope == 'listed' then + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 + end, vim.api.nvim_list_bufs()) + elseif scope == 'quickfix' then + local items = vim.fn.getqflist() + local file_to_bufnr = {} + for _, item in ipairs(items) do + local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) + if filename and item.bufnr and utils.buf_valid(item.bufnr) then + file_to_bufnr[filename] = item.bufnr + end + end + buffers = vim.tbl_values(file_to_bufnr) + elseif tonumber(scope) then + local bufnr = tonumber(scope) + if utils.buf_valid(bufnr) then + buffers = { bufnr } end end - if not found_buf then - error('Buffer not found: ' .. name) - end - local data, mimetype = resources.get_buffer(found_buf) - if not data then - error('Buffer not found: ' .. name) - end - return { - { - uri = 'buffer://' .. name, - name = name, - mimetype = mimetype, - data = data, - }, - } - end, - }, - buffers = { - group = 'copilot', - uri = 'buffers://{scope}', - description = 'Fetches content from multiple buffers. Helps with discussing or analyzing code across multiple files simultaneously.', - - schema = { - type = 'object', - required = { 'scope' }, - properties = { - scope = { - type = 'string', - description = 'Scope of buffers to include in chat context.', - enum = { 'listed', 'visible' }, - default = 'listed', - }, - }, - }, + if #buffers == 0 then + error('No buffers found for input: ' .. scope) + end - resolve = function(input) - utils.schedule_main() - return vim - .iter(vim.api.nvim_list_bufs()) - :filter(function(bufnr) - return utils.buf_valid(bufnr) - and vim.fn.buflisted(bufnr) == 1 - and (input.scope == 'listed' or #vim.fn.win_findbuf(bufnr) > 0) - end) - :map(function(bufnr) - local name = vim.api.nvim_buf_get_name(bufnr) - local data, mimetype = resources.get_buffer(bufnr) - if not data then - return nil + local results = {} + for _, bufnr in ipairs(buffers) do + local name = vim.api.nvim_buf_get_name(bufnr) + local data, mimetype = resources.get_buffer(bufnr) + if data then + local diag_text = get_diagnostics_text(bufnr) + if diag_text ~= '' then + data = data .. diag_text end - return { - uri = 'buffer://' .. name, + + table.insert(results, { + uri = 'buffer://' .. bufnr, name = name, mimetype = mimetype, data = data, - } - end) - :filter(function(file_data) - return file_data ~= nil - end) - :totable() + }) + end + end + + return results end, }, selection = { group = 'copilot', uri = 'neovim://selection', - description = 'Includes the content of the current visual selection. Useful for discussing specific code snippets or text blocks.', + description = 'Includes the content of the current visual selection with diagnostics. Useful for discussing specific code snippets or text blocks.', resolve = function(_, source) utils.schedule_main() - local selection = require('CopilotChat.select').get(source.bufnr) + + local select = require('CopilotChat.select') + local selection = select.get(source.bufnr) if not selection then return {} end + local data = selection.content + local diag_text = get_diagnostics_text(source.bufnr, selection.start_line, selection.end_line) + if diag_text ~= '' then + data = data .. diag_text + end + return { { uri = 'neovim://selection', name = selection.filename, mimetype = files.mimetype_to_filetype(selection.filetype), - data = selection.content, + data = data, annotations = { start_line = selection.start_line, end_line = selection.end_line, @@ -242,210 +258,86 @@ return { end, }, - quickfix = { + clipboard = { group = 'copilot', - uri = 'neovim://quickfix', - description = 'Includes the content of all files referenced in the current quickfix list. Useful for discussing compilation errors, search results, or other collected locations.', + uri = 'neovim://clipboard', + description = 'Provides access to the system clipboard content. Useful for discussing copied text or code snippets.', resolve = function() utils.schedule_main() - - local items = vim.fn.getqflist() - if not items or #items == 0 then + local lines = vim.fn.getreg('+') + if not lines or lines == '' then return {} end - local file_to_bufnr = {} - for _, item in ipairs(items) do - local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) - if filename then - if item.bufnr and utils.buf_valid(item.bufnr) then - file_to_bufnr[filename] = item.bufnr - else - file_to_bufnr[filename] = false - end - end - end - - return vim - .iter(vim.tbl_keys(file_to_bufnr)) - :map(function(file) - local bufnr = file_to_bufnr[file] - local data, mimetype, uri - if bufnr and bufnr ~= false then - data, mimetype = resources.get_buffer(bufnr) - uri = 'buffer://' .. file - else - data, mimetype = resources.get_file(file) - uri = 'file://' .. file - end - if not data then - return nil - end - return { - uri = uri, - name = file, - mimetype = mimetype, - data = data, - } - end) - :filter(function(file_data) - return file_data ~= nil - end) - :totable() + return { + { + uri = 'neovim://clipboard', + mimetype = 'text/plain', + data = lines, + }, + } end, }, - diagnostics = { + glob = { group = 'copilot', - uri = 'neovim://diagnostics/{scope}/{severity}', - description = 'Collects code diagnostics (errors, warnings, etc.) from specified buffers. Helpful for troubleshooting and fixing code issues.', + uri = 'files://glob/{pattern}', + description = 'Lists filenames matching a pattern in your workspace. Useful for discovering relevant files or understanding the project structure.', schema = { type = 'object', - required = { 'scope', 'severity' }, + required = { 'pattern' }, properties = { - scope = { - type = 'string', - description = 'Scope of buffers to use for retrieving diagnostics.', - enum = { 'current', 'listed', 'visible', 'selection' }, - default = 'current', - }, - severity = { + pattern = { type = 'string', - description = 'Minimum severity level of diagnostics to include.', - enum = { 'error', 'warn', 'info', 'hint' }, - default = 'warn', + description = 'Glob pattern to match files.', + default = '**/*', }, }, }, resolve = function(input, source) - utils.schedule_main() - local out = {} - local scope = input.scope or 'current' - local buffers = {} - - -- Get buffers based on scope - if scope == 'current' or scope == 'selection' then - if source and source.bufnr and utils.buf_valid(source.bufnr) then - buffers = { source.bufnr } - end - elseif scope == 'listed' then - buffers = vim.tbl_filter(function(b) - return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 - end, vim.api.nvim_list_bufs()) - elseif scope == 'visible' then - buffers = vim.tbl_filter(function(b) - return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 and #vim.fn.win_findbuf(b) > 0 - end, vim.api.nvim_list_bufs()) - else - buffers = vim.tbl_filter(function(b) - return utils.buf_valid(b) and vim.api.nvim_buf_get_name(b) == input.scope - end, vim.api.nvim_list_bufs()) - end - - -- By default, collect from the whole buffer - local selection_start_line = 1 - local selection_end_line = vim.api.nvim_buf_line_count(source.bufnr) - -- Determine selection range if scope is 'selection' - if scope == 'selection' then - local select = require('CopilotChat.select') - local selection = select.get(source.bufnr) - if selection then - selection_start_line = selection.start_line - selection_end_line = selection.end_line - else - return out - end - end - - -- Collect diagnostics for each buffer - for _, bufnr in ipairs(buffers) do - local name = vim.api.nvim_buf_get_name(bufnr) - local diagnostics = vim.diagnostic.get(bufnr, { - severity = { - min = vim.diagnostic.severity[input.severity:upper()], - }, - }) - - if #diagnostics > 0 then - local diag_lines = {} - for _, diag in ipairs(diagnostics) do - -- Diagnostics.lnum are 0-indexed, so add 1 for comparison - local diag_lnum = diag.lnum + 1 - if diag_lnum >= selection_start_line and diag_lnum <= selection_end_line then - local severity = vim.diagnostic.severity[diag.severity] or 'UNKNOWN' - local line_text = vim.api.nvim_buf_get_lines(bufnr, diag.lnum, diag.lnum + 1, false)[1] or '' - - table.insert( - diag_lines, - string.format( - '%s line=%d-%d: %s\n > %s', - severity, - diag.lnum + 1, - diag.end_lnum and (diag.end_lnum + 1) or (diag.lnum + 1), - diag.message, - line_text - ) - ) - end - end - - table.insert(out, { - uri = 'neovim://diagnostics/' .. name, - mimetype = 'text/plain', - data = table.concat(diag_lines, '\n'), - }) - end - end + local out = files.glob(source.cwd(), { + pattern = input.pattern, + }) - return out + return { + { + uri = 'files://glob/' .. input.pattern, + mimetype = 'text/plain', + data = table.concat(out, '\n'), + }, + } end, }, - register = { + grep = { group = 'copilot', - uri = 'neovim://register/{register}', - description = 'Provides access to the content of a specified Vim register. Useful for discussing yanked text, clipboard content, or previously executed commands.', + uri = 'files://grep/{pattern}', + description = 'Searches for a pattern across files in your workspace. Helpful for finding specific code elements or patterns.', schema = { type = 'object', - required = { 'register' }, + required = { 'pattern' }, properties = { - register = { + pattern = { type = 'string', - description = 'Register to include in chat context.', - enum = { - '+', - '*', - '"', - '0', - '-', - '.', - '%', - ':', - '#', - '=', - '/', - }, - default = '+', + description = 'Pattern to search for.', }, }, }, - resolve = function(input) - utils.schedule_main() - local lines = vim.fn.getreg(input.register) - if not lines or lines == '' then - return {} - end + resolve = function(input, source) + local out = files.grep(source.cwd(), { + pattern = input.pattern, + }) return { { - uri = 'neovim://register/' .. input.register, + uri = 'files://grep/' .. input.pattern, mimetype = 'text/plain', - data = lines, + data = table.concat(out, '\n'), }, } end, @@ -499,65 +391,104 @@ return { end, }, - gitstatus = { + bash = { group = 'copilot', - uri = 'git://status', - description = 'Retrieves the status of the current git repository. Useful for discussing changes, commits, and other git-related tasks.', + description = 'Executes a bash command and returns its output. Useful for running shell commands, checking file contents, or gathering system information.', - resolve = function(_, source) - local cmd = { - 'git', - '-C', - source.cwd(), - 'status', - } + schema = { + type = 'object', + required = { 'command' }, + properties = { + command = { + type = 'string', + description = 'Bash command to execute.', + }, + }, + }, - local out = utils.system(cmd) + resolve = function(input, source) + local cmd = { 'bash', '-c', input.command } + local out = utils.system(cmd, { cwd = source.cwd() }) return { { - uri = 'git://status', - mimetype = 'text/plain', data = out.stdout, }, } end, }, - url = { + edit = { group = 'copilot', - uri = 'https://{url}', - description = 'Fetches content from a specified URL. Useful for referencing documentation, examples, or other online resources.', + description = 'Applies a unified diff to a file. The diff should be in unified diff format (similar to diff -U0 output).', schema = { type = 'object', - required = { 'url' }, + required = { 'filename', 'diff' }, properties = { - url = { + filename = { type = 'string', - description = 'URL to include in chat context.', + description = 'Path to file to edit.', + }, + diff = { + type = 'string', + description = 'Unified diff content to apply to the file.', }, }, }, - resolve = function(input) - if not input.url:match('^https?://') then - input.url = 'https://' .. input.url + resolve = function(input, source) + utils.schedule_main() + + local select = require('CopilotChat.select') + local diff = require('CopilotChat.utils.diff') + + -- Find or create the buffer for the file + local filename = input.filename + local diff_bufnr = nil + + -- Try to find matching buffer first + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if files.filename_same(vim.api.nvim_buf_get_name(buf), filename) then + diff_bufnr = buf + break + end end - utils.schedule_main() - local data, mimetype = resources.get_url(input.url) - if not data then - error('URL not found: ' .. input.url) + -- If still not found, try to load or create buffer + if not diff_bufnr then + diff_bufnr = vim.fn.bufadd(filename) + vim.fn.bufload(diff_bufnr) end - return { - { - uri = input.url, - mimetype = mimetype, - data = data, - }, - } + -- Get current buffer content + local lines = vim.api.nvim_buf_get_lines(diff_bufnr, 0, -1, false) + local content = table.concat(lines, '\n') + + -- Apply the unified diff + local new_lines, applied, first, last = diff.apply_unified_diff(input.diff, content) + + if applied then + -- Apply changes to buffer + vim.api.nvim_buf_set_lines(diff_bufnr, 0, -1, false, new_lines) + + -- If source window is valid, switch to the edited buffer and highlight changes + if source and source.winnr and vim.api.nvim_win_is_valid(source.winnr) then + vim.api.nvim_win_set_buf(source.winnr, diff_bufnr) + if first and last then + select.set(diff_bufnr, source.winnr, first, last) + select.highlight(diff_bufnr) + end + end + + return { + { + data = string.format('Successfully applied diff to %s (lines %d-%d)', filename, first or 0, last or 0), + }, + } + else + error('Failed to apply diff to ' .. filename) + end end, }, } diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index bcd9590e..448e9516 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -61,6 +61,7 @@ end ---@field accept_diff CopilotChat.config.mapping|false|nil ---@field jump_to_diff CopilotChat.config.mapping|false|nil ---@field quickfix_diffs CopilotChat.config.mapping|false|nil +---@field quickfix_answers CopilotChat.config.mapping|false|nil ---@field yank_diff CopilotChat.config.mapping.yank_diff|false|nil ---@field show_diff CopilotChat.config.mapping|false|nil ---@field show_info CopilotChat.config.mapping|false|nil @@ -133,34 +134,6 @@ return { end, }, - clear_stickies = { - normal = 'grx', - callback = function() - local message = copilot.chat:get_message(constants.ROLE.USER) - local section = message and message.section - if not section then - return - end - - local lines = utils.split_lines(message.content) - local new_lines = {} - local changed = false - - for _, line in ipairs(lines) do - if not vim.startswith(vim.trim(line), '> ') then - table.insert(new_lines, line) - else - changed = true - end - end - - if changed then - message.content = table.concat(new_lines, '\n') - copilot.chat:add_message(message, true) - end - end, - }, - accept_diff = { normal = '', insert = '', diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index e63b63a3..9d0befdf 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -57,7 +57,14 @@ local function filter_schema(tbl, root) for k, v in pairs(tbl) do if not utils.empty(v) then if type(v) ~= 'function' and k ~= 'examples' then - result[k] = type(v) == 'table' and filter_schema(v) or v + if k == 'enum' and type(v) == 'table' and type(v[1]) == 'table' and v[1].value then + -- If enum contains objects with value/display, extract just the values + result[k] = vim.tbl_map(function(item) + return item.value + end, v) + else + result[k] = type(v) == 'table' and filter_schema(v) or v + end end end end @@ -211,11 +218,30 @@ function M.enter_input(schema, source) if #choices == 0 then choice = nil elseif #choices == 1 then - choice = choices[1] + -- Handle both string and table choices + choice = type(choices[1]) == 'table' and choices[1].value or choices[1] else - choice = utils.select(choices, { - prompt = string.format('Select %s> ', prop_name), - }) + -- Check if choices are objects with display/value + local has_display = type(choices[1]) == 'table' and choices[1].display ~= nil + local selected + + if has_display then + -- Use format_item to display the display field + selected = utils.select(choices, { + prompt = string.format('Select %s> ', prop_name), + format_item = function(item) + return item.display + end, + }) + -- Extract the value from the selected item + choice = selected and selected.value or nil + else + -- Regular string choices + selected = utils.select(choices, { + prompt = string.format('Select %s> ', prop_name), + }) + choice = selected + end end table.insert(out, choice or '') From 3ee1a96f567e3c0f23c6180a9059a2bc076b7528 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Oct 2025 01:52:35 +0000 Subject: [PATCH 633/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 54 +++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 70b1a375..27042059 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 01 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 06 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -145,13 +145,13 @@ EXAMPLES *CopilotChat-examples* # Sticky prompt that persists - > #buffer:current + > #buffer:active > You are a helpful coding assistant < -When you use `@copilot`, the LLM can call functions like `glob`, `file`, -`gitdiff` etc. You’ll see the proposed function call and can approve/reject -it before execution. +When you use `@copilot`, the LLM can call functions like `bash`, `edit`, +`file`, `glob`, `grep`, `gitdiff` etc. You’ll see the proposed function call +and can approve/reject it before execution. ============================================================================== @@ -183,7 +183,6 @@ CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* Reset and clear the chat window Submit the current prompt - grr Toggle sticky prompt for line under cursor - - grx Clear all sticky prompts in prompt Accept nearest diff - gj Jump to section of nearest diff - gqa Add all answers from chat to quickfix list @@ -206,37 +205,40 @@ PREDEFINED FUNCTIONS *CopilotChat-predefined-functions* All predefined functions belong to the `copilot` group. - ------------------------------------------------------------------------------ - Function Description Example Usage - ------------- ----------------------------------------- ---------------------- - buffer Retrieves content from a specific buffer #buffer + ------------------------------------------------------------------------------------- + Function Type Description Example Usage + ----------- ---------- ----------------------------------------- -------------------- + bash Tool Executes a bash command and returns @copilot only + output - buffers Fetches content from multiple buffers #buffers:visible + buffer Resource Retrieves content from buffer(s) with #buffer:active + diagnostics - diagnostics Collects code diagnostics (errors, #diagnostics:current - warnings) + clipboard Resource Provides access to system clipboard #clipboard + content - file Reads content from a specified file path #file:path/to/file + edit Tool Applies a unified diff to a file @copilot only - gitdiff Retrieves git diff information #gitdiff:staged + file Resource Reads content from a specified file path #file:path/to/file - gitstatus Retrieves git status information #gitstatus + gitdiff Resource Retrieves git diff information #gitdiff:staged - glob Lists filenames matching a pattern in #glob:**/*.lua - workspace + glob Resource Lists filenames matching a pattern in #glob:**/*.lua + workspace - grep Searches for a pattern across files in #grep:TODO - workspace + grep Resource Searches for a pattern across files in #grep:TODO + workspace - quickfix Includes content of files in quickfix #quickfix - list + selection Resource Includes the current visual selection #selection + with diagnostics - register Provides access to specified Vim register #register:+ + url Resource Fetches content from a specified URL #url:https://... + ------------------------------------------------------------------------------------- +**Type Legend:** - selection Includes the current visual selection #selection +- **Resource**: Can be used manually via `#function` syntax +- **Tool**: Can only be called by LLM via `@copilot` (for safety/complexity reasons) - url Fetches content from a specified URL #url:https://... - ------------------------------------------------------------------------------ PREDEFINED PROMPTS *CopilotChat-predefined-prompts* From 92f269971c33a6e2f405da8b14f01cd109b9a3a3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 6 Oct 2025 03:58:07 +0200 Subject: [PATCH 634/728] fix(utils): properly pass cwd as argument to system wrapper (#1458) --- lua/CopilotChat/config/functions.lua | 2 +- lua/CopilotChat/utils.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index da97beb1..8d6d72ec 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -408,7 +408,7 @@ return { resolve = function(input, source) local cmd = { 'bash', '-c', input.command } - local out = utils.system(cmd, { cwd = source.cwd() }) + local out = utils.system(cmd, source.cwd()) return { { diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 8b32a85b..cbdced39 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -125,9 +125,9 @@ end --- Call a system command ---@param cmd table The command ---@async -M.system = async.wrap(function(cmd, callback) - vim.system(cmd, { text = true }, callback) -end, 2) +M.system = async.wrap(function(cmd, cwd, callback) + vim.system(cmd, { cwd = cwd, text = true }, callback) +end, 3) --- Schedule a function only when needed (not on main thread) ---@param callback function The callback From 5801bfeaae4146f3127cb6c0bcbac721a172b85d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 8 Oct 2025 04:08:52 +0200 Subject: [PATCH 635/728] fix(mappings): use resource name if available in preview header (#1459) When displaying resource previews, prefer showing the resource name instead of the URI if the name is available. This improves readability in the preview header, especially for resources with user-friendly names. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 448e9516..635886c2 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -385,7 +385,7 @@ return { for _, resource in ipairs(resolved_resources) do local resource_lines = vim.split(resource.data, '\n') local preview = vim.list_slice(resource_lines, 1, math.min(10, #resource_lines)) - local header = string.format('**%s** (%s lines)', resource.uri, #resource_lines) + local header = string.format('**%s** (%s lines)', resource.name or resource.uri, #resource_lines) if #resource_lines > 10 then header = header .. ' (truncated)' end From f68deee85b8d734db1a9fbf63ce17a8164921267 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 02:09:11 +0000 Subject: [PATCH 636/728] 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 27042059..ea562be2 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 06 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 08 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From db4b51e1bb6a96b94496a6050f300f67258be872 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 18 Oct 2025 03:18:20 +0200 Subject: [PATCH 637/728] fix(mappings): use get_messages for quickfix answers (#1466) Closes #1465 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 635886c2..f484df24 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -275,9 +275,10 @@ return { normal = 'gqa', callback = function() local items = {} - for i, message in ipairs(copilot.chat.messages) do + local messages = copilot.chat:get_messages() + for i, message in ipairs(messages) do if message.section and message.role == constants.ROLE.ASSISTANT then - local prev_message = copilot.chat.messages[i - 1] + local prev_message = messages[i - 1] local text = '' if prev_message then text = prev_message.content From 98435447243eb80a7c228b1447f14f3294d39739 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 18 Oct 2025 01:18:43 +0000 Subject: [PATCH 638/728] 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 ea562be2..2c939e8d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 08 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 18 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From b06fd119d66559a3ff29bae2fabb70dea01fafc8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 21 Oct 2025 22:20:48 +0200 Subject: [PATCH 639/728] feat(prompt): support custom Copilot instructions file (#1467) Add support for including custom Copilot instructions from .github/copilot-instructions.md in the system prompt. Refactor prompt resolution logic into a new prompt.lua module, and update function and tool resolution to use this module. Also simplify diff hunk application fallback logic. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/config/mappings.lua | 4 +- lua/CopilotChat/init.lua | 347 ++-------------- .../instructions/custom_instructions.lua | 6 + lua/CopilotChat/prompt.lua | 390 ++++++++++++++++++ lua/CopilotChat/utils/diff.lua | 18 +- 6 files changed, 435 insertions(+), 332 deletions(-) create mode 100644 lua/CopilotChat/instructions/custom_instructions.lua create mode 100644 lua/CopilotChat/prompt.lua diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index e43b1838..682923b3 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -14,7 +14,7 @@ ---@field blend number? ---@class CopilotChat.config.Shared ----@field system_prompt nil|string|fun(source: CopilotChat.source):string +---@field system_prompt nil|string ---@field model string? ---@field tools string|table|nil ---@field resources string|table|nil diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index f484df24..a7cd5158 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -307,10 +307,10 @@ return { end local lines = {} - local config, prompt = copilot.resolve_prompt(message.content) - local system_prompt = config.system_prompt async.run(function() + local config, prompt = copilot.resolve_prompt(message.content) + local system_prompt = config.system_prompt local resolved_resources = copilot.resolve_functions(prompt, config) local selected_tools = copilot.resolve_tools(prompt, config) local selected_model = copilot.resolve_model(prompt, config) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 34f564ae..c738d4a8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,18 +1,13 @@ local async = require('plenary.async') local log = require('plenary.log') -local functions = require('CopilotChat.functions') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') -local notify = require('CopilotChat.notify') +local prompts = require('CopilotChat.prompt') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') local orderedmap = require('CopilotChat.utils.orderedmap') -local WORD = '([^%s:]+)' -local WORD_NO_INPUT = '([^%s]+)' -local WORD_WITH_INPUT_QUOTED = WORD .. ':`([^`]+)`' -local WORD_WITH_INPUT_UNQUOTED = WORD .. ':?([^%s`]*)' local BLOCK_OUTPUT_FORMAT = '```%s\n%s\n```' ---@class CopilotChat @@ -234,19 +229,25 @@ end ---@return any local function handle_error(config, cb) return function() - local ok, out = pcall(cb) + local function error_handler(err) + return { + err = utils.make_string(err), + traceback = debug.traceback(), + } + end + + local ok, out = xpcall(cb, error_handler) if ok then return out end + log.error(out.err .. '\n' .. out.traceback) - log.error(out) if config.headless then return end utils.schedule_main() - out = out or 'Unknown error' - out = utils.make_string(out) + out = out.err M.chat:add_message({ role = constants.ROLE.ASSISTANT, @@ -307,291 +308,25 @@ end ---@param config CopilotChat.config.Shared? ---@return table, string function M.resolve_tools(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - local tools = {} - for _, tool in ipairs(functions.parse_tools(M.config.functions)) do - tools[tool.name] = tool - end - - local enabled_tools = {} - local tool_matches = utils.to_table(config.tools) - - -- Check for @tool pattern to find enabled tools - prompt = prompt:gsub('@' .. WORD, function(match) - for name, tool in pairs(M.config.functions) do - if name == match or tool.group == match then - table.insert(tool_matches, match) - return '' - end - end - return '@' .. match - end) - for _, match in ipairs(tool_matches) do - for name, tool in pairs(M.config.functions) do - if name == match or tool.group == match then - table.insert(enabled_tools, tools[name]) - end - end - end - - return enabled_tools, prompt + return prompts.resolve_tools(prompt, config) end --- Call and resolve function calls from the prompt. ---@param prompt string? ---@param config CopilotChat.config.Shared? ----@return table, table, string +---@return table, table, table, string ---@async function M.resolve_functions(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - local tools = {} - for _, tool in ipairs(functions.parse_tools(M.config.functions)) do - tools[tool.name] = tool - end - - if config.resources then - local resources = utils.to_table(config.resources) - local lines = utils.split_lines(prompt) - for i = #resources, 1, -1 do - local resource = resources[i] - table.insert(lines, 1, '#' .. resource) - end - prompt = table.concat(lines, '\n') - end - - local resolved_resources = {} - local resolved_tools = {} - local tool_calls = {} - for _, message in ipairs(M.chat:get_messages()) do - if message.tool_calls then - for _, tool_call in ipairs(message.tool_calls) do - table.insert(tool_calls, tool_call) - end - end - end - - local resource_matches = {} - - -- Check for #word:`input` pattern - for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_QUOTED) do - local pattern = string.format('#%s:`%s`', word, input) - table.insert(resource_matches, { - pattern = pattern, - word = word, - input = input, - }) - end - - -- Check for #word:input pattern - for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_UNQUOTED) do - local pattern = utils.empty(input) and string.format('#%s', word) or string.format('#%s:%s', word, input) - table.insert(resource_matches, { - pattern = pattern, - word = word, - input = input, - }) - end - - -- Check for ##word:input pattern - for word in prompt:gmatch('##' .. WORD_NO_INPUT) do - local pattern = string.format('##%s', word) - table.insert(resource_matches, { - pattern = pattern, - word = word, - }) - end - - -- Resolve each function reference - local function expand_function(name, input) - notify.publish(notify.STATUS, 'Running function: ' .. name) - - local tool_id = nil - if not utils.empty(tool_calls) then - for _, tool_call in ipairs(tool_calls) do - if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) then - input = utils.empty(tool_call.arguments) and {} or utils.json_decode(tool_call.arguments) - tool_id = tool_call.id - break - end - end - end - - local tool = M.config.functions[name] - if not tool then - -- Check if input matches uri - for tool_name, tool_spec in pairs(M.config.functions) do - if tool_spec.uri then - local match = functions.match_uri(name, tool_spec.uri) - if match then - name = tool_name - tool = tool_spec - input = match - break - end - end - end - end - if not tool then - return nil - end - if not tool_id and not tool.uri then - return nil - end - - local schema = tools[name] and tools[name].schema or nil - local ok, output - if config.stop_on_function_failure then - output = tool.resolve(functions.parse_input(input, schema), state.source) - ok = true - else - ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source) - end - - local result = '' - if not ok then - result = utils.make_string(output) - else - for _, content in ipairs(output) do - if content then - local content_out = nil - if content.uri then - if - not vim.tbl_contains(resolved_resources, function(resource) - return resource.uri == content.uri - end, { predicate = true }) - then - content_out = '##' .. content.uri - table.insert(resolved_resources, content) - end - - if tool_id then - table.insert(state.sticky, '##' .. content.uri) - end - else - content_out = content.data - end - - if content_out then - if not utils.empty(result) then - result = result .. '\n' - end - result = result .. content_out - end - end - end - end - - if tool_id then - table.insert(resolved_tools, { - id = tool_id, - result = result, - }) - - return '' - end - - return result - end - - -- Resolve and process all tools - for _, match in ipairs(resource_matches) do - if not utils.empty(match.pattern) then - local out = expand_function(match.word, match.input) - if out == nil then - out = match.pattern - end - out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub - prompt = prompt:gsub(vim.pesc(match.pattern), out, 1) - end - end - - return resolved_resources, resolved_tools, prompt + return prompts.resolve_functions(prompt, config) end --- Resolve the final prompt and config from prompt template. ---@param prompt string? ---@param config CopilotChat.config.Shared? ---@return CopilotChat.config.prompts.Prompt, string +---@async function M.resolve_prompt(prompt, config) - if prompt == nil then - local message = M.chat:get_message(constants.ROLE.USER) - if message then - prompt = message.content - end - end - - local prompts_to_use = list_prompts() - local depth = 0 - local MAX_DEPTH = 10 - - local function resolve(inner_config, inner_prompt) - if depth >= MAX_DEPTH then - return inner_config, inner_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_config, resolved_prompt = resolve(p, p.prompt or '') - inner_config = vim.tbl_deep_extend('force', inner_config, resolved_config) - return resolved_prompt - end - - return '/' .. match - end) - - depth = depth - 1 - return inner_config, inner_prompt - end - - local function resolve_system_prompt(system_prompt) - if type(system_prompt) == 'function' then - local ok, result = pcall(system_prompt) - if not ok then - log.warn('Failed to resolve system prompt function: ' .. result) - return nil - end - return result - end - - return system_prompt - end - - config = vim.tbl_deep_extend('force', M.config, config or {}) - config, prompt = resolve(config, prompt or '') - - if config.system_prompt then - config.system_prompt = resolve_system_prompt(config.system_prompt) - - if M.config.prompts[config.system_prompt] then - -- Name references are good for making system prompt auto sticky - config.system_prompt = M.config.prompts[config.system_prompt].system_prompt - end - - config.system_prompt = vim.trim(config.system_prompt) .. '\n' .. M.config.prompts.COPILOT_BASE.system_prompt - config.system_prompt = vim.trim(config.system_prompt) - .. '\n' - .. vim.trim(require('CopilotChat.instructions.tool_use')) - - if config.diff == 'unified' then - config.system_prompt = vim.trim(config.system_prompt) - .. '\n' - .. vim.trim(require('CopilotChat.instructions.edit_file_unified')) - else - config.system_prompt = vim.trim(config.system_prompt) - .. '\n' - .. vim.trim(require('CopilotChat.instructions.edit_file_block')) - end - - config.system_prompt = config.system_prompt:gsub('{OS_NAME}', vim.uv.os_uname().sysname) - config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) - config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) - end - - return config, prompt + return prompts.resolve_prompt(prompt, config) end --- Resolve the model from the prompt. @@ -600,22 +335,7 @@ end ---@return string, string ---@async function M.resolve_model(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - local models = vim.tbl_map(function(model) - return model.id - end, list_models()) - - local selected_model = config.model or '' - 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 + return prompts.resolve_model(prompt, config) end --- Get the current source buffer and window. @@ -813,30 +533,33 @@ function M.ask(prompt, config) -- After opening window we need to schedule to next cycle so everything properly resolves schedule(function() - -- Prepare chat if not config.headless then + -- Prepare chat store_sticky(prompt) M.chat:start() M.chat:append('\n') end - -- Resolve prompt references - config, prompt = M.resolve_prompt(prompt, config) - local system_prompt = config.system_prompt or '' - - -- Remove sticky prefix - prompt = table.concat( - vim.tbl_map(function(l) - return l:gsub('^>%s+', '') - end, vim.split(prompt, '\n')), - '\n' - ) - async.run(handle_error(config, function() + config, prompt = M.resolve_prompt(prompt, config) + local system_prompt = config.system_prompt or '' local selected_tools, prompt = M.resolve_tools(prompt, config) - local resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) + local resolved_resources, resolved_tools, resolved_stickies, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) + -- Remove sticky prefix + prompt = table.concat( + vim.tbl_map(function(l) + return l:gsub('^>%s+', '') + end, vim.split(prompt, '\n')), + '\n' + ) + + -- Add resolved stickies to state + for _, sticky in ipairs(resolved_stickies) do + table.insert(state.sticky, sticky) + end + prompt = vim.trim(prompt) if not config.headless then @@ -916,7 +639,7 @@ function M.ask(prompt, config) end), }) - -- If there was no error and no response, it means job was cancelled + -- If there was no error and no response, it means job was canceled if ask_response == nil then return end diff --git a/lua/CopilotChat/instructions/custom_instructions.lua b/lua/CopilotChat/instructions/custom_instructions.lua new file mode 100644 index 00000000..57b1ba44 --- /dev/null +++ b/lua/CopilotChat/instructions/custom_instructions.lua @@ -0,0 +1,6 @@ +return [[ + +Custom instructions from user's `{FILENAME}`: +{CONTENT} + +]] diff --git a/lua/CopilotChat/prompt.lua b/lua/CopilotChat/prompt.lua new file mode 100644 index 00000000..b35da0f2 --- /dev/null +++ b/lua/CopilotChat/prompt.lua @@ -0,0 +1,390 @@ +local client = require('CopilotChat.client') +local constants = require('CopilotChat.constants') +local functions = require('CopilotChat.functions') +local notify = require('CopilotChat.notify') +local files = require('CopilotChat.utils.files') +local utils = require('CopilotChat.utils') + +local WORD = '([^%s:]+)' +local WORD_NO_INPUT = '([^%s]+)' +local WORD_WITH_INPUT_QUOTED = WORD .. ':`([^`]+)`' +local WORD_WITH_INPUT_UNQUOTED = WORD .. ':?([^%s`]*)' + +--- List available models. +--- @return CopilotChat.client.Model[] +local function list_models() + local models = client:models() + local result = vim.tbl_keys(models) + + table.sort(result, function(a, b) + a = models[a] + b = models[b] + if a.provider ~= b.provider then + return a.provider < b.provider + end + return a.id < b.id + end) + + return vim.tbl_map(function(id) + return models[id] + end, result) +end + +--- List available prompts. +---@return table +local function list_prompts() + local config = require('CopilotChat.config') + local prompts_to_use = {} + + for name, prompt in pairs(config.prompts) do + local val = prompt + if type(prompt) == 'string' then + val = { + prompt = prompt, + } + end + + prompts_to_use[name] = val + end + + return prompts_to_use +end + +--- Find custom instructions in the current working directory. +---@param cwd string +---@return table +local function find_custom_instructions(cwd) + local out = {} + local copilot_instructions_path = vim.fs.joinpath(cwd, '.github', 'copilot-instructions.md') + local copilot_instructions = files.read_file(copilot_instructions_path) + if copilot_instructions then + table.insert(out, { + filename = copilot_instructions_path, + content = vim.trim(copilot_instructions), + }) + end + return out +end + +local M = {} + +--- Resolve enabled tools from the prompt. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return table, string +function M.resolve_tools(prompt, config) + config, prompt = M.resolve_prompt(prompt, config) + + local tools = {} + for _, tool in ipairs(functions.parse_tools(config.functions)) do + tools[tool.name] = tool + end + + local enabled_tools = {} + local tool_matches = utils.to_table(config.tools) + + -- Check for @tool pattern to find enabled tools + prompt = prompt:gsub('@' .. WORD, function(match) + for name, tool in pairs(config.functions) do + if name == match or tool.group == match then + table.insert(tool_matches, match) + return '' + end + end + return '@' .. match + end) + for _, match in ipairs(tool_matches) do + for name, tool in pairs(config.functions) do + if name == match or tool.group == match then + table.insert(enabled_tools, tools[name]) + end + end + end + + return enabled_tools, prompt +end + +--- Call and resolve function calls from the prompt. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return table, table, table, string +---@async +function M.resolve_functions(prompt, config) + config, prompt = M.resolve_prompt(prompt, config) + + local chat = require('CopilotChat').chat + local source = require('CopilotChat').get_source() + + local tools = {} + for _, tool in ipairs(functions.parse_tools(config.functions)) do + tools[tool.name] = tool + end + + if config.resources then + local resources = utils.to_table(config.resources) + local lines = utils.split_lines(prompt) + for i = #resources, 1, -1 do + local resource = resources[i] + table.insert(lines, 1, '#' .. resource) + end + prompt = table.concat(lines, '\n') + end + + local resolved_resources = {} + local resolved_tools = {} + local resolved_stickies = {} + local tool_calls = {} + + utils.schedule_main() + for _, message in ipairs(chat:get_messages()) do + if message.tool_calls then + for _, tool_call in ipairs(message.tool_calls) do + table.insert(tool_calls, tool_call) + end + end + end + + local resource_matches = {} + + -- Check for #word:`input` pattern + for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_QUOTED) do + local pattern = string.format('#%s:`%s`', word, input) + table.insert(resource_matches, { + pattern = pattern, + word = word, + input = input, + }) + end + + -- Check for #word:input pattern + for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_UNQUOTED) do + local pattern = utils.empty(input) and string.format('#%s', word) or string.format('#%s:%s', word, input) + table.insert(resource_matches, { + pattern = pattern, + word = word, + input = input, + }) + end + + -- Check for ##word:input pattern + for word in prompt:gmatch('##' .. WORD_NO_INPUT) do + local pattern = string.format('##%s', word) + table.insert(resource_matches, { + pattern = pattern, + word = word, + }) + end + + -- Resolve each function reference + local function expand_function(name, input) + notify.publish(notify.STATUS, 'Running function: ' .. name) + + local tool_id = nil + if not utils.empty(tool_calls) then + for _, tool_call in ipairs(tool_calls) do + if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) then + input = utils.empty(tool_call.arguments) and {} or utils.json_decode(tool_call.arguments) + tool_id = tool_call.id + break + end + end + end + + local tool = config.functions[name] + if not tool then + -- Check if input matches uri + for tool_name, tool_spec in pairs(config.functions) do + if tool_spec.uri then + local match = functions.match_uri(name, tool_spec.uri) + if match then + name = tool_name + tool = tool_spec + input = match + break + end + end + end + end + if not tool then + return nil + end + if not tool_id and not tool.uri then + return nil + end + + local schema = tools[name] and tools[name].schema or nil + local ok, output + if config.stop_on_function_failure then + output = tool.resolve(functions.parse_input(input, schema), source) + ok = true + else + ok, output = pcall(tool.resolve, functions.parse_input(input, schema), source) + end + + local result = '' + if not ok then + result = utils.make_string(output) + else + for _, content in ipairs(output) do + if content then + local content_out = nil + if content.uri then + if + not vim.tbl_contains(resolved_resources, function(resource) + return resource.uri == content.uri + end, { predicate = true }) + then + content_out = '##' .. content.uri + table.insert(resolved_resources, content) + end + + if tool_id then + table.insert(resolved_stickies, '##' .. content.uri) + end + else + content_out = content.data + end + + if content_out then + if not utils.empty(result) then + result = result .. '\n' + end + result = result .. content_out + end + end + end + end + + if tool_id then + table.insert(resolved_tools, { + id = tool_id, + result = result, + }) + + return '' + end + + return result + end + + -- Resolve and process all tools + for _, match in ipairs(resource_matches) do + if not utils.empty(match.pattern) then + local out = expand_function(match.word, match.input) + if out == nil then + out = match.pattern + end + out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub + prompt = prompt:gsub(vim.pesc(match.pattern), out, 1) + end + end + + return resolved_resources, resolved_tools, resolved_stickies, prompt +end + +--- Resolve the final prompt and config from prompt template. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return CopilotChat.config.prompts.Prompt, string +---@async +function M.resolve_prompt(prompt, config) + local chat = require('CopilotChat').chat + local source = require('CopilotChat').get_source() + + if prompt == nil then + utils.schedule_main() + local message = chat:get_message(constants.ROLE.USER) + if message then + prompt = message.content + end + end + + local prompts_to_use = list_prompts() + local depth = 0 + local MAX_DEPTH = 10 + + local function resolve(inner_config, inner_prompt) + if depth >= MAX_DEPTH then + return inner_config, inner_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_config, resolved_prompt = resolve(p, p.prompt or '') + inner_config = vim.tbl_deep_extend('force', inner_config, resolved_config) + return resolved_prompt + end + + return '/' .. match + end) + + depth = depth - 1 + return inner_config, inner_prompt + end + + config = vim.tbl_deep_extend('force', require('CopilotChat.config'), config or {}) + config, prompt = resolve(config, prompt or '') + + if config.system_prompt then + if config.prompts[config.system_prompt] then + -- Name references are good for making system prompt auto sticky + config.system_prompt = config.prompts[config.system_prompt].system_prompt + end + + local custom_instructions = vim.trim(require('CopilotChat.instructions.custom_instructions')) + for _, instruction in ipairs(find_custom_instructions(source.cwd())) do + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. custom_instructions:gsub('{FILENAME}', instruction.filename):gsub('{CONTENT}', instruction.content) + end + + config.system_prompt = vim.trim(config.system_prompt) .. '\n' .. config.prompts.COPILOT_BASE.system_prompt + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. vim.trim(require('CopilotChat.instructions.tool_use')) + + if config.diff == 'unified' then + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. vim.trim(require('CopilotChat.instructions.edit_file_unified')) + else + config.system_prompt = vim.trim(config.system_prompt) + .. '\n' + .. vim.trim(require('CopilotChat.instructions.edit_file_block')) + end + + config.system_prompt = config.system_prompt:gsub('{OS_NAME}', vim.uv.os_uname().sysname) + config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) + config.system_prompt = config.system_prompt:gsub('{DIR}', source.cwd()) + end + + return config, prompt +end + +--- Resolve the model from the prompt. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return string, string +---@async +function M.resolve_model(prompt, config) + config, prompt = M.resolve_prompt(prompt, config) + + local models = vim.tbl_map(function(model) + return model.id + end, list_models()) + + local selected_model = config.model or '' + 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 + +return M diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 6ff56bac..17eb3091 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -54,7 +54,7 @@ local function apply_hunk(hunk, content) return patched, true end - -- Fallback: try smaller context window + -- Fallback: direct replacement local lines = vim.split(content, '\n') local insert_idx = hunk.start_old or 1 if not hunk.start_old then @@ -83,24 +83,8 @@ local function apply_hunk(hunk, content) end end - -- Define context window around insert point - local context_size = 10 local start_idx = insert_idx local end_idx = insert_idx + #hunk.old_snippet - local context_start = math.max(1, start_idx - context_size) - local context_end = math.min(#lines, end_idx + context_size) - local context_window = table.concat(vim.list_slice(lines, context_start, context_end), '\n') - - local patched_window, window_results = dmp.patch_apply(patch, context_window) - if not vim.tbl_contains(window_results, false) then - -- Patch succeeded in window, splice back - local new_lines = vim.list_slice(lines, 1, context_start - 1) - vim.list_extend(new_lines, vim.split(patched_window, '\n')) - vim.list_extend(new_lines, lines, context_end + 1, #lines) - return table.concat(new_lines, '\n'), true - end - - -- Fallback: direct replacement local new_lines = vim.list_slice(lines, 1, start_idx - 1) vim.list_extend(new_lines, hunk.new_snippet) vim.list_extend(new_lines, lines, end_idx + 1, #lines) From b967f5410fe43ff21a7cedacb5dc40f70ad041c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 20:21:14 +0000 Subject: [PATCH 640/728] 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 2c939e8d..168f7a56 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 18 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 21 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 4d9256418ca276b294f3e00445aa0d6faec4c1ea Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 21 Oct 2025 23:13:30 +0200 Subject: [PATCH 641/728] refactor: move list_prompts to prompts module completely (#1468) - Renamed `prompt.lua` to `prompts.lua` for clarity and consistency. - Moved prompt listing logic from `init.lua` to `prompts.lua`. - Updated all references to use the new `prompts` module. - Removed unused model and prompt listing functions from `init.lua`. --- lua/CopilotChat/init.lua | 62 +++++++-------------- lua/CopilotChat/{prompt.lua => prompts.lua} | 61 +++++++------------- 2 files changed, 38 insertions(+), 85 deletions(-) rename lua/CopilotChat/{prompt.lua => prompts.lua} (94%) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c738d4a8..0eb6f9de 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -2,7 +2,7 @@ local async = require('plenary.async') local log = require('plenary.log') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') -local prompts = require('CopilotChat.prompt') +local prompts = require('CopilotChat.prompts') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') @@ -145,45 +145,6 @@ local function store_sticky(prompt) state.sticky = sticky end ---- List available models. ---- @return CopilotChat.client.Model[] -local function list_models() - local models = client:models() - local result = vim.tbl_keys(models) - - table.sort(result, function(a, b) - a = models[a] - b = models[b] - if a.provider ~= b.provider then - return a.provider < b.provider - end - return a.id < b.id - end) - - return vim.tbl_map(function(id) - return models[id] - end, result) -end - ---- List available prompts. ----@return table -local function list_prompts() - local prompts_to_use = {} - - for name, prompt in pairs(M.config.prompts) do - local val = prompt - if type(prompt) == 'string' then - val = { - prompt = prompt, - } - end - - prompts_to_use[name] = val - end - - return prompts_to_use -end - --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) @@ -413,7 +374,22 @@ end --- Select default Copilot GPT model. function M.select_model() async.run(function() - local models = list_models() + local models = client:models() + local result = vim.tbl_keys(models) + + table.sort(result, function(a, b) + a = models[a] + b = models[b] + if a.provider ~= b.provider then + return a.provider < b.provider + end + return a.id < b.id + end) + + models = vim.tbl_map(function(id) + return models[id] + end, result) + local choices = vim.tbl_map(function(model) return { id = model.id, @@ -467,7 +443,7 @@ end --- Select a prompt template to use. ---@param config CopilotChat.config.Shared? function M.select_prompt(config) - local prompts = list_prompts() + local prompts = prompts.list_prompts() local keys = vim.tbl_keys(prompts) table.sort(keys) @@ -859,7 +835,7 @@ function M.setup(config) end) end - for name, prompt in pairs(list_prompts()) do + for name, prompt in pairs(prompts.list_prompts()) do if prompt.prompt then vim.api.nvim_create_user_command('CopilotChat' .. name, function(args) local input = prompt.prompt diff --git a/lua/CopilotChat/prompt.lua b/lua/CopilotChat/prompts.lua similarity index 94% rename from lua/CopilotChat/prompt.lua rename to lua/CopilotChat/prompts.lua index b35da0f2..c67c4ee7 100644 --- a/lua/CopilotChat/prompt.lua +++ b/lua/CopilotChat/prompts.lua @@ -10,29 +10,27 @@ local WORD_NO_INPUT = '([^%s]+)' local WORD_WITH_INPUT_QUOTED = WORD .. ':`([^`]+)`' local WORD_WITH_INPUT_UNQUOTED = WORD .. ':?([^%s`]*)' ---- List available models. ---- @return CopilotChat.client.Model[] -local function list_models() - local models = client:models() - local result = vim.tbl_keys(models) - - table.sort(result, function(a, b) - a = models[a] - b = models[b] - if a.provider ~= b.provider then - return a.provider < b.provider - end - return a.id < b.id - end) - - return vim.tbl_map(function(id) - return models[id] - end, result) +--- Find custom instructions in the current working directory. +---@param cwd string +---@return table +local function find_custom_instructions(cwd) + local out = {} + local copilot_instructions_path = vim.fs.joinpath(cwd, '.github', 'copilot-instructions.md') + local copilot_instructions = files.read_file(copilot_instructions_path) + if copilot_instructions then + table.insert(out, { + filename = copilot_instructions_path, + content = vim.trim(copilot_instructions), + }) + end + return out end +local M = {} + --- List available prompts. ---@return table -local function list_prompts() +function M.list_prompts() local config = require('CopilotChat.config') local prompts_to_use = {} @@ -50,24 +48,6 @@ local function list_prompts() return prompts_to_use end ---- Find custom instructions in the current working directory. ----@param cwd string ----@return table -local function find_custom_instructions(cwd) - local out = {} - local copilot_instructions_path = vim.fs.joinpath(cwd, '.github', 'copilot-instructions.md') - local copilot_instructions = files.read_file(copilot_instructions_path) - if copilot_instructions then - table.insert(out, { - filename = copilot_instructions_path, - content = vim.trim(copilot_instructions), - }) - end - return out -end - -local M = {} - --- Resolve enabled tools from the prompt. ---@param prompt string? ---@param config CopilotChat.config.Shared? @@ -299,7 +279,7 @@ function M.resolve_prompt(prompt, config) end end - local prompts_to_use = list_prompts() + local prompts_to_use = M.list_prompts() local depth = 0 local MAX_DEPTH = 10 @@ -370,10 +350,7 @@ end ---@async function M.resolve_model(prompt, config) config, prompt = M.resolve_prompt(prompt, config) - - local models = vim.tbl_map(function(model) - return model.id - end, list_models()) + local models = vim.tbl_keys(client:models()) local selected_model = config.model or '' prompt = prompt:gsub('%$' .. WORD, function(match) From 1ff0bb3b28d92e916d7c63a9189415ea2aaa24de Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 23 Oct 2025 07:49:12 +0200 Subject: [PATCH 642/728] refactor!: move source/sticky logic to chat window (#1469) This refactors the management of source and sticky prompt logic by moving it from the main module to the chat window implementation. The `CopilotChat.source` and sticky state are now encapsulated within the `CopilotChat.ui.chat.Chat` class, providing a cleaner separation of concerns and reducing global state. All related methods and type annotations have been updated accordingly. The sticky prompt insertion and retrieval logic is now handled via `get_sticky` and `set_sticky` methods on the chat window, and the source window/buffer is managed through `get_source` and `set_source` methods. This change also removes the sticky toggle mapping and updates documentation and function signatures for consistency. BREAKING CHANGE: get_source, and set_source methods moved to require('CopilotChat').chat --- README.md | 9 +- lua/CopilotChat/completion.lua | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/config/functions.lua | 2 +- lua/CopilotChat/config/mappings.lua | 36 +------- lua/CopilotChat/functions.lua | 2 +- lua/CopilotChat/init.lua | 132 +++++++-------------------- lua/CopilotChat/prompts.lua | 4 +- lua/CopilotChat/ui/chat.lua | 115 +++++++++++++---------- 9 files changed, 111 insertions(+), 193 deletions(-) diff --git a/README.md b/README.md index bcc84655..0daa5d60 100644 --- a/README.md +++ b/README.md @@ -408,10 +408,6 @@ chat.toggle(config) -- Toggle chat window visibility with optional con chat.reset() -- Reset the chat chat.stop() -- Stop current output --- Source Management -chat.get_source() -- Get the current source buffer and window -chat.set_source(winnr) -- Set the source window - -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector @@ -441,7 +437,6 @@ window:get_message(role, cursor) -- Get chat message by role, eith window:add_message({ role, content }, replace) -- Add or replace a message in chat window:remove_message(role, cursor) -- Remove chat message by role, either last or closest to cursor window:get_block(role, cursor) -- Get code block by role, either last or closest to cursor -window:add_sticky(sticky) -- Add sticky prompt to chat message -- Content Management window:append(text) -- Append text to chat window @@ -449,6 +444,10 @@ window:clear() -- Clear chat window content window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window +-- Source Management +window.get_source() -- Get the current source buffer and window +window.set_source(winnr) -- Set the source window + -- Navigation window:follow() -- Move cursor to end of chat content window:focus() -- Focus the chat window diff --git a/lua/CopilotChat/completion.lua b/lua/CopilotChat/completion.lua index 97b3e9d4..fdb509de 100644 --- a/lua/CopilotChat/completion.lua +++ b/lua/CopilotChat/completion.lua @@ -131,7 +131,7 @@ end --- Trigger the completion for the chat window. ---@param without_input boolean? function M.complete(without_input) - local source = require('CopilotChat').get_source() + local source = require('CopilotChat').chat:get_source() local info = M.info() local bufnr = vim.api.nvim_get_current_buf() local line = vim.api.nvim_get_current_line() diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 682923b3..b78ddf61 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -23,7 +23,7 @@ ---@field language string? ---@field temperature number? ---@field headless boolean? ----@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.source) +---@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.ui.chat.Source) ---@field remember_as_sticky boolean? ---@field window CopilotChat.config.Window? ---@field show_help boolean? diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 8d6d72ec..92226594 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -45,7 +45,7 @@ end ---@field schema table? ---@field group string? ---@field uri string? ----@field resolve fun(input: table, source: CopilotChat.source):CopilotChat.client.Resource[] +---@field resolve fun(input: table, source: CopilotChat.ui.chat.Source):CopilotChat.client.Resource[] ---@type table return { diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index a7cd5158..c286bfee 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -9,7 +9,7 @@ local files = require('CopilotChat.utils.files') --- Prepare a buffer for applying a diff ---@param filename string? ----@param source CopilotChat.source +---@param source CopilotChat.ui.chat.Source ---@return integer local function prepare_diff_buffer(filename, source) if not filename then @@ -47,7 +47,7 @@ end ---@class CopilotChat.config.mapping ---@field normal string? ---@field insert string? ----@field callback fun(source: CopilotChat.source) +---@field callback fun(source: CopilotChat.ui.chat.Source) ---@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping ---@field register string? @@ -57,7 +57,6 @@ end ---@field close CopilotChat.config.mapping|false|nil ---@field reset CopilotChat.config.mapping|false|nil ---@field submit_prompt CopilotChat.config.mapping|false|nil ----@field toggle_sticky CopilotChat.config.mapping|false|nil ---@field accept_diff CopilotChat.config.mapping|false|nil ---@field jump_to_diff CopilotChat.config.mapping|false|nil ---@field quickfix_diffs CopilotChat.config.mapping|false|nil @@ -103,37 +102,6 @@ return { end, }, - toggle_sticky = { - normal = 'grr', - callback = function() - local message = copilot.chat:get_message(constants.ROLE.USER) - local section = message and message.section - if not section then - return - end - - local cursor = vim.api.nvim_win_get_cursor(copilot.chat.winnr) - if cursor[1] < section.start_line or cursor[1] > section.end_line then - return - end - - local current_line = vim.trim(vim.api.nvim_get_current_line()) - if current_line == '' then - return - end - - local cur_line = cursor[1] - vim.api.nvim_buf_set_lines(copilot.chat.bufnr, cur_line - 1, cur_line, false, {}) - - if vim.startswith(current_line, '> ') then - return - end - - copilot.chat:add_sticky(current_line) - vim.api.nvim_win_set_cursor(copilot.chat.winnr, cursor) - end, - }, - accept_diff = { normal = '', insert = '', diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 9d0befdf..bed78c8a 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -199,7 +199,7 @@ end --- Get input from the user based on the schema ---@param schema table? ----@param source CopilotChat.source +---@param source CopilotChat.ui.chat.Source ---@return string? function M.enter_input(schema, source) if not schema or not schema.properties then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0eb6f9de..717e4801 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -30,35 +30,18 @@ local M = setmetatable({}, { end, }) ---- @class CopilotChat.source ---- @field bufnr number? ---- @field winnr number? ---- @field cwd fun():string - ---- @class CopilotChat.state ---- @field source CopilotChat.source ---- @field sticky string[] -local state = { - source = { - bufnr = nil, - winnr = nil, - cwd = function() - return '.' - end, - }, - - sticky = {}, -} - ---- Insert sticky values from config into prompt +--- Process sticky values from prompt and config +--- Extracts stickies from prompt, adds config-based stickies, stores them, returns clean prompt ---@param prompt string ---@param config CopilotChat.config.Shared -local function insert_sticky(prompt, config) +---@return string clean_prompt The prompt without sticky prefixes +local function process_sticky(prompt, config) local existing_prompt = M.chat:get_message(constants.ROLE.USER) local combined_prompt = (existing_prompt and existing_prompt.content or '') .. '\n' .. (prompt or '') local lines = vim.split(prompt or '', '\n') local stickies = orderedmap() + -- Extract existing stickies from combined prompt local sticky_indices = {} local in_code_block = false for _, line in ipairs(vim.split(combined_prompt, '\n')) do @@ -69,6 +52,8 @@ local function insert_sticky(prompt, config) stickies:set(vim.trim(line:sub(3)), true) end end + + -- Find sticky lines in new prompt to remove them for i, line in ipairs(lines) do if vim.startswith(line, '> ') then table.insert(sticky_indices, i) @@ -80,6 +65,7 @@ local function insert_sticky(prompt, config) lines = vim.split(vim.trim(table.concat(lines, '\n')), '\n') + -- Add config-based stickies if config.remember_as_sticky and config.model and config.model ~= M.config.model then stickies:set('$' .. config.model, true) end @@ -111,38 +97,17 @@ local function insert_sticky(prompt, config) end end - -- Insert stickies at start of prompt - local prompt_lines = {} + -- Store stickies + local sticky_array = {} for _, sticky in ipairs(stickies:keys()) do if sticky ~= '' then - table.insert(prompt_lines, '> ' .. sticky) + table.insert(sticky_array, sticky) end end - if #prompt_lines > 0 then - table.insert(prompt_lines, '') - end - for _, line in ipairs(lines) do - table.insert(prompt_lines, line) - end - if #lines == 0 then - table.insert(prompt_lines, '') - end + M.chat:set_sticky(sticky_array) - return table.concat(prompt_lines, '\n') -end - -local function store_sticky(prompt) - local sticky = {} - local in_code_block = false - for _, line in ipairs(vim.split(prompt, '\n')) do - if line:match('^```') then - in_code_block = not in_code_block - end - if vim.startswith(line, '> ') and not in_code_block then - table.insert(sticky, line:sub(3)) - end - end - state.sticky = sticky + -- Return clean prompt + return table.concat(lines, '\n') end --- Finish writing to chat buffer. @@ -155,15 +120,16 @@ local function finish(start_of_chat) table.insert(sticky, sticky_line) end end - state.sticky = sticky + M.chat:set_sticky(sticky) end local prompt_content = '' local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) local tool_calls = assistant_message and assistant_message.tool_calls or {} - if not utils.empty(state.sticky) then - for _, sticky in ipairs(state.sticky) do + local current_sticky = M.chat:get_sticky() + if not utils.empty(current_sticky) then + for _, sticky in ipairs(current_sticky) do prompt_content = prompt_content .. '> ' .. sticky .. '\n' end prompt_content = prompt_content .. '\n' @@ -231,7 +197,7 @@ local function map_key(name, bufnr, fn) if not fn then fn = function() - key.callback(state.source) + key.callback(M.chat:get_source()) end end @@ -261,7 +227,7 @@ end --- Updates the source buffer based on previous or current window. local function update_source() local use_prev_window = M.chat:focused() - M.set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) + M.chat:set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) end --- Resolve enabled tools from the prompt. @@ -299,39 +265,6 @@ function M.resolve_model(prompt, config) return prompts.resolve_model(prompt, config) end ---- Get the current source buffer and window. -function M.get_source() - return state.source -end - ---- Sets the source to the given window. ----@param source_winnr number ----@return boolean if the source was set -function M.set_source(source_winnr) - local source_bufnr = vim.api.nvim_win_get_buf(source_winnr) - - -- Check if the window is valid to use as a source - if source_winnr ~= M.chat.winnr and source_bufnr ~= M.chat.bufnr and vim.fn.win_gettype(source_winnr) == '' then - state.source = { - bufnr = source_bufnr, - winnr = source_winnr, - cwd = function() - local ok, dir = pcall(function() - return vim.w[source_winnr].cchat_cwd - end) - if not ok or not dir or dir == '' then - return '.' - end - return dir - end, - } - - return true - end - - return false -end - --- Open the chat window. ---@param config CopilotChat.config.Shared? function M.open(config) @@ -343,11 +276,11 @@ function M.open(config) -- Add sticky values from provided config when opening the chat local message = M.chat:get_message(constants.ROLE.USER) if message then - local prompt = insert_sticky(message.content, config) - if prompt then + local clean_prompt = process_sticky(message.content, config) + if clean_prompt and clean_prompt ~= '' then M.chat:add_message({ role = constants.ROLE.USER, - content = '\n' .. prompt, + content = '\n> ' .. table.concat(M.chat:get_sticky(), '\n> ') .. '\n\n' .. clean_prompt, }, true) end end @@ -358,7 +291,7 @@ end --- Close the chat window. function M.close() - M.chat:close(state.source.bufnr) + M.chat:close() end --- Toggle the chat window. @@ -504,14 +437,13 @@ function M.ask(prompt, config) end -- Resolve prompt after window is opened - prompt = insert_sticky(prompt, config) + prompt = process_sticky(prompt, config) prompt = vim.trim(prompt) -- After opening window we need to schedule to next cycle so everything properly resolves schedule(function() if not config.headless then -- Prepare chat - store_sticky(prompt) M.chat:start() M.chat:append('\n') end @@ -531,10 +463,12 @@ function M.ask(prompt, config) '\n' ) - -- Add resolved stickies to state + -- Add resolved stickies to chat + local current_sticky = M.chat:get_sticky() for _, sticky in ipairs(resolved_stickies) do - table.insert(state.sticky, sticky) + table.insert(current_sticky, sticky) end + M.chat:set_sticky(current_sticky) prompt = vim.trim(prompt) @@ -627,7 +561,7 @@ function M.ask(prompt, config) -- Call the callback function if config.callback then utils.schedule_main() - config.callback(response, state.source) + config.callback(response, M.chat:get_source()) end if not config.headless then @@ -656,7 +590,7 @@ function M.stop(reset) if reset then M.chat:clear() vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) - select.set(state.source.bufnr) + select.set(M.chat:get_source().bufnr) end if stopped or reset then @@ -797,7 +731,7 @@ function M.setup(config) -- Initialize chat if M.chat then - M.chat:close(state.source.bufnr) + M.chat:close() M.chat:delete() else M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) @@ -815,7 +749,7 @@ function M.setup(config) end vim.schedule(function() - select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) + select.highlight(M.chat:get_source().bufnr, not (M.config.highlight_selection and M.chat:focused())) end) end, }) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index c67c4ee7..21eb6cd4 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -93,7 +93,7 @@ function M.resolve_functions(prompt, config) config, prompt = M.resolve_prompt(prompt, config) local chat = require('CopilotChat').chat - local source = require('CopilotChat').get_source() + local source = chat:get_source() local tools = {} for _, tool in ipairs(functions.parse_tools(config.functions)) do @@ -269,7 +269,7 @@ end ---@async function M.resolve_prompt(prompt, config) local chat = require('CopilotChat').chat - local source = require('CopilotChat').get_source() + local source = chat:get_source() if prompt == nil then utils.schedule_main() diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 8cd08c11..0d2e9bc5 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -106,6 +106,11 @@ end ---@field id string? ---@field section CopilotChat.ui.chat.Section? +--- @class CopilotChat.ui.chat.Source +--- @field bufnr number? +--- @field winnr number? +--- @field cwd fun():string + ---@class CopilotChat.ui.chat.Chat : CopilotChat.ui.overlay.Overlay ---@field winnr number? ---@field config CopilotChat.config.Shared @@ -118,6 +123,8 @@ end ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay ---@field private last_changedtick number? +---@field private source CopilotChat.ui.chat.Source +---@field private sticky table local Chat = class(function(self, config, on_buf_create) Overlay.init(self, 'copilot-chat', utils.key_to_info('show_help', config.mappings.show_help), on_buf_create) @@ -127,6 +134,16 @@ local Chat = class(function(self, config, on_buf_create) self.token_max_count = nil self.messages = orderedmap() + self.source = { + bufnr = nil, + winnr = nil, + cwd = function() + return '.' + end, + } + + self.sticky = {} + self.layout = nil self.headers = {} for k, v in pairs(config.headers or {}) do @@ -269,53 +286,21 @@ function Chat:get_message(role, cursor) end end ---- Add a sticky line to the prompt in the chat window. ----@param sticky string -function Chat:add_sticky(sticky) - if not self:visible() then - return - end - - local prompt = self:get_message(constants.ROLE.USER) - if not prompt or not prompt.section then - return - end - - local lines = vim.split(prompt.content, '\n') - local insert_line = 1 - local first_one = true - local found = false - - for i = insert_line, #lines do - local line = lines[i] - if line and line ~= '' then - if vim.startswith(line, '> ') then - if line:sub(3) == sticky then - found = true - break - end - - first_one = false - else - break - end - elseif i >= 2 then - break - end - - insert_line = insert_line + 1 - end +--- Get the current sticky array. +---@return table +function Chat:get_sticky() + return self.sticky +end - if found then - return - end +--- Set the sticky array. +---@param sticky table +function Chat:set_sticky(sticky) + self.sticky = sticky +end - insert_line = prompt.section.start_line + insert_line - 1 - local to_insert = first_one and { '> ' .. sticky, '' } or { '> ' .. sticky } - local modifiable = vim.bo[self.bufnr].modifiable - vim.bo[self.bufnr].modifiable = true - vim.api.nvim_buf_set_lines(self.bufnr, insert_line - 1, insert_line - 1, false, to_insert) - vim.bo[self.bufnr].modifiable = modifiable +--- Clear the sticky array. +function Chat:clear_sticky() + self.sticky = {} end ---@class CopilotChat.ui.Chat.show_overlay @@ -430,8 +415,7 @@ function Chat:open(config) end --- Close the chat window. ----@param bufnr number? -function Chat:close(bufnr) +function Chat:close() if not self:visible() then return end @@ -441,8 +425,8 @@ function Chat:close(bufnr) end if self.layout == 'replace' then - if bufnr then - self:restore(self.winnr, bufnr) + if self.source.bufnr then + self:restore(self.winnr, self.source.bufnr) end else vim.api.nvim_win_close(self.winnr, true) @@ -919,4 +903,37 @@ function Chat:render() end end +--- Get the current source buffer and window. +function Chat:get_source() + return self.source +end + +--- Sets the source to the given window. +---@param source_winnr number +---@return boolean if the source was set +function Chat:set_source(source_winnr) + local source_bufnr = vim.api.nvim_win_get_buf(source_winnr) + + -- Check if the window is valid to use as a source + if source_winnr ~= self.winnr and source_bufnr ~= self.bufnr and vim.fn.win_gettype(source_winnr) == '' then + self.source = { + bufnr = source_bufnr, + winnr = source_winnr, + cwd = function() + local ok, dir = pcall(function() + return vim.w[source_winnr].cchat_cwd + end) + if not ok or not dir or dir == '' then + return '.' + end + return dir + end, + } + + return true + end + + return false +end + return Chat From 5d46a69645a78f68475f1fcb49b2902bc8066efe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Oct 2025 05:49:37 +0000 Subject: [PATCH 643/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 168f7a56..3c131191 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 21 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 23 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -484,10 +484,6 @@ CORE *CopilotChat-core* chat.reset() -- Reset the chat chat.stop() -- Stop current output - -- Source Management - chat.get_source() -- Get the current source buffer and window - chat.set_source(winnr) -- Set the source window - -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector @@ -518,7 +514,6 @@ You can also access the chat window UI methods through the `chat.chat` object: window:add_message({ role, content }, replace) -- Add or replace a message in chat window:remove_message(role, cursor) -- Remove chat message by role, either last or closest to cursor window:get_block(role, cursor) -- Get code block by role, either last or closest to cursor - window:add_sticky(sticky) -- Add sticky prompt to chat message -- Content Management window:append(text) -- Append text to chat window @@ -526,6 +521,10 @@ You can also access the chat window UI methods through the `chat.chat` object: window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window + -- Source Management + window.get_source() -- Get the current source buffer and window + window.set_source(winnr) -- Set the source window + -- Navigation window:follow() -- Move cursor to end of chat content window:focus() -- Focus the chat window From cb8fb0f888c5352bc96a2f0320e60bfb4ba478d8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 23 Oct 2025 08:04:36 +0200 Subject: [PATCH 644/728] refactor(chat)!: move prompt resolution helpers to prompts module (#1470) Moves the prompt resolution functions (`resolve_prompt`, `resolve_tools`, `resolve_model`, and `resolve_functions`) from the main CopilotChat module to the `CopilotChat.prompts` module. Updates all references and documentation to use the new location. This improves code organization by separating prompt parsing logic from the main chat interface. BREAKING CHANGE: The prompt resolution functions have been moved to `require('CopilotChat.prompts')` --- README.md | 13 +++-- lua/CopilotChat/config/mappings.lua | 78 ++++++++++++++++++----------- lua/CopilotChat/init.lua | 43 ++-------------- 3 files changed, 62 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 0daa5d60..b2d80dc0 100644 --- a/README.md +++ b/README.md @@ -397,9 +397,6 @@ local chat = require("CopilotChat") -- Basic Chat Functions chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text -chat.resolve_prompt() -- Resolve prompt references -chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM -chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management chat.open(config) -- Open chat window with optional config @@ -456,6 +453,16 @@ window:focus() -- Focus the chat window window:overlay(opts) -- Show overlay with specified options ``` +## Prompt parser + +```lua +local parser = require("CopilotChat.prompts") + +parser.resolve_prompt() -- Resolve prompt references +parser.resolve_tools() -- Resolve tools that are available for automatic use by LLM +parser.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) +``` + ## Example Usage ```lua diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index c286bfee..fb4b6388 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,10 +1,8 @@ local async = require('plenary.async') -local copilot = require('CopilotChat') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') -local diff = require('CopilotChat.utils.diff') local files = require('CopilotChat.utils.files') --- Prepare a buffer for applying a diff @@ -77,7 +75,7 @@ return { normal = 'q', insert = '', callback = function() - copilot.close() + require('CopilotChat').close() end, }, @@ -85,7 +83,7 @@ return { normal = '', insert = '', callback = function() - copilot.reset() + require('CopilotChat').reset() end, }, @@ -93,6 +91,7 @@ return { normal = '', insert = '', callback = function() + local copilot = require('CopilotChat') local message = copilot.chat:get_message(constants.ROLE.USER, true) if not message then return @@ -106,7 +105,10 @@ return { normal = '', insert = '', callback = function(source) - local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + local chat = require('CopilotChat').chat + local diff = require('CopilotChat.utils.diff') + + local block = chat:get_block(constants.ROLE.ASSISTANT, true) if not block then return end @@ -127,7 +129,10 @@ return { jump_to_diff = { normal = 'gj', callback = function(source) - local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + local chat = require('CopilotChat').chat + local diff = require('CopilotChat.utils.diff') + + local block = chat:get_block(constants.ROLE.ASSISTANT, true) if not block then return end @@ -147,19 +152,24 @@ return { normal = 'gy', register = '"', -- Default register to use for yanking callback = function() - local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + local config = require('CopilotChat.config') + local chat = require('CopilotChat').chat + local block = chat:get_block(constants.ROLE.ASSISTANT, true) if not block then return end - vim.fn.setreg(copilot.config.mappings.yank_diff.register, block.content) + vim.fn.setreg(config.mappings.yank_diff.register, block.content) end, }, show_diff = { normal = 'gd', callback = function(source) - local block = copilot.chat:get_block(constants.ROLE.ASSISTANT, true) + local chat = require('CopilotChat').chat + local diff = require('CopilotChat.utils.diff') + + local block = chat:get_block(constants.ROLE.ASSISTANT, true) if not block then return end @@ -168,7 +178,7 @@ return { local bufnr = prepare_diff_buffer(path, source) -- Collect all blocks for the same filename - local message = copilot.chat:get_message(constants.ROLE.ASSISTANT, true) + local message = chat:get_message(constants.ROLE.ASSISTANT, true) local blocks = {} if message and message.section and message.section.blocks then for _, b in ipairs(message.section.blocks) do @@ -196,26 +206,27 @@ return { vim.cmd('diffthis') end) - vim.api.nvim_win_call(copilot.chat.winnr, function() + vim.api.nvim_win_call(chat.winnr, function() vim.cmd('diffthis') end) end opts.on_hide = function() - vim.api.nvim_win_call(copilot.chat.winnr, function() + vim.api.nvim_win_call(chat.winnr, function() vim.cmd('diffoff') end) end - copilot.chat:overlay(opts) + chat:overlay(opts) end, }, quickfix_diffs = { normal = 'gqd', callback = function() + local chat = require('CopilotChat').chat local items = {} - local messages = copilot.chat:get_messages() + local messages = chat:get_messages() for _, message in ipairs(messages) do if message.section then for _, block in ipairs(message.section.blocks) do @@ -225,7 +236,7 @@ return { end table.insert(items, { - bufnr = copilot.chat.bufnr, + bufnr = chat.bufnr, lnum = block.start_line, end_lnum = block.end_line, text = text, @@ -242,8 +253,9 @@ return { quickfix_answers = { normal = 'gqa', callback = function() + local chat = require('CopilotChat').chat local items = {} - local messages = copilot.chat:get_messages() + local messages = chat:get_messages() for i, message in ipairs(messages) do if message.section and message.role == constants.ROLE.ASSISTANT then local prev_message = messages[i - 1] @@ -253,7 +265,7 @@ return { end table.insert(items, { - bufnr = copilot.chat.bufnr, + bufnr = chat.bufnr, lnum = message.section.start_line, end_lnum = message.section.end_line, text = text, @@ -269,7 +281,10 @@ return { show_info = { normal = 'gc', callback = function(source) - local message = copilot.chat:get_message(constants.ROLE.USER, true) + local chat = require('CopilotChat').chat + local prompts = require('CopilotChat.prompts') + + local message = chat:get_message(constants.ROLE.USER, true) if not message then return end @@ -277,11 +292,11 @@ return { local lines = {} async.run(function() - local config, prompt = copilot.resolve_prompt(message.content) + local config, prompt = prompts.resolve_prompt(message.content) local system_prompt = config.system_prompt - local resolved_resources = copilot.resolve_functions(prompt, config) - local selected_tools = copilot.resolve_tools(prompt, config) - local selected_model = copilot.resolve_model(prompt, config) + local resolved_resources = prompts.resolve_functions(prompt, config) + local selected_tools = prompts.resolve_tools(prompt, config) + local selected_model = prompts.resolve_model(prompt, config) local infos = client:info() selected_tools = vim.tbl_map(function(tool) @@ -289,8 +304,8 @@ return { end, selected_tools) utils.schedule_main() - table.insert(lines, '**Logs**: `' .. copilot.config.log_path .. '`') - table.insert(lines, '**History**: `' .. copilot.config.history_path .. '`') + table.insert(lines, '**Logs**: `' .. config.log_path .. '`') + table.insert(lines, '**History**: `' .. config.history_path .. '`') table.insert(lines, '') for provider, infolines in pairs(infos) do @@ -368,7 +383,7 @@ return { table.insert(lines, '') end - copilot.chat:overlay({ + chat:overlay({ text = vim.trim(table.concat(lines, '\n')) .. '\n', }) end) @@ -378,6 +393,9 @@ return { show_help = { normal = 'gh', callback = function() + local config = require('CopilotChat.config') + local chat = require('CopilotChat').chat + local chat_help = '**`Special tokens`**\n' chat_help = chat_help .. '`@` to share function\n' chat_help = chat_help .. '`#` to add resource\n' @@ -387,22 +405,22 @@ return { 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(copilot.config.mappings) + local chat_keys = vim.tbl_keys(config.mappings) table.sort(chat_keys, function(a, b) - a = copilot.config.mappings[a] + a = config.mappings[a] a = a and (a.normal or a.insert) or '' - b = copilot.config.mappings[b] + b = config.mappings[b] b = b and (b.normal or b.insert) or '' return a < b end) for _, name in ipairs(chat_keys) do - local info = utils.key_to_info(name, copilot.config.mappings[name], '`') + local info = utils.key_to_info(name, config.mappings[name], '`') if info ~= '' then chat_help = chat_help .. info .. '\n' end end - copilot.chat:overlay({ + chat:overlay({ text = chat_help, }) end, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 717e4801..64745e8c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -230,41 +230,6 @@ local function update_source() M.chat:set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) end ---- Resolve enabled tools from the prompt. ----@param prompt string? ----@param config CopilotChat.config.Shared? ----@return table, string -function M.resolve_tools(prompt, config) - return prompts.resolve_tools(prompt, config) -end - ---- Call and resolve function calls from the prompt. ----@param prompt string? ----@param config CopilotChat.config.Shared? ----@return table, table, table, string ----@async -function M.resolve_functions(prompt, config) - return prompts.resolve_functions(prompt, config) -end - ---- Resolve the final prompt and config from prompt template. ----@param prompt string? ----@param config CopilotChat.config.Shared? ----@return CopilotChat.config.prompts.Prompt, string ----@async -function M.resolve_prompt(prompt, config) - return prompts.resolve_prompt(prompt, config) -end - ---- Resolve the model from the prompt. ----@param prompt string? ----@param config CopilotChat.config.Shared? ----@return string, string ----@async -function M.resolve_model(prompt, config) - return prompts.resolve_model(prompt, config) -end - --- Open the chat window. ---@param config CopilotChat.config.Shared? function M.open(config) @@ -449,11 +414,11 @@ function M.ask(prompt, config) end async.run(handle_error(config, function() - config, prompt = M.resolve_prompt(prompt, config) + config, prompt = prompts.resolve_prompt(prompt, config) local system_prompt = config.system_prompt or '' - local selected_tools, prompt = M.resolve_tools(prompt, config) - local resolved_resources, resolved_tools, resolved_stickies, prompt = M.resolve_functions(prompt, config) - local selected_model, prompt = M.resolve_model(prompt, config) + local selected_tools, prompt = prompts.resolve_tools(prompt, config) + local resolved_resources, resolved_tools, resolved_stickies, prompt = prompts.resolve_functions(prompt, config) + local selected_model, prompt = prompts.resolve_model(prompt, config) -- Remove sticky prefix prompt = table.concat( From bd8b48e8851c8796c5ca7d0e55875a6ed77d9bd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Oct 2025 06:05:02 +0000 Subject: [PATCH 645/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3c131191..2da651a9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -27,6 +27,7 @@ Table of Contents *CopilotChat-table-of-contents* 5. API Reference |CopilotChat-api-reference| - Core |CopilotChat-core| - Chat Window |CopilotChat-chat-window| + - Prompt parser |CopilotChat-prompt-parser| - Example Usage |CopilotChat-example-usage| 6. Development |CopilotChat-development| - Setup |CopilotChat-setup| @@ -473,9 +474,6 @@ CORE *CopilotChat-core* -- Basic Chat Functions chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text - chat.resolve_prompt() -- Resolve prompt references - chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM - chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management chat.open(config) -- Open chat window with optional config @@ -534,6 +532,17 @@ You can also access the chat window UI methods through the `chat.chat` object: < +PROMPT PARSER *CopilotChat-prompt-parser* + +>lua + local parser = require("CopilotChat.prompts") + + parser.resolve_prompt() -- Resolve prompt references + parser.resolve_tools() -- Resolve tools that are available for automatic use by LLM + parser.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) +< + + EXAMPLE USAGE *CopilotChat-example-usage* >lua From 94dfc019f86659d3aeee54d5f1999f4c93a35aa6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 23 Oct 2025 10:40:38 +0200 Subject: [PATCH 646/728] fix(chat): reinsert stickies before prompt processing (#1472) This change ensures that sticky messages are properly reinserted before processing the prompt, preserving their context in the chat flow. The previous logic for removing sticky prefixes was removed, and stickies are now stored correctly after resolution. Closes #1471 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 64745e8c..212bcb55 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -404,6 +404,7 @@ function M.ask(prompt, config) -- Resolve prompt after window is opened prompt = process_sticky(prompt, config) prompt = vim.trim(prompt) + prompt = table.concat(M.chat:get_sticky(), '\n') .. '\n\n' .. prompt -- After opening window we need to schedule to next cycle so everything properly resolves schedule(function() @@ -420,15 +421,7 @@ function M.ask(prompt, config) local resolved_resources, resolved_tools, resolved_stickies, prompt = prompts.resolve_functions(prompt, config) local selected_model, prompt = prompts.resolve_model(prompt, config) - -- Remove sticky prefix - prompt = table.concat( - vim.tbl_map(function(l) - return l:gsub('^>%s+', '') - end, vim.split(prompt, '\n')), - '\n' - ) - - -- Add resolved stickies to chat + -- Store resolved stickies to chat local current_sticky = M.chat:get_sticky() for _, sticky in ipairs(resolved_stickies) do table.insert(current_sticky, sticky) From 746a6971d15eec41a4e22bfdb88cd9f0f135442d Mon Sep 17 00:00:00 2001 From: Mihamina Rakotomandimby Date: Fri, 24 Oct 2025 19:23:03 +0300 Subject: [PATCH 647/728] feat(provider): Support OpenAI Responses API (#1463) Closes #1442 --- lua/CopilotChat/config/providers.lua | 274 ++++++++++++++++++++++++++- 1 file changed, 272 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5d0126e6..5b4e95f9 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -196,6 +196,36 @@ local function get_github_models_token(tag) return github_device_flow(tag, '178c6fc778ccc68e1d6a', 'read:user copilot') end +--- Helper function to extract text content from Responses API output parts +---@param parts table Array of content parts from Responses API +---@return string The concatenated text content +local function extract_text_from_parts(parts) + local content = '' + if not parts or type(parts) ~= 'table' then + return content + end + + for _, part in ipairs(parts) do + if type(part) == 'table' then + -- Handle different content types from Responses API + if part.type == 'output_text' or part.type == 'text' then + content = content .. (part.text or '') + elseif part.output_text then + -- Handle nested output_text + if type(part.output_text) == 'string' then + content = content .. part.output_text + elseif type(part.output_text) == 'table' and part.output_text.text then + content = content .. part.output_text.text + end + end + elseif type(part) == 'string' then + content = content .. part + end + end + + return content +end + ---@class CopilotChat.config.providers.Options ---@field model CopilotChat.client.Model ---@field temperature number? @@ -308,6 +338,10 @@ M.copilot = { return model.capabilities.type == 'chat' and model.model_picker_enabled end) :map(function(model) + local supported_endpoints = model.supported_endpoints or {} + -- Pre-compute whether this model uses the Responses API + local use_responses = vim.tbl_contains(supported_endpoints, '/responses') + return { id = model.id, name = model.name, @@ -318,6 +352,7 @@ M.copilot = { tools = model.capabilities.supports.tool_calls, policy = not model['policy'] or model['policy']['state'] == 'enabled', version = model.version, + use_responses = use_responses, } end) :totable() @@ -347,6 +382,63 @@ M.copilot = { prepare_input = function(inputs, opts) local is_o1 = vim.startswith(opts.model.id, 'o1') + -- Check if this model uses the Responses API + if opts.model.use_responses then + -- Prepare input for Responses API + local instructions = nil + local input_messages = {} + + for _, msg in ipairs(inputs) do + if msg.role == constants.ROLE.SYSTEM then + -- Combine system messages as instructions + if instructions then + instructions = instructions .. '\n\n' .. msg.content + else + instructions = msg.content + end + else + -- Include the message in the input array + table.insert(input_messages, { + role = msg.role, + content = msg.content, + }) + end + end + + -- The Responses API expects the input field to be an array of message objects + local out = { + model = opts.model.id, + -- Always request streaming for Responses API (honor model.streaming or default to true) + stream = opts.model.streaming ~= false, + input = input_messages, + } + + -- Add instructions if we have any system messages + if instructions then + out.instructions = instructions + end + + -- Add tools for Responses API if available + if opts.tools and opts.model.tools then + out.tools = vim.tbl_map(function(tool) + return { + type = 'function', + ['function'] = { + name = tool.name, + description = tool.description, + parameters = tool.schema, + strict = true, + }, + } + end, opts.tools) + end + + -- Note: temperature is not supported by Responses API, so we don't include it + + return out + end + + -- Original Chat Completion API logic inputs = vim.tbl_map(function(input) local output = { role = input.role, @@ -411,7 +503,179 @@ M.copilot = { return out end, - prepare_output = function(output) + prepare_output = function(output, opts) + -- Check if this model uses the Responses API + if opts and opts.model and opts.model.use_responses then + -- Handle Responses API output format + local content = '' + local reasoning = '' + local finish_reason = nil + local total_tokens = 0 + local tool_calls = {} + + -- Check for error in response + if output.error then + -- Surface the error as a finish reason to stop processing + local error_msg = output.error + if type(error_msg) == 'table' then + error_msg = error_msg.message or vim.inspect(error_msg) + end + return { + content = '', + reasoning = '', + finish_reason = 'error: ' .. tostring(error_msg), + total_tokens = nil, + tool_calls = {}, + } + end + + if output.type then + -- This is a streaming response from Responses API + if output.type == 'response.created' or output.type == 'response.in_progress' then + -- In-progress events, we don't have content yet + return { + content = '', + reasoning = '', + finish_reason = nil, + total_tokens = nil, + tool_calls = {}, + } + elseif output.type == 'response.completed' then + -- Completed response: do NOT resend content here to avoid duplication. + -- Only signal finish and capture usage/reasoning. + local response = output.response + if response then + if response.reasoning and response.reasoning.summary then + reasoning = response.reasoning.summary + end + if response.usage then + total_tokens = response.usage.total_tokens + end + finish_reason = 'stop' + end + return { + content = '', + reasoning = reasoning, + finish_reason = finish_reason, + total_tokens = total_tokens, + tool_calls = {}, + } + elseif output.type == 'response.content.delta' or output.type == 'response.output_text.delta' then + -- Streaming content delta + if output.delta then + if type(output.delta) == 'string' then + content = output.delta + elseif type(output.delta) == 'table' then + if output.delta.content then + content = output.delta.content + elseif output.delta.output_text then + content = extract_text_from_parts({ output.delta.output_text }) + elseif output.delta.text then + content = output.delta.text + end + end + end + elseif output.type == 'response.delta' then + -- Handle response.delta with nested output_text + if output.delta and output.delta.output_text then + content = extract_text_from_parts({ output.delta.output_text }) + end + elseif output.type == 'response.content.done' or output.type == 'response.output_text.done' then + -- Terminal content event; keep streaming open until response.completed provides usage info + finish_reason = nil + elseif output.type == 'response.error' then + -- Handle error event + local error_msg = output.error + if type(error_msg) == 'table' then + error_msg = error_msg.message or vim.inspect(error_msg) + end + finish_reason = 'error: ' .. tostring(error_msg) + elseif output.type == 'response.tool_call.delta' then + -- Handle tool call delta events + if output.delta and output.delta.tool_calls then + for _, tool_call in ipairs(output.delta.tool_calls) do + local id = tool_call.id or ('tooluse_' .. (tool_call.index or 1)) + local existing_call = nil + for _, tc in ipairs(tool_calls) do + if tc.id == id then + existing_call = tc + break + end + end + if not existing_call then + table.insert(tool_calls, { + id = id, + index = tool_call.index or #tool_calls + 1, + name = tool_call.name or '', + arguments = tool_call.arguments or '', + }) + else + -- Append arguments + existing_call.arguments = existing_call.arguments .. (tool_call.arguments or '') + end + end + end + end + elseif output.response then + -- Non-streaming response or final response + local response = output.response + + -- Check for error in the response object + if response.error then + local error_msg = response.error + if type(error_msg) == 'table' then + error_msg = error_msg.message or vim.inspect(error_msg) + end + return { + content = '', + reasoning = '', + finish_reason = 'error: ' .. tostring(error_msg), + total_tokens = nil, + tool_calls = {}, + } + end + + if response.output and #response.output > 0 then + for _, msg in ipairs(response.output) do + if msg.content and #msg.content > 0 then + content = content .. extract_text_from_parts(msg.content) + end + -- Extract tool calls from output messages + if msg.tool_calls then + for i, tool_call in ipairs(msg.tool_calls) do + local id = tool_call.id or ('tooluse_' .. i) + table.insert(tool_calls, { + id = id, + index = tool_call.index or i, + name = tool_call.name or '', + arguments = tool_call.arguments or '', + }) + end + end + end + end + + if response.reasoning and response.reasoning.summary then + reasoning = response.reasoning.summary + end + + if response.usage then + total_tokens = response.usage.total_tokens + end + + finish_reason = response.status == 'completed' and 'stop' or nil + end + + return { + content = content, + reasoning = reasoning, + finish_reason = finish_reason, + total_tokens = total_tokens, + tool_calls = tool_calls, + } + end + + -- Original Chat Completion API logic local tool_calls = {} local choice @@ -458,7 +722,13 @@ M.copilot = { } end, - get_url = function() + get_url = function(opts) + -- Check if this model uses the Responses API + if opts and opts.model and opts.model.use_responses then + return 'https://api.githubcopilot.com/responses' + end + + -- Default to Chat Completion API return 'https://api.githubcopilot.com/chat/completions' end, } From a7138a0ee04d8af42c262554eccee168bbf1454f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 24 Oct 2025 16:23:26 +0000 Subject: [PATCH 648/728] 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 2da651a9..729d2e4b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 23 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 24 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 2ef8c894d74b85bf9d7207369f721064aeb9fb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=B6lfel?= Date: Sun, 2 Nov 2025 12:31:38 +0100 Subject: [PATCH 649/728] fix(functions): insert explicit selected buffers (#1477) --- lua/CopilotChat/config/functions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 92226594..f9063047 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -150,7 +150,7 @@ return { local name = vim.api.nvim_buf_get_name(buf) if name and name ~= '' then local display_name = vim.fn.fnamemodify(name, ':~:.') - table.insert(opts, { display = display_name, value = name }) + table.insert(opts, { display = display_name, value = tostring(buf) }) end end end From 4fd6dbae83220de920eb366fd05d72db0194ea76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 2 Nov 2025 11:31:57 +0000 Subject: [PATCH 650/728] 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 729d2e4b..0c3004d2 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 October 24 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 3d16702149d61d0c0b84a72756e6d2a65f3f30a8 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 12:32:54 +0100 Subject: [PATCH 651/728] docs: add towoe as a contributor for code (#1478) * 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 f658f7f4..8bbee71d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -466,6 +466,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/49014608?v=4", "profile": "https://github.com/ctchen222", "contributions": ["code"] + }, + { + "login": "towoe", + "name": "Tobias Wölfel", + "avatar_url": "https://avatars.githubusercontent.com/u/8666134?v=4", + "profile": "https://github.com/towoe", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b2d80dc0..0732a5d9 100644 --- a/README.md +++ b/README.md @@ -625,6 +625,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Samiul Islam
Samiul Islam

💻 Rui Costa
Rui Costa

💻 CTCHEN
CTCHEN

💻 + Tobias Wölfel
Tobias Wölfel

💻 From a1a7bf6ac7d4f0792174a0072b80adb5d361e086 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:13:45 +0100 Subject: [PATCH 652/728] [pre-commit.ci] pre-commit autoupdate (#1480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.3.0 → v2.3.1](https://github.com/JohnnyMorganz/StyLua/compare/v2.3.0...v2.3.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 7602f97a..ac8e688c 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: v2.3.0 + rev: v2.3.1 hooks: - id: stylua-github From db56110b19c702053e86d54b48d8d570c59d0b6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Nov 2025 20:14:10 +0000 Subject: [PATCH 653/728] 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 0c3004d2..cf023055 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 02 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 03 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -627,7 +627,7 @@ Apache License 2.0. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 8769d22bde355f7160f3649c3ca66e195b6fcddc Mon Sep 17 00:00:00 2001 From: Alexander Garcia Date: Sun, 9 Nov 2025 16:28:05 -0600 Subject: [PATCH 654/728] fix(chat): insert stickies passed to `open()` (#1476) Ensure that `stickies` that are passed as arguments to `open()` are merged with the static config and persisted in the chat window Fixes #1475 --- 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 212bcb55..9125bb4a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -242,10 +242,18 @@ function M.open(config) local message = M.chat:get_message(constants.ROLE.USER) if message then local clean_prompt = process_sticky(message.content, config) + local stickies = M.chat:get_sticky() + local content = '' + if not vim.tbl_isempty(stickies) then + content = '\n> ' .. table.concat(stickies, '\n> ') .. '\n\n' + end if clean_prompt and clean_prompt ~= '' then + content = content .. clean_prompt + end + if content ~= '' then M.chat:add_message({ role = constants.ROLE.USER, - content = '\n> ' .. table.concat(M.chat:get_sticky(), '\n> ') .. '\n\n' .. clean_prompt, + content = content, }, true) end end From e58b67720aa4eca9d6679f361dc9b40be892582b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 9 Nov 2025 22:28:25 +0000 Subject: [PATCH 655/728] 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 cf023055..539986b7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 03 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From ce485330c76a5b63ccfb02b7dd18890a748ca558 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 23:29:00 +0100 Subject: [PATCH 656/728] docs: add garcia5 as a contributor for code (#1484) * 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 8bbee71d..7d438502 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -473,6 +473,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/8666134?v=4", "profile": "https://github.com/towoe", "contributions": ["code"] + }, + { + "login": "garcia5", + "name": "Alexander Garcia", + "avatar_url": "https://avatars.githubusercontent.com/u/21695295?v=4", + "profile": "https://github.com/garcia5", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0732a5d9..ffdbb60b 100644 --- a/README.md +++ b/README.md @@ -626,6 +626,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Rui Costa
Rui Costa

💻 CTCHEN
CTCHEN

💻 Tobias Wölfel
Tobias Wölfel

💻 + Alexander Garcia
Alexander Garcia

💻 From d34aa9ed142fc9702a8f375ef2df5aaadb4fce97 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 19 Nov 2025 08:44:52 +0100 Subject: [PATCH 657/728] refactor(diff): remove diff-match-patch dependency (#1491) Remove the bundled diff-match-patch Lua port and replace its usage in the diff utility with a simpler, line-based matching and replacement algorithm. This reduces code complexity and removes a large vendored file. The new approach uses context-aware line matching to apply hunks, which should be sufficient for the plugin's needs. Closes #1490 Signed-off-by: Tomas Slusny --- README.md | 8 - lua/CopilotChat/utils/diff.lua | 127 +- lua/CopilotChat/vendor/diff_match_patch.lua | 2085 ------------------- tests/diff_spec.lua | 75 + 4 files changed, 156 insertions(+), 2139 deletions(-) delete mode 100644 lua/CopilotChat/vendor/diff_match_patch.lua diff --git a/README.md b/README.md index ffdbb60b..89607cbc 100644 --- a/README.md +++ b/README.md @@ -523,14 +523,6 @@ make test See [CONTRIBUTING.md](/CONTRIBUTING.md) for detailed guidelines. -# Acknowledgments - -## diff-match-patch - -CopilotChat.nvim includes [diff-match-patch (Lua port)](https://github.com/google/diff-match-patch) for diffing and patching functionality. -Copyright 2018 The diff-match-patch Authors. -Licensed under the Apache License 2.0. - # Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 17eb3091..51cf1887 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -40,55 +40,81 @@ local function parse_hunks(diff_text) return hunks end ---- Apply a single hunk to content, with fallback/context logic +--- Try to match old_snippet in lines starting at approximate start_line +---@param lines table +---@param old_snippet table +---@param approx_start number +---@param search_range number +---@return number? matched_start +local function find_best_match(lines, old_snippet, approx_start, search_range) + local best_idx, best_score = nil, -1 + local old_len = #old_snippet + + if old_len == 0 then + return approx_start + end + + local min_start = math.max(1, approx_start - search_range) + local max_start = math.min(#lines - old_len + 1, approx_start + search_range) + + for start_idx = min_start, max_start do + local score = 0 + for i = 1, old_len do + if vim.trim(lines[start_idx + i - 1] or '') == vim.trim(old_snippet[i] or '') then + score = score + 1 + end + end + + if score > best_score then + best_score = score + best_idx = start_idx + end + + if score == old_len then + return best_idx + end + end + + if best_score >= math.ceil(old_len * 0.8) then + return best_idx + end + + return nil +end + +--- Apply a single hunk to content ---@param hunk table ---@param content string ---@return string patched_content, boolean applied_cleanly local function apply_hunk(hunk, content) - local dmp = require('CopilotChat.vendor.diff_match_patch') - local patch = dmp.patch_make(table.concat(hunk.old_snippet, '\n'), table.concat(hunk.new_snippet, '\n')) - - -- First try: direct application - local patched, results = dmp.patch_apply(patch, content) - if not vim.tbl_contains(results, false) then - return patched, true - end - - -- Fallback: direct replacement local lines = vim.split(content, '\n') - local insert_idx = hunk.start_old or 1 - if not hunk.start_old then - -- No starting point, try to find best match - local match_idx, best_score = nil, -1 - local context_lines = vim.tbl_filter(function(line) - return line and line ~= '' - end, hunk.old_snippet) - local context_len = #context_lines - if context_len > 0 then - for i = 1, #lines - context_len + 1 do - local score = 0 - for j = 1, context_len do - if vim.trim(lines[i + j - 1] or '') == vim.trim(context_lines[j] or '') then - score = score + 1 - end - end - if score > best_score then - best_score = score - match_idx = i - end - end + local start_idx = hunk.start_old + + -- If we have a start line hint, try to find best match within +/- 2 lines + if start_idx and start_idx > 0 and start_idx <= #lines then + local match_idx = find_best_match(lines, hunk.old_snippet, start_idx, 2) + if match_idx then + start_idx = match_idx end - if best_score > 0 and match_idx then - insert_idx = match_idx + else + -- No valid start line, search for best match in whole content + local match_idx = find_best_match(lines, hunk.old_snippet, 1, #lines) + if match_idx then + start_idx = match_idx + else + start_idx = 1 end end - local start_idx = insert_idx - local end_idx = insert_idx + #hunk.old_snippet + -- Replace old lines with new lines + local end_idx = start_idx + #hunk.old_snippet - 1 local new_lines = vim.list_slice(lines, 1, start_idx - 1) vim.list_extend(new_lines, hunk.new_snippet) vim.list_extend(new_lines, lines, end_idx + 1, #lines) - return table.concat(new_lines, '\n'), false + + -- Check if we matched exactly at the hinted position + local applied_cleanly = find_best_match(lines, hunk.old_snippet, hunk.start_old or start_idx, 0) == start_idx + return table.concat(new_lines, '\n'), applied_cleanly end --- Apply unified diff to a table of lines and return new lines @@ -104,16 +130,25 @@ function M.apply_unified_diff(diff_text, original_content) new_content = patched applied = applied or ok end - local original_lines = vim.split(original_content, '\n', { trimempty = true }) + local new_lines = vim.split(new_content, '\n', { trimempty = true }) + local hunks = vim.diff( + original_content, + new_content, + { algorithm = 'myers', ctxlen = 10, interhunkctxlen = 10, ignore_whitespace_change = true, result_type = 'indices' } + ) + if not hunks or #hunks == 0 then + return new_lines, applied, nil, nil + end local first, last - local max_len = math.max(#original_lines, #new_lines) - for i = 1, max_len do - if original_lines[i] ~= new_lines[i] then - if not first then - first = i - end - last = i + for _, hunk in ipairs(hunks) do + local hunk_start = hunk[1] + local hunk_end = hunk[1] + hunk[2] - 1 + if not first or hunk_start < first then + first = hunk_start + end + if not last or hunk_end > last then + last = hunk_end end end return new_lines, applied, first, last @@ -129,7 +164,7 @@ function M.get_diff(block, lines) return block.content, content end - local patched_lines = vim.split(block.content, '\n') + local patched_lines = vim.split(block.content, '\n', { trimempty = true }) local start_idx = block.header.start_line local end_idx = block.header.end_line local original_lines = lines diff --git a/lua/CopilotChat/vendor/diff_match_patch.lua b/lua/CopilotChat/vendor/diff_match_patch.lua deleted file mode 100644 index b2c397d0..00000000 --- a/lua/CopilotChat/vendor/diff_match_patch.lua +++ /dev/null @@ -1,2085 +0,0 @@ ---[[ -* Diff Match and Patch -* Copyright 2018 The diff-match-patch Authors. -* https://github.com/google/diff-match-patch -* -* Based on the JavaScript implementation by Neil Fraser. -* Ported to Lua by Duncan Cross. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. ---]] - -local bit = require('bit') -local band, bor, lshift = bit.band, bit.bor, bit.lshift -local type, setmetatable, ipairs, select = type, setmetatable, ipairs, select -local unpack, tonumber, error = unpack, tonumber, error -local strsub, strbyte, strchar, gmatch, gsub = string.sub, string.byte, string.char, string.gmatch, string.gsub -local strmatch, strfind, strformat = string.match, string.find, string.format -local tinsert, tremove, tconcat = table.insert, table.remove, table.concat -local max, min, floor, ceil, abs = math.max, math.min, math.floor, math.ceil, math.abs -local clock = os.clock - --- Utility functions. - -local percentEncode_pattern = "[^A-Za-z0-9%-=;',./~!@#$%&*%(%)_%+ %?]" -local function percentEncode_replace(v) - return strformat('%%%02X', strbyte(v)) -end - -local function indexOf(a, b, start) - if #b == 0 then - return nil - end - return strfind(a, b, start, true) -end - -local htmlEncode_pattern = '[&<>\n]' -local htmlEncode_replace = { - ['&'] = '&', - ['<'] = '<', - ['>'] = '>', - ['\n'] = '¶
', -} - --- Public API Functions --- (Exported at the end of the script) - -local diff_main, diff_cleanupSemantic, diff_cleanupEfficiency, diff_levenshtein, diff_prettyHtml - -local match_main - -local patch_make, patch_toText, patch_fromText, patch_apply - ---[[ -* The data structure representing a diff is an array of tuples: -* {{DIFF_DELETE, 'Hello'}, {DIFF_INSERT, 'Goodbye'}, {DIFF_EQUAL, ' world.'}} -* which means: delete 'Hello', add 'Goodbye' and keep ' world.' ---]] -local DIFF_DELETE = -1 -local DIFF_INSERT = 1 -local DIFF_EQUAL = 0 - --- Number of seconds to map a diff before giving up (0 for infinity). -local Diff_Timeout = 1.0 --- Cost of an empty edit operation in terms of edit characters. -local Diff_EditCost = 4 --- At what point is no match declared (0.0 = perfection, 1.0 = very loose). -local Match_Threshold = 0.5 --- How far to search for a match (0 = exact location, 1000+ = broad match). --- A match this many characters away from the expected location will add --- 1.0 to the score (0.0 is a perfect match). -local Match_Distance = 1000 --- When deleting a large block of text (over ~64 characters), how close do --- the contents have to be to match the expected contents. (0.0 = perfection, --- 1.0 = very loose). Note that Match_Threshold controls how closely the --- end points of a delete need to match. -local Patch_DeleteThreshold = 0.5 --- Chunk size for context length. -local Patch_Margin = 4 --- The number of bits in an int. -local Match_MaxBits = 32 - -function settings(new) - if new then - Diff_Timeout = new.Diff_Timeout or Diff_Timeout - Diff_EditCost = new.Diff_EditCost or Diff_EditCost - Match_Threshold = new.Match_Threshold or Match_Threshold - Match_Distance = new.Match_Distance or Match_Distance - Patch_DeleteThreshold = new.Patch_DeleteThreshold or Patch_DeleteThreshold - Patch_Margin = new.Patch_Margin or Patch_Margin - Match_MaxBits = new.Match_MaxBits or Match_MaxBits - else - return { - Diff_Timeout = Diff_Timeout, - Diff_EditCost = Diff_EditCost, - Match_Threshold = Match_Threshold, - Match_Distance = Match_Distance, - Patch_DeleteThreshold = Patch_DeleteThreshold, - Patch_Margin = Patch_Margin, - Match_MaxBits = Match_MaxBits, - } - end -end - --- --------------------------------------------------------------------------- --- DIFF API --- --------------------------------------------------------------------------- - --- The private diff functions -local _diff_compute, _diff_bisect, _diff_halfMatchI, _diff_halfMatch, _diff_cleanupSemanticScore, _diff_cleanupSemanticLossless, _diff_cleanupMerge, _diff_commonPrefix, _diff_commonSuffix, _diff_commonOverlap, _diff_xIndex, _diff_text1, _diff_text2, _diff_toDelta, _diff_fromDelta - ---[[ -* Find the differences between two texts. Simplifies the problem by stripping -* any common prefix or suffix off the texts before diffing. -* @param {string} text1 Old string to be diffed. -* @param {string} text2 New string to be diffed. -* @param {boolean} opt_checklines Has no effect in Lua. -* @param {number} opt_deadline Optional time when the diff should be complete -* by. Used internally for recursive calls. Users should set DiffTimeout -* instead. -* @return {Array.>} Array of diff tuples. ---]] -function diff_main(text1, text2, opt_checklines, opt_deadline) - -- Set a deadline by which time the diff must be complete. - if opt_deadline == nil then - if Diff_Timeout <= 0 then - opt_deadline = 2 ^ 31 - else - opt_deadline = clock() + Diff_Timeout - end - end - local deadline = opt_deadline - - -- Check for null inputs. - if text1 == nil or text1 == nil then - error('Null inputs. (diff_main)') - end - - -- Check for equality (speedup). - if text1 == text2 then - if #text1 > 0 then - return { { DIFF_EQUAL, text1 } } - end - return {} - end - - -- LUANOTE: Due to the lack of Unicode support, Lua is incapable of - -- implementing the line-mode speedup. - local checklines = false - - -- Trim off common prefix (speedup). - local commonlength = _diff_commonPrefix(text1, text2) - local commonprefix - if commonlength > 0 then - commonprefix = strsub(text1, 1, commonlength) - text1 = strsub(text1, commonlength + 1) - text2 = strsub(text2, commonlength + 1) - end - - -- Trim off common suffix (speedup). - commonlength = _diff_commonSuffix(text1, text2) - local commonsuffix - if commonlength > 0 then - commonsuffix = strsub(text1, -commonlength) - text1 = strsub(text1, 1, -commonlength - 1) - text2 = strsub(text2, 1, -commonlength - 1) - end - - -- Compute the diff on the middle block. - local diffs = _diff_compute(text1, text2, checklines, deadline) - - -- Restore the prefix and suffix. - if commonprefix then - tinsert(diffs, 1, { DIFF_EQUAL, commonprefix }) - end - if commonsuffix then - diffs[#diffs + 1] = { DIFF_EQUAL, commonsuffix } - end - - _diff_cleanupMerge(diffs) - return diffs -end - ---[[ -* Reduce the number of edits by eliminating semantically trivial equalities. -* @param {Array.>} diffs Array of diff tuples. ---]] -function diff_cleanupSemantic(diffs) - local changes = false - local equalities = {} -- Stack of indices where equalities are found. - local equalitiesLength = 0 -- Keeping our own length var is faster. - local lastEquality = nil - -- Always equal to diffs[equalities[equalitiesLength]][2] - local pointer = 1 -- Index of current position. - -- Number of characters that changed prior to the equality. - local length_insertions1 = 0 - local length_deletions1 = 0 - -- Number of characters that changed after the equality. - local length_insertions2 = 0 - local length_deletions2 = 0 - - while diffs[pointer] do - if diffs[pointer][1] == DIFF_EQUAL then -- Equality found. - equalitiesLength = equalitiesLength + 1 - equalities[equalitiesLength] = pointer - length_insertions1 = length_insertions2 - length_deletions1 = length_deletions2 - length_insertions2 = 0 - length_deletions2 = 0 - lastEquality = diffs[pointer][2] - else -- An insertion or deletion. - if diffs[pointer][1] == DIFF_INSERT then - length_insertions2 = length_insertions2 + #diffs[pointer][2] - else - length_deletions2 = length_deletions2 + #diffs[pointer][2] - end - -- Eliminate an equality that is smaller or equal to the edits on both - -- sides of it. - if - lastEquality - and (#lastEquality <= max(length_insertions1, length_deletions1)) - and (#lastEquality <= max(length_insertions2, length_deletions2)) - then - -- Duplicate record. - tinsert(diffs, equalities[equalitiesLength], { DIFF_DELETE, lastEquality }) - -- Change second copy to insert. - diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT - -- Throw away the equality we just deleted. - equalitiesLength = equalitiesLength - 1 - -- Throw away the previous equality (it needs to be reevaluated). - equalitiesLength = equalitiesLength - 1 - pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 - length_insertions1, length_deletions1 = 0, 0 -- Reset the counters. - length_insertions2, length_deletions2 = 0, 0 - lastEquality = nil - changes = true - end - end - pointer = pointer + 1 - end - - -- Normalize the diff. - if changes then - _diff_cleanupMerge(diffs) - end - _diff_cleanupSemanticLossless(diffs) - - -- Find any overlaps between deletions and insertions. - -- e.g: abcxxxxxxdef - -- -> abcxxxdef - -- e.g: xxxabcdefxxx - -- -> defxxxabc - -- Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 2 - while diffs[pointer] do - if diffs[pointer - 1][1] == DIFF_DELETE and diffs[pointer][1] == DIFF_INSERT then - local deletion = diffs[pointer - 1][2] - local insertion = diffs[pointer][2] - local overlap_length1 = _diff_commonOverlap(deletion, insertion) - local overlap_length2 = _diff_commonOverlap(insertion, deletion) - if overlap_length1 >= overlap_length2 then - if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then - -- Overlap found. Insert an equality and trim the surrounding edits. - tinsert(diffs, pointer, { DIFF_EQUAL, strsub(insertion, 1, overlap_length1) }) - diffs[pointer - 1][2] = strsub(deletion, 1, #deletion - overlap_length1) - diffs[pointer + 1][2] = strsub(insertion, overlap_length1 + 1) - pointer = pointer + 1 - end - else - if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then - -- Reverse overlap found. - -- Insert an equality and swap and trim the surrounding edits. - tinsert(diffs, pointer, { DIFF_EQUAL, strsub(deletion, 1, overlap_length2) }) - diffs[pointer - 1] = { DIFF_INSERT, strsub(insertion, 1, #insertion - overlap_length2) } - diffs[pointer + 1] = { DIFF_DELETE, strsub(deletion, overlap_length2 + 1) } - pointer = pointer + 1 - end - end - pointer = pointer + 1 - end - pointer = pointer + 1 - end -end - ---[[ -* Reduce the number of edits by eliminating operationally trivial equalities. -* @param {Array.>} diffs Array of diff tuples. ---]] -function diff_cleanupEfficiency(diffs) - local changes = false - -- Stack of indices where equalities are found. - local equalities = {} - -- Keeping our own length var is faster. - local equalitiesLength = 0 - -- Always equal to diffs[equalities[equalitiesLength]][2] - local lastEquality = nil - -- Index of current position. - local pointer = 1 - - -- The following four are really booleans but are stored as numbers because - -- they are used at one point like this: - -- - -- (pre_ins + pre_del + post_ins + post_del) == 3 - -- - -- ...i.e. checking that 3 of them are true and 1 of them is false. - - -- Is there an insertion operation before the last equality. - local pre_ins = 0 - -- Is there a deletion operation before the last equality. - local pre_del = 0 - -- Is there an insertion operation after the last equality. - local post_ins = 0 - -- Is there a deletion operation after the last equality. - local post_del = 0 - - while diffs[pointer] do - if diffs[pointer][1] == DIFF_EQUAL then -- Equality found. - local diffText = diffs[pointer][2] - if (#diffText < Diff_EditCost) and (post_ins == 1 or post_del == 1) then - -- Candidate found. - equalitiesLength = equalitiesLength + 1 - equalities[equalitiesLength] = pointer - pre_ins, pre_del = post_ins, post_del - lastEquality = diffText - else - -- Not a candidate, and can never become one. - equalitiesLength = 0 - lastEquality = nil - end - post_ins, post_del = 0, 0 - else -- An insertion or deletion. - if diffs[pointer][1] == DIFF_DELETE then - post_del = 1 - else - post_ins = 1 - end - --[[ - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - --]] - if - lastEquality - and ( - (pre_ins + pre_del + post_ins + post_del == 4) - or ((#lastEquality < Diff_EditCost / 2) and (pre_ins + pre_del + post_ins + post_del == 3)) - ) - then - -- Duplicate record. - tinsert(diffs, equalities[equalitiesLength], { DIFF_DELETE, lastEquality }) - -- Change second copy to insert. - diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT - -- Throw away the equality we just deleted. - equalitiesLength = equalitiesLength - 1 - lastEquality = nil - if (pre_ins == 1) and (pre_del == 1) then - -- No changes made which could affect previous entry, keep going. - post_ins, post_del = 1, 1 - equalitiesLength = 0 - else - -- Throw away the previous equality. - equalitiesLength = equalitiesLength - 1 - pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 - post_ins, post_del = 0, 0 - end - changes = true - end - end - pointer = pointer + 1 - end - - if changes then - _diff_cleanupMerge(diffs) - end -end - ---[[ -* Compute the Levenshtein distance; the number of inserted, deleted or -* substituted characters. -* @param {Array.>} diffs Array of diff tuples. -* @return {number} Number of changes. ---]] -function diff_levenshtein(diffs) - local levenshtein = 0 - local insertions, deletions = 0, 0 - for x, diff in ipairs(diffs) do - local op, data = diff[1], diff[2] - if op == DIFF_INSERT then - insertions = insertions + #data - elseif op == DIFF_DELETE then - deletions = deletions + #data - elseif op == DIFF_EQUAL then - -- A deletion and an insertion is one substitution. - levenshtein = levenshtein + max(insertions, deletions) - insertions = 0 - deletions = 0 - end - end - levenshtein = levenshtein + max(insertions, deletions) - return levenshtein -end - ---[[ -* Convert a diff array into a pretty HTML report. -* @param {Array.>} diffs Array of diff tuples. -* @return {string} HTML representation. ---]] -function diff_prettyHtml(diffs) - local html = {} - for x, diff in ipairs(diffs) do - local op = diff[1] -- Operation (insert, delete, equal) - local data = diff[2] -- Text of change. - local text = gsub(data, htmlEncode_pattern, htmlEncode_replace) - if op == DIFF_INSERT then - html[x] = '' .. text .. '' - elseif op == DIFF_DELETE then - html[x] = '' .. text .. '' - elseif op == DIFF_EQUAL then - html[x] = '' .. text .. '' - end - end - return tconcat(html) -end - --- --------------------------------------------------------------------------- --- UNOFFICIAL/PRIVATE DIFF FUNCTIONS --- --------------------------------------------------------------------------- - ---[[ -* Find the differences between two texts. Assumes that the texts do not -* have any common prefix or suffix. -* @param {string} text1 Old string to be diffed. -* @param {string} text2 New string to be diffed. -* @param {boolean} checklines Has no effect in Lua. -* @param {number} deadline Time when the diff should be complete by. -* @return {Array.>} Array of diff tuples. -* @private ---]] -function _diff_compute(text1, text2, checklines, deadline) - if #text1 == 0 then - -- Just add some text (speedup). - return { { DIFF_INSERT, text2 } } - end - - if #text2 == 0 then - -- Just delete some text (speedup). - return { { DIFF_DELETE, text1 } } - end - - local diffs - - local longtext = (#text1 > #text2) and text1 or text2 - local shorttext = (#text1 > #text2) and text2 or text1 - local i = indexOf(longtext, shorttext) - - if i ~= nil then - -- Shorter text is inside the longer text (speedup). - diffs = { - { DIFF_INSERT, strsub(longtext, 1, i - 1) }, - { DIFF_EQUAL, shorttext }, - { DIFF_INSERT, strsub(longtext, i + #shorttext) }, - } - -- Swap insertions for deletions if diff is reversed. - if #text1 > #text2 then - diffs[1][1], diffs[3][1] = DIFF_DELETE, DIFF_DELETE - end - return diffs - end - - if #shorttext == 1 then - -- Single character string. - -- After the previous speedup, the character can't be an equality. - return { { DIFF_DELETE, text1 }, { DIFF_INSERT, text2 } } - end - - -- Check to see if the problem can be split in two. - do - local text1_a, text1_b, text2_a, text2_b, mid_common = _diff_halfMatch(text1, text2) - - if text1_a then - -- A half-match was found, sort out the return data. - -- Send both pairs off for separate processing. - local diffs_a = diff_main(text1_a, text2_a, checklines, deadline) - local diffs_b = diff_main(text1_b, text2_b, checklines, deadline) - -- Merge the results. - local diffs_a_len = #diffs_a - diffs = diffs_a - diffs[diffs_a_len + 1] = { DIFF_EQUAL, mid_common } - for i, b_diff in ipairs(diffs_b) do - diffs[diffs_a_len + 1 + i] = b_diff - end - return diffs - end - end - - return _diff_bisect(text1, text2, deadline) -end - ---[[ -* Find the 'middle snake' of a diff, split the problem in two -* and return the recursively constructed diff. -* See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. -* @param {string} text1 Old string to be diffed. -* @param {string} text2 New string to be diffed. -* @param {number} deadline Time at which to bail if not yet complete. -* @return {Array.>} Array of diff tuples. -* @private ---]] -function _diff_bisect(text1, text2, deadline) - -- Cache the text lengths to prevent multiple calls. - local text1_length = #text1 - local text2_length = #text2 - local _sub, _element - local max_d = ceil((text1_length + text2_length) / 2) - local v_offset = max_d - local v_length = 2 * max_d - local v1 = {} - local v2 = {} - -- Setting all elements to -1 is faster in Lua than mixing integers and nil. - for x = 0, v_length - 1 do - v1[x] = -1 - v2[x] = -1 - end - v1[v_offset + 1] = 0 - v2[v_offset + 1] = 0 - local delta = text1_length - text2_length - -- If the total number of characters is odd, then - -- the front path will collide with the reverse path. - local front = (delta % 2 ~= 0) - -- Offsets for start and end of k loop. - -- Prevents mapping of space beyond the grid. - local k1start = 0 - local k1end = 0 - local k2start = 0 - local k2end = 0 - for d = 0, max_d - 1 do - -- Bail out if deadline is reached. - if clock() > deadline then - break - end - - -- Walk the front path one step. - for k1 = -d + k1start, d - k1end, 2 do - local k1_offset = v_offset + k1 - local x1 - if (k1 == -d) or ((k1 ~= d) and (v1[k1_offset - 1] < v1[k1_offset + 1])) then - x1 = v1[k1_offset + 1] - else - x1 = v1[k1_offset - 1] + 1 - end - local y1 = x1 - k1 - while (x1 <= text1_length) and (y1 <= text2_length) and (strsub(text1, x1, x1) == strsub(text2, y1, y1)) do - x1 = x1 + 1 - y1 = y1 + 1 - end - v1[k1_offset] = x1 - if x1 > text1_length + 1 then - -- Ran off the right of the graph. - k1end = k1end + 2 - elseif y1 > text2_length + 1 then - -- Ran off the bottom of the graph. - k1start = k1start + 2 - elseif front then - local k2_offset = v_offset + delta - k1 - if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] ~= -1 then - -- Mirror x2 onto top-left coordinate system. - local x2 = text1_length - v2[k2_offset] + 1 - if x1 > x2 then - -- Overlap detected. - return _diff_bisectSplit(text1, text2, x1, y1, deadline) - end - end - end - end - - -- Walk the reverse path one step. - for k2 = -d + k2start, d - k2end, 2 do - local k2_offset = v_offset + k2 - local x2 - if (k2 == -d) or ((k2 ~= d) and (v2[k2_offset - 1] < v2[k2_offset + 1])) then - x2 = v2[k2_offset + 1] - else - x2 = v2[k2_offset - 1] + 1 - end - local y2 = x2 - k2 - while (x2 <= text1_length) and (y2 <= text2_length) and (strsub(text1, -x2, -x2) == strsub(text2, -y2, -y2)) do - x2 = x2 + 1 - y2 = y2 + 1 - end - v2[k2_offset] = x2 - if x2 > text1_length + 1 then - -- Ran off the left of the graph. - k2end = k2end + 2 - elseif y2 > text2_length + 1 then - -- Ran off the top of the graph. - k2start = k2start + 2 - elseif not front then - local k1_offset = v_offset + delta - k2 - if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] ~= -1 then - local x1 = v1[k1_offset] - local y1 = v_offset + x1 - k1_offset - -- Mirror x2 onto top-left coordinate system. - x2 = text1_length - x2 + 1 - if x1 > x2 then - -- Overlap detected. - return _diff_bisectSplit(text1, text2, x1, y1, deadline) - end - end - end - end - end - -- Diff took too long and hit the deadline or - -- number of diffs equals number of characters, no commonality at all. - return { { DIFF_DELETE, text1 }, { DIFF_INSERT, text2 } } -end - ---[[ - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {Array.>} Array of diff tuples. - * @private ---]] -function _diff_bisectSplit(text1, text2, x, y, deadline) - local text1a = strsub(text1, 1, x - 1) - local text2a = strsub(text2, 1, y - 1) - local text1b = strsub(text1, x) - local text2b = strsub(text2, y) - - -- Compute both diffs serially. - local diffs = diff_main(text1a, text2a, false, deadline) - local diffsb = diff_main(text1b, text2b, false, deadline) - - local diffs_len = #diffs - for i, v in ipairs(diffsb) do - diffs[diffs_len + i] = v - end - return diffs -end - ---[[ -* Determine the common prefix of two strings. -* @param {string} text1 First string. -* @param {string} text2 Second string. -* @return {number} The number of characters common to the start of each -* string. ---]] -function _diff_commonPrefix(text1, text2) - -- Quick check for common null cases. - if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, 1) ~= strbyte(text2, 1)) then - return 0 - end - -- Binary search. - -- Performance analysis: https://neil.fraser.name/news/2007/10/09/ - local pointermin = 1 - local pointermax = min(#text1, #text2) - local pointermid = pointermax - local pointerstart = 1 - while pointermin < pointermid do - if strsub(text1, pointerstart, pointermid) == strsub(text2, pointerstart, pointermid) then - pointermin = pointermid - pointerstart = pointermin - else - pointermax = pointermid - end - pointermid = floor(pointermin + (pointermax - pointermin) / 2) - end - return pointermid -end - ---[[ -* Determine the common suffix of two strings. -* @param {string} text1 First string. -* @param {string} text2 Second string. -* @return {number} The number of characters common to the end of each string. ---]] -function _diff_commonSuffix(text1, text2) - -- Quick check for common null cases. - if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, -1) ~= strbyte(text2, -1)) then - return 0 - end - -- Binary search. - -- Performance analysis: https://neil.fraser.name/news/2007/10/09/ - local pointermin = 1 - local pointermax = min(#text1, #text2) - local pointermid = pointermax - local pointerend = 1 - while pointermin < pointermid do - if strsub(text1, -pointermid, -pointerend) == strsub(text2, -pointermid, -pointerend) then - pointermin = pointermid - pointerend = pointermin - else - pointermax = pointermid - end - pointermid = floor(pointermin + (pointermax - pointermin) / 2) - end - return pointermid -end - ---[[ -* Determine if the suffix of one string is the prefix of another. -* @param {string} text1 First string. -* @param {string} text2 Second string. -* @return {number} The number of characters common to the end of the first -* string and the start of the second string. -* @private ---]] -function _diff_commonOverlap(text1, text2) - -- Cache the text lengths to prevent multiple calls. - local text1_length = #text1 - local text2_length = #text2 - -- Eliminate the null case. - if text1_length == 0 or text2_length == 0 then - return 0 - end - -- Truncate the longer string. - if text1_length > text2_length then - text1 = strsub(text1, text1_length - text2_length + 1) - elseif text1_length < text2_length then - text2 = strsub(text2, 1, text1_length) - end - local text_length = min(text1_length, text2_length) - -- Quick check for the worst case. - if text1 == text2 then - return text_length - end - - -- Start by looking for a single character match - -- and increase length until no match is found. - -- Performance analysis: https://neil.fraser.name/news/2010/11/04/ - local best = 0 - local length = 1 - while true do - local pattern = strsub(text1, text_length - length + 1) - local found = strfind(text2, pattern, 1, true) - if found == nil then - return best - end - length = length + found - 1 - if found == 1 or strsub(text1, text_length - length + 1) == strsub(text2, 1, length) then - best = length - length = length + 1 - end - end -end - ---[[ -* Does a substring of shorttext exist within longtext such that the substring -* is at least half the length of longtext? -* This speedup can produce non-minimal diffs. -* Closure, but does not reference any external variables. -* @param {string} longtext Longer string. -* @param {string} shorttext Shorter string. -* @param {number} i Start index of quarter length substring within longtext. -* @return {?Array.} Five element Array, containing the prefix of -* longtext, the suffix of longtext, the prefix of shorttext, the suffix -* of shorttext and the common middle. Or nil if there was no match. -* @private ---]] -function _diff_halfMatchI(longtext, shorttext, i) - -- Start with a 1/4 length substring at position i as a seed. - local seed = strsub(longtext, i, i + floor(#longtext / 4)) - local j = 0 -- LUANOTE: do not change to 1, was originally -1 - local best_common = '' - local best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b - while true do - j = indexOf(shorttext, seed, j + 1) - if j == nil then - break - end - local prefixLength = _diff_commonPrefix(strsub(longtext, i), strsub(shorttext, j)) - local suffixLength = _diff_commonSuffix(strsub(longtext, 1, i - 1), strsub(shorttext, 1, j - 1)) - if #best_common < suffixLength + prefixLength then - best_common = strsub(shorttext, j - suffixLength, j - 1) .. strsub(shorttext, j, j + prefixLength - 1) - best_longtext_a = strsub(longtext, 1, i - suffixLength - 1) - best_longtext_b = strsub(longtext, i + prefixLength) - best_shorttext_a = strsub(shorttext, 1, j - suffixLength - 1) - best_shorttext_b = strsub(shorttext, j + prefixLength) - end - end - if #best_common * 2 >= #longtext then - return { best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common } - else - return nil - end -end - ---[[ -* Do the two texts share a substring which is at least half the length of the -* longer text? -* @param {string} text1 First string. -* @param {string} text2 Second string. -* @return {?Array.} Five element Array, containing the prefix of -* text1, the suffix of text1, the prefix of text2, the suffix of -* text2 and the common middle. Or nil if there was no match. -* @private ---]] -function _diff_halfMatch(text1, text2) - if Diff_Timeout <= 0 then - -- Don't risk returning a non-optimal diff if we have unlimited time. - return nil - end - local longtext = (#text1 > #text2) and text1 or text2 - local shorttext = (#text1 > #text2) and text2 or text1 - if (#longtext < 4) or (#shorttext * 2 < #longtext) then - return nil -- Pointless. - end - - -- First check if the second quarter is the seed for a half-match. - local hm1 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 4)) - -- Check again based on the third quarter. - local hm2 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 2)) - local hm - if not hm1 and not hm2 then - return nil - elseif not hm2 then - hm = hm1 - elseif not hm1 then - hm = hm2 - else - -- Both matched. Select the longest. - hm = (#hm1[5] > #hm2[5]) and hm1 or hm2 - end - - -- A half-match was found, sort out the return data. - local text1_a, text1_b, text2_a, text2_b - if #text1 > #text2 then - text1_a, text1_b = hm[1], hm[2] - text2_a, text2_b = hm[3], hm[4] - else - text2_a, text2_b = hm[1], hm[2] - text1_a, text1_b = hm[3], hm[4] - end - local mid_common = hm[5] - return text1_a, text1_b, text2_a, text2_b, mid_common -end - ---[[ -* Given two strings, compute a score representing whether the internal -* boundary falls on logical boundaries. -* Scores range from 6 (best) to 0 (worst). -* @param {string} one First string. -* @param {string} two Second string. -* @return {number} The score. -* @private ---]] -function _diff_cleanupSemanticScore(one, two) - if (#one == 0) or (#two == 0) then - -- Edges are the best. - return 6 - end - - -- Each port of this function behaves slightly differently due to - -- subtle differences in each language's definition of things like - -- 'whitespace'. Since this function's purpose is largely cosmetic, - -- the choice has been made to use each language's native features - -- rather than force total conformity. - local char1 = strsub(one, -1) - local char2 = strsub(two, 1, 1) - local nonAlphaNumeric1 = strmatch(char1, '%W') - local nonAlphaNumeric2 = strmatch(char2, '%W') - local whitespace1 = nonAlphaNumeric1 and strmatch(char1, '%s') - local whitespace2 = nonAlphaNumeric2 and strmatch(char2, '%s') - local lineBreak1 = whitespace1 and strmatch(char1, '%c') - local lineBreak2 = whitespace2 and strmatch(char2, '%c') - local blankLine1 = lineBreak1 and strmatch(one, '\n\r?\n$') - local blankLine2 = lineBreak2 and strmatch(two, '^\r?\n\r?\n') - - if blankLine1 or blankLine2 then - -- Five points for blank lines. - return 5 - elseif lineBreak1 or lineBreak2 then - -- Four points for line breaks. - return 4 - elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then - -- Three points for end of sentences. - return 3 - elseif whitespace1 or whitespace2 then - -- Two points for whitespace. - return 2 - elseif nonAlphaNumeric1 or nonAlphaNumeric2 then - -- One point for non-alphanumeric. - return 1 - end - return 0 -end - ---[[ -* Look for single edits surrounded on both sides by equalities -* which can be shifted sideways to align the edit to a word boundary. -* e.g: The cat came. -> The cat came. -* @param {Array.>} diffs Array of diff tuples. ---]] -function _diff_cleanupSemanticLossless(diffs) - local pointer = 2 - -- Intentionally ignore the first and last element (don't need checking). - while diffs[pointer + 1] do - local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] - if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then - -- This is a single edit surrounded by equalities. - local diff = diffs[pointer] - - local equality1 = prevDiff[2] - local edit = diff[2] - local equality2 = nextDiff[2] - - -- First, shift the edit as far left as possible. - local commonOffset = _diff_commonSuffix(equality1, edit) - if commonOffset > 0 then - local commonString = strsub(edit, -commonOffset) - equality1 = strsub(equality1, 1, -commonOffset - 1) - edit = commonString .. strsub(edit, 1, -commonOffset - 1) - equality2 = commonString .. equality2 - end - - -- Second, step character by character right, looking for the best fit. - local bestEquality1 = equality1 - local bestEdit = edit - local bestEquality2 = equality2 - local bestScore = _diff_cleanupSemanticScore(equality1, edit) + _diff_cleanupSemanticScore(edit, equality2) - - while strbyte(edit, 1) == strbyte(equality2, 1) do - equality1 = equality1 .. strsub(edit, 1, 1) - edit = strsub(edit, 2) .. strsub(equality2, 1, 1) - equality2 = strsub(equality2, 2) - local score = _diff_cleanupSemanticScore(equality1, edit) + _diff_cleanupSemanticScore(edit, equality2) - -- The >= encourages trailing rather than leading whitespace on edits. - if score >= bestScore then - bestScore = score - bestEquality1 = equality1 - bestEdit = edit - bestEquality2 = equality2 - end - end - if prevDiff[2] ~= bestEquality1 then - -- We have an improvement, save it back to the diff. - if #bestEquality1 > 0 then - diffs[pointer - 1][2] = bestEquality1 - else - tremove(diffs, pointer - 1) - pointer = pointer - 1 - end - diffs[pointer][2] = bestEdit - if #bestEquality2 > 0 then - diffs[pointer + 1][2] = bestEquality2 - else - tremove(diffs, pointer + 1, 1) - pointer = pointer - 1 - end - end - end - pointer = pointer + 1 - end -end - ---[[ -* Reorder and merge like edit sections. Merge equalities. -* Any edit section can move as long as it doesn't cross an equality. -* @param {Array.>} diffs Array of diff tuples. ---]] -function _diff_cleanupMerge(diffs) - diffs[#diffs + 1] = { DIFF_EQUAL, '' } -- Add a dummy entry at the end. - local pointer = 1 - local count_delete, count_insert = 0, 0 - local text_delete, text_insert = '', '' - local commonlength - while diffs[pointer] do - local diff_type = diffs[pointer][1] - if diff_type == DIFF_INSERT then - count_insert = count_insert + 1 - text_insert = text_insert .. diffs[pointer][2] - pointer = pointer + 1 - elseif diff_type == DIFF_DELETE then - count_delete = count_delete + 1 - text_delete = text_delete .. diffs[pointer][2] - pointer = pointer + 1 - elseif diff_type == DIFF_EQUAL then - -- Upon reaching an equality, check for prior redundancies. - if count_delete + count_insert > 1 then - if (count_delete > 0) and (count_insert > 0) then - -- Factor out any common prefixies. - commonlength = _diff_commonPrefix(text_insert, text_delete) - if commonlength > 0 then - local back_pointer = pointer - count_delete - count_insert - if (back_pointer > 1) and (diffs[back_pointer - 1][1] == DIFF_EQUAL) then - diffs[back_pointer - 1][2] = diffs[back_pointer - 1][2] .. strsub(text_insert, 1, commonlength) - else - tinsert(diffs, 1, { DIFF_EQUAL, strsub(text_insert, 1, commonlength) }) - pointer = pointer + 1 - end - text_insert = strsub(text_insert, commonlength + 1) - text_delete = strsub(text_delete, commonlength + 1) - end - -- Factor out any common suffixies. - commonlength = _diff_commonSuffix(text_insert, text_delete) - if commonlength ~= 0 then - diffs[pointer][2] = strsub(text_insert, -commonlength) .. diffs[pointer][2] - text_insert = strsub(text_insert, 1, -commonlength - 1) - text_delete = strsub(text_delete, 1, -commonlength - 1) - end - end - -- Delete the offending records and add the merged ones. - pointer = pointer - count_delete - count_insert - for i = 1, count_delete + count_insert do - tremove(diffs, pointer) - end - if #text_delete > 0 then - tinsert(diffs, pointer, { DIFF_DELETE, text_delete }) - pointer = pointer + 1 - end - if #text_insert > 0 then - tinsert(diffs, pointer, { DIFF_INSERT, text_insert }) - pointer = pointer + 1 - end - pointer = pointer + 1 - elseif (pointer > 1) and (diffs[pointer - 1][1] == DIFF_EQUAL) then - -- Merge this equality with the previous one. - diffs[pointer - 1][2] = diffs[pointer - 1][2] .. diffs[pointer][2] - tremove(diffs, pointer) - else - pointer = pointer + 1 - end - count_insert, count_delete = 0, 0 - text_delete, text_insert = '', '' - end - end - if diffs[#diffs][2] == '' then - diffs[#diffs] = nil -- Remove the dummy entry at the end. - end - - -- Second pass: look for single edits surrounded on both sides by equalities - -- which can be shifted sideways to eliminate an equality. - -- e.g: ABAC -> ABAC - local changes = false - pointer = 2 - -- Intentionally ignore the first and last element (don't need checking). - while pointer < #diffs do - local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] - if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then - -- This is a single edit surrounded by equalities. - local diff = diffs[pointer] - local currentText = diff[2] - local prevText = prevDiff[2] - local nextText = nextDiff[2] - if #prevText == 0 then - tremove(diffs, pointer - 1) - changes = true - elseif strsub(currentText, -#prevText) == prevText then - -- Shift the edit over the previous equality. - diff[2] = prevText .. strsub(currentText, 1, -#prevText - 1) - nextDiff[2] = prevText .. nextDiff[2] - tremove(diffs, pointer - 1) - changes = true - elseif strsub(currentText, 1, #nextText) == nextText then - -- Shift the edit over the next equality. - prevDiff[2] = prevText .. nextText - diff[2] = strsub(currentText, #nextText + 1) .. nextText - tremove(diffs, pointer + 1) - changes = true - end - end - pointer = pointer + 1 - end - -- If shifts were made, the diff needs reordering and another shift sweep. - if changes then - -- LUANOTE: no return value, but necessary to use 'return' to get - -- tail calls. - return _diff_cleanupMerge(diffs) - end -end - ---[[ -* loc is a location in text1, compute and return the equivalent location in -* text2. -* e.g. 'The cat' vs 'The big cat', 1->1, 5->8 -* @param {Array.>} diffs Array of diff tuples. -* @param {number} loc Location within text1. -* @return {number} Location within text2. ---]] -function _diff_xIndex(diffs, loc) - local chars1 = 1 - local chars2 = 1 - local last_chars1 = 1 - local last_chars2 = 1 - local x - for _x, diff in ipairs(diffs) do - x = _x - if diff[1] ~= DIFF_INSERT then -- Equality or deletion. - chars1 = chars1 + #diff[2] - end - if diff[1] ~= DIFF_DELETE then -- Equality or insertion. - chars2 = chars2 + #diff[2] - end - if chars1 > loc then -- Overshot the location. - break - end - last_chars1 = chars1 - last_chars2 = chars2 - end - -- Was the location deleted? - if diffs[x + 1] and (diffs[x][1] == DIFF_DELETE) then - return last_chars2 - end - -- Add the remaining character length. - return last_chars2 + (loc - last_chars1) -end - ---[[ -* Compute and return the source text (all equalities and deletions). -* @param {Array.>} diffs Array of diff tuples. -* @return {string} Source text. ---]] -function _diff_text1(diffs) - local text = {} - for x, diff in ipairs(diffs) do - if diff[1] ~= DIFF_INSERT then - text[#text + 1] = diff[2] - end - end - return tconcat(text) -end - ---[[ -* Compute and return the destination text (all equalities and insertions). -* @param {Array.>} diffs Array of diff tuples. -* @return {string} Destination text. ---]] -function _diff_text2(diffs) - local text = {} - for x, diff in ipairs(diffs) do - if diff[1] ~= DIFF_DELETE then - text[#text + 1] = diff[2] - end - end - return tconcat(text) -end - ---[[ -* Crush the diff into an encoded string which describes the operations -* required to transform text1 into text2. -* E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. -* Operations are tab-separated. Inserted text is escaped using %xx notation. -* @param {Array.>} diffs Array of diff tuples. -* @return {string} Delta text. ---]] -function _diff_toDelta(diffs) - local text = {} - for x, diff in ipairs(diffs) do - local op, data = diff[1], diff[2] - if op == DIFF_INSERT then - text[x] = '+' .. gsub(data, percentEncode_pattern, percentEncode_replace) - elseif op == DIFF_DELETE then - text[x] = '-' .. #data - elseif op == DIFF_EQUAL then - text[x] = '=' .. #data - end - end - return tconcat(text, '\t') -end - ---[[ -* Given the original text1, and an encoded string which describes the -* operations required to transform text1 into text2, compute the full diff. -* @param {string} text1 Source string for the diff. -* @param {string} delta Delta text. -* @return {Array.>} Array of diff tuples. -* @throws {Errorend If invalid input. ---]] -function _diff_fromDelta(text1, delta) - local diffs = {} - local diffsLength = 0 -- Keeping our own length var is faster - local pointer = 1 -- Cursor in text1 - for token in gmatch(delta, '[^\t]+') do - -- Each token begins with a one character parameter which specifies the - -- operation of this token (delete, insert, equality). - local tokenchar, param = strsub(token, 1, 1), strsub(token, 2) - if tokenchar == '+' then - local invalidDecode = false - local decoded = gsub(param, '%%(.?.?)', function(c) - local n = tonumber(c, 16) - if (#c ~= 2) or (n == nil) then - invalidDecode = true - return '' - end - return strchar(n) - end) - if invalidDecode then - -- Malformed URI sequence. - error('Illegal escape in _diff_fromDelta: ' .. param) - end - diffsLength = diffsLength + 1 - diffs[diffsLength] = { DIFF_INSERT, decoded } - elseif (tokenchar == '-') or (tokenchar == '=') then - local n = tonumber(param) - if (n == nil) or (n < 0) then - error('Invalid number in _diff_fromDelta: ' .. param) - end - local text = strsub(text1, pointer, pointer + n - 1) - pointer = pointer + n - if tokenchar == '=' then - diffsLength = diffsLength + 1 - diffs[diffsLength] = { DIFF_EQUAL, text } - else - diffsLength = diffsLength + 1 - diffs[diffsLength] = { DIFF_DELETE, text } - end - else - error('Invalid diff operation in _diff_fromDelta: ' .. token) - end - end - if pointer ~= #text1 + 1 then - error('Delta length (' .. (pointer - 1) .. ') does not equal source text length (' .. #text1 .. ').') - end - return diffs -end - --- --------------------------------------------------------------------------- --- MATCH API --- --------------------------------------------------------------------------- - -local _match_bitap, _match_alphabet - ---[[ -* Locate the best instance of 'pattern' in 'text' near 'loc'. -* @param {string} text The text to search. -* @param {string} pattern The pattern to search for. -* @param {number} loc The location to search around. -* @return {number} Best match index or -1. ---]] -function match_main(text, pattern, loc) - -- Check for null inputs. - if text == nil or pattern == nil or loc == nil then - error('Null inputs. (match_main)') - end - - if text == pattern then - -- Shortcut (potentially not guaranteed by the algorithm) - return 1 - elseif #text == 0 then - -- Nothing to match. - return -1 - end - loc = max(1, min(loc, #text)) - if strsub(text, loc, loc + #pattern - 1) == pattern then - -- Perfect match at the perfect spot! (Includes case of null pattern) - return loc - else - -- Do a fuzzy compare. - return _match_bitap(text, pattern, loc) - end -end - --- --------------------------------------------------------------------------- --- UNOFFICIAL/PRIVATE MATCH FUNCTIONS --- --------------------------------------------------------------------------- - ---[[ -* Initialise the alphabet for the Bitap algorithm. -* @param {string} pattern The text to encode. -* @return {Object} Hash of character locations. -* @private ---]] -function _match_alphabet(pattern) - local s = {} - local i = 0 - for c in gmatch(pattern, '.') do - s[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1)) - i = i + 1 - end - return s -end - ---[[ -* Locate the best instance of 'pattern' in 'text' near 'loc' using the -* Bitap algorithm. -* @param {string} text The text to search. -* @param {string} pattern The pattern to search for. -* @param {number} loc The location to search around. -* @return {number} Best match index or -1. -* @private ---]] -function _match_bitap(text, pattern, loc) - if #pattern > Match_MaxBits then - error('Pattern too long.') - end - - -- Initialise the alphabet. - local s = _match_alphabet(pattern) - - --[[ - * Compute and return the score for a match with e errors and x location. - * Accesses loc and pattern through being a closure. - * @param {number} e Number of errors in match. - * @param {number} x Location of match. - * @return {number} Overall score for match (0.0 = good, 1.0 = bad). - * @private - --]] - local function _match_bitapScore(e, x) - local accuracy = e / #pattern - local proximity = abs(loc - x) - if Match_Distance == 0 then - -- Dodge divide by zero error. - return (proximity == 0) and 1 or accuracy - end - return accuracy + (proximity / Match_Distance) - end - - -- Highest score beyond which we give up. - local score_threshold = Match_Threshold - -- Is there a nearby exact match? (speedup) - local best_loc = indexOf(text, pattern, loc) - if best_loc then - score_threshold = min(_match_bitapScore(0, best_loc), score_threshold) - -- LUANOTE: Ideally we'd also check from the other direction, but Lua - -- doesn't have an efficent lastIndexOf function. - end - - -- Initialise the bit arrays. - local matchmask = lshift(1, #pattern - 1) - best_loc = -1 - - local bin_min, bin_mid - local bin_max = #pattern + #text - local last_rd - for d = 0, #pattern - 1, 1 do - -- Scan for the best match; each iteration allows for one more error. - -- Run a binary search to determine how far from 'loc' we can stray at this - -- error level. - bin_min = 0 - bin_mid = bin_max - while bin_min < bin_mid do - if _match_bitapScore(d, loc + bin_mid) <= score_threshold then - bin_min = bin_mid - else - bin_max = bin_mid - end - bin_mid = floor(bin_min + (bin_max - bin_min) / 2) - end - -- Use the result from this iteration as the maximum for the next. - bin_max = bin_mid - local start = max(1, loc - bin_mid + 1) - local finish = min(loc + bin_mid, #text) + #pattern - - local rd = {} - for j = start, finish do - rd[j] = 0 - end - rd[finish + 1] = lshift(1, d) - 1 - for j = finish, start, -1 do - local charMatch = s[strsub(text, j - 1, j - 1)] or 0 - if d == 0 then -- First pass: exact match. - rd[j] = band(bor((rd[j + 1] * 2), 1), charMatch) - else - -- Subsequent passes: fuzzy match. - -- Functions instead of operators make this hella messy. - rd[j] = bor( - band(bor(lshift(rd[j + 1], 1), 1), charMatch), - bor(bor(lshift(bor(last_rd[j + 1], last_rd[j]), 1), 1), last_rd[j + 1]) - ) - end - if band(rd[j], matchmask) ~= 0 then - local score = _match_bitapScore(d, j - 1) - -- This match will almost certainly be better than any existing match. - -- But check anyway. - if score <= score_threshold then - -- Told you so. - score_threshold = score - best_loc = j - 1 - if best_loc > loc then - -- When passing loc, don't exceed our current distance from loc. - start = max(1, loc * 2 - best_loc) - else - -- Already passed loc, downhill from here on in. - break - end - end - end - end - -- No hope for a (better) match at greater error levels. - if _match_bitapScore(d + 1, loc) > score_threshold then - break - end - last_rd = rd - end - return best_loc -end - --- ----------------------------------------------------------------------------- --- PATCH API --- ----------------------------------------------------------------------------- - -local _patch_addContext, _patch_deepCopy, _patch_addPadding, _patch_splitMax, _patch_appendText, _new_patch_obj - ---[[ -* Compute a list of patches to turn text1 into text2. -* Use diffs if provided, otherwise compute it ourselves. -* There are four ways to call this function, depending on what data is -* available to the caller: -* Method 1: -* a = text1, b = text2 -* Method 2: -* a = diffs -* Method 3 (optimal): -* a = text1, b = diffs -* Method 4 (deprecated, use method 3): -* a = text1, b = text2, c = diffs -* -* @param {string|Array.>} a text1 (methods 1,3,4) or -* Array of diff tuples for text1 to text2 (method 2). -* @param {string|Array.>} opt_b text2 (methods 1,4) or -* Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). -* @param {string|Array.>} opt_c Array of diff tuples for -* text1 to text2 (method 4) or undefined (methods 1,2,3). -* @return {Array.<_new_patch_obj>} Array of patch objects. ---]] -function patch_make(a, opt_b, opt_c) - local text1, diffs - local type_a, type_b, type_c = type(a), type(opt_b), type(opt_c) - if (type_a == 'string') and (type_b == 'string') and (type_c == 'nil') then - -- Method 1: text1, text2 - -- Compute diffs from text1 and text2. - text1 = a - diffs = diff_main(text1, opt_b, true) - if #diffs > 2 then - diff_cleanupSemantic(diffs) - diff_cleanupEfficiency(diffs) - end - elseif (type_a == 'table') and (type_b == 'nil') and (type_c == 'nil') then - -- Method 2: diffs - -- Compute text1 from diffs. - diffs = a - text1 = _diff_text1(diffs) - elseif (type_a == 'string') and (type_b == 'table') and (type_c == 'nil') then - -- Method 3: text1, diffs - text1 = a - diffs = opt_b - elseif (type_a == 'string') and (type_b == 'string') and (type_c == 'table') then - -- Method 4: text1, text2, diffs - -- text2 is not used. - text1 = a - diffs = opt_c - else - error('Unknown call format to patch_make.') - end - - if diffs[1] == nil then - return {} -- Get rid of the null case. - end - - local patches = {} - local patch = _new_patch_obj() - local patchDiffLength = 0 -- Keeping our own length var is faster. - local char_count1 = 0 -- Number of characters into the text1 string. - local char_count2 = 0 -- Number of characters into the text2 string. - -- Start with text1 (prepatch_text) and apply the diffs until we arrive at - -- text2 (postpatch_text). We recreate the patches one by one to determine - -- context info. - local prepatch_text, postpatch_text = text1, text1 - for x, diff in ipairs(diffs) do - local diff_type, diff_text = diff[1], diff[2] - - if (patchDiffLength == 0) and (diff_type ~= DIFF_EQUAL) then - -- A new patch starts here. - patch.start1 = char_count1 + 1 - patch.start2 = char_count2 + 1 - end - - if diff_type == DIFF_INSERT then - patchDiffLength = patchDiffLength + 1 - patch.diffs[patchDiffLength] = diff - patch.length2 = patch.length2 + #diff_text - postpatch_text = strsub(postpatch_text, 1, char_count2) .. diff_text .. strsub(postpatch_text, char_count2 + 1) - elseif diff_type == DIFF_DELETE then - patch.length1 = patch.length1 + #diff_text - patchDiffLength = patchDiffLength + 1 - patch.diffs[patchDiffLength] = diff - postpatch_text = strsub(postpatch_text, 1, char_count2) .. strsub(postpatch_text, char_count2 + #diff_text + 1) - elseif diff_type == DIFF_EQUAL then - if (#diff_text <= Patch_Margin * 2) and (patchDiffLength ~= 0) and (#diffs ~= x) then - -- Small equality inside a patch. - patchDiffLength = patchDiffLength + 1 - patch.diffs[patchDiffLength] = diff - patch.length1 = patch.length1 + #diff_text - patch.length2 = patch.length2 + #diff_text - elseif #diff_text >= Patch_Margin * 2 then - -- Time for a new patch. - if patchDiffLength ~= 0 then - _patch_addContext(patch, prepatch_text) - patches[#patches + 1] = patch - patch = _new_patch_obj() - patchDiffLength = 0 - -- Unlike Unidiff, our patch lists have a rolling context. - -- https://github.com/google/diff-match-patch/wiki/Unidiff - -- Update prepatch text & pos to reflect the application of the - -- just completed patch. - prepatch_text = postpatch_text - char_count1 = char_count2 - end - end - end - - -- Update the current character count. - if diff_type ~= DIFF_INSERT then - char_count1 = char_count1 + #diff_text - end - if diff_type ~= DIFF_DELETE then - char_count2 = char_count2 + #diff_text - end - end - - -- Pick up the leftover patch if not empty. - if patchDiffLength > 0 then - _patch_addContext(patch, prepatch_text) - patches[#patches + 1] = patch - end - - return patches -end - ---[[ -* Merge a set of patches onto the text. Return a patched text, as well -* as a list of true/false values indicating which patches were applied. -* @param {Array.<_new_patch_obj>} patches Array of patch objects. -* @param {string} text Old text. -* @return {Array.>} Two return values, the -* new text and an array of boolean values. ---]] -function patch_apply(patches, text) - if patches[1] == nil then - return text, {} - end - - -- Deep copy the patches so that no changes are made to originals. - patches = _patch_deepCopy(patches) - - local nullPadding = _patch_addPadding(patches) - text = nullPadding .. text .. nullPadding - - _patch_splitMax(patches) - -- delta keeps track of the offset between the expected and actual location - -- of the previous patch. If there are patches expected at positions 10 and - -- 20, but the first patch was found at 12, delta is 2 and the second patch - -- has an effective expected position of 22. - local delta = 0 - local results = {} - for x, patch in ipairs(patches) do - local expected_loc = patch.start2 + delta - local text1 = _diff_text1(patch.diffs) - local start_loc - local end_loc = -1 - if #text1 > Match_MaxBits then - -- _patch_splitMax will only provide an oversized pattern in - -- the case of a monster delete. - start_loc = match_main(text, strsub(text1, 1, Match_MaxBits), expected_loc) - if start_loc ~= -1 then - end_loc = match_main(text, strsub(text1, -Match_MaxBits), expected_loc + #text1 - Match_MaxBits) - if end_loc == -1 or start_loc >= end_loc then - -- Can't find valid trailing context. Drop this patch. - start_loc = -1 - end - end - else - start_loc = match_main(text, text1, expected_loc) - end - if start_loc == -1 then - -- No match found. :( - results[x] = false - -- Subtract the delta for this failed patch from subsequent patches. - delta = delta - patch.length2 - patch.length1 - else - -- Found a match. :) - results[x] = true - delta = start_loc - expected_loc - local text2 - if end_loc == -1 then - text2 = strsub(text, start_loc, start_loc + #text1 - 1) - else - text2 = strsub(text, start_loc, end_loc + Match_MaxBits - 1) - end - if text1 == text2 then - -- Perfect match, just shove the replacement text in. - text = strsub(text, 1, start_loc - 1) .. _diff_text2(patch.diffs) .. strsub(text, start_loc + #text1) - else - -- Imperfect match. Run a diff to get a framework of equivalent - -- indices. - local diffs = diff_main(text1, text2, false) - if (#text1 > Match_MaxBits) and (diff_levenshtein(diffs) / #text1 > Patch_DeleteThreshold) then - -- The end points match, but the content is unacceptably bad. - results[x] = false - else - _diff_cleanupSemanticLossless(diffs) - local index1 = 1 - local index2 - for y, mod in ipairs(patch.diffs) do - if mod[1] ~= DIFF_EQUAL then - index2 = _diff_xIndex(diffs, index1) - end - if mod[1] == DIFF_INSERT then - text = strsub(text, 1, start_loc + index2 - 2) .. mod[2] .. strsub(text, start_loc + index2 - 1) - elseif mod[1] == DIFF_DELETE then - text = strsub(text, 1, start_loc + index2 - 2) - .. strsub(text, start_loc + _diff_xIndex(diffs, index1 + #mod[2] - 1)) - end - if mod[1] ~= DIFF_DELETE then - index1 = index1 + #mod[2] - end - end - end - end - end - end - -- Strip the padding off. - text = strsub(text, #nullPadding + 1, -#nullPadding - 1) - return text, results -end - ---[[ -* Take a list of patches and return a textual representation. -* @param {Array.<_new_patch_obj>} patches Array of patch objects. -* @return {string} Text representation of patches. ---]] -function patch_toText(patches) - local text = {} - for x, patch in ipairs(patches) do - _patch_appendText(patch, text) - end - return tconcat(text) -end - ---[[ -* Parse a textual representation of patches and return a list of patch objects. -* @param {string} textline Text representation of patches. -* @return {Array.<_new_patch_obj>} Array of patch objects. -* @throws {Error} If invalid input. ---]] -function patch_fromText(textline) - local patches = {} - if #textline == 0 then - return patches - end - local text = {} - for line in gmatch(textline, '([^\n]*)') do - text[#text + 1] = line - end - local textPointer = 1 - while textPointer <= #text do - local start1, length1, start2, length2 = strmatch(text[textPointer], '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$') - if start1 == nil then - error('Invalid patch string: "' .. text[textPointer] .. '"') - end - local patch = _new_patch_obj() - patches[#patches + 1] = patch - - start1 = tonumber(start1) - length1 = tonumber(length1) or 1 - if length1 == 0 then - start1 = start1 + 1 - end - patch.start1 = start1 - patch.length1 = length1 - - start2 = tonumber(start2) - length2 = tonumber(length2) or 1 - if length2 == 0 then - start2 = start2 + 1 - end - patch.start2 = start2 - patch.length2 = length2 - - textPointer = textPointer + 1 - - while true do - local line = text[textPointer] - if line == nil then - break - end - local sign - sign, line = strsub(line, 1, 1), strsub(line, 2) - - local invalidDecode = false - local decoded = gsub(line, '%%(.?.?)', function(c) - local n = tonumber(c, 16) - if (#c ~= 2) or (n == nil) then - invalidDecode = true - return '' - end - return strchar(n) - end) - if invalidDecode then - -- Malformed URI sequence. - error('Illegal escape in patch_fromText: ' .. line) - end - - line = decoded - - if sign == '-' then - -- Deletion. - patch.diffs[#patch.diffs + 1] = { DIFF_DELETE, line } - elseif sign == '+' then - -- Insertion. - patch.diffs[#patch.diffs + 1] = { DIFF_INSERT, line } - elseif sign == ' ' then - -- Minor equality. - patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, line } - elseif sign == '@' then - -- Start of next patch. - break - elseif sign == '' then - -- Blank line? Whatever. - else - -- WTF? - error('Invalid patch mode "' .. sign .. '" in: ' .. line) - end - textPointer = textPointer + 1 - end - end - return patches -end - --- --------------------------------------------------------------------------- --- UNOFFICIAL/PRIVATE PATCH FUNCTIONS --- --------------------------------------------------------------------------- - -local patch_meta = { - __tostring = function(patch) - local buf = {} - _patch_appendText(patch, buf) - return tconcat(buf) - end, -} - ---[[ -* Class representing one patch operation. -* @constructor ---]] -function _new_patch_obj() - return setmetatable({ - --[[ @type {Array.>} ]] - diffs = {}, - --[[ @type {?number} ]] - start1 = 1, -- nil; - --[[ @type {?number} ]] - start2 = 1, -- nil; - --[[ @type {number} ]] - length1 = 0, - --[[ @type {number} ]] - length2 = 0, - }, patch_meta) -end - ---[[ -* Increase the context until it is unique, -* but don't let the pattern expand beyond Match_MaxBits. -* @param {_new_patch_obj} patch The patch to grow. -* @param {string} text Source text. -* @private ---]] -function _patch_addContext(patch, text) - if #text == 0 then - return - end - local pattern = strsub(text, patch.start2, patch.start2 + patch.length1 - 1) - local padding = 0 - - -- LUANOTE: Lua's lack of a lastIndexOf function results in slightly - -- different logic here than in other language ports. - -- Look for the first two matches of pattern in text. If two are found, - -- increase the pattern length. - local firstMatch = indexOf(text, pattern) - local secondMatch = nil - if firstMatch ~= nil then - secondMatch = indexOf(text, pattern, firstMatch + 1) - end - while (#pattern == 0 or secondMatch ~= nil) and (#pattern < Match_MaxBits - Patch_Margin - Patch_Margin) do - padding = padding + Patch_Margin - pattern = strsub(text, max(1, patch.start2 - padding), patch.start2 + patch.length1 - 1 + padding) - firstMatch = indexOf(text, pattern) - if firstMatch ~= nil then - secondMatch = indexOf(text, pattern, firstMatch + 1) - else - secondMatch = nil - end - end - -- Add one chunk for good luck. - padding = padding + Patch_Margin - - -- Add the prefix. - local prefix = strsub(text, max(1, patch.start2 - padding), patch.start2 - 1) - if #prefix > 0 then - tinsert(patch.diffs, 1, { DIFF_EQUAL, prefix }) - end - -- Add the suffix. - local suffix = strsub(text, patch.start2 + patch.length1, patch.start2 + patch.length1 - 1 + padding) - if #suffix > 0 then - patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, suffix } - end - - -- Roll back the start points. - patch.start1 = patch.start1 - #prefix - patch.start2 = patch.start2 - #prefix - -- Extend the lengths. - patch.length1 = patch.length1 + #prefix + #suffix - patch.length2 = patch.length2 + #prefix + #suffix -end - ---[[ -* Given an array of patches, return another array that is identical. -* @param {Array.<_new_patch_obj>} patches Array of patch objects. -* @return {Array.<_new_patch_obj>} Array of patch objects. ---]] -function _patch_deepCopy(patches) - local patchesCopy = {} - for x, patch in ipairs(patches) do - local patchCopy = _new_patch_obj() - local diffsCopy = {} - for i, diff in ipairs(patch.diffs) do - diffsCopy[i] = { diff[1], diff[2] } - end - patchCopy.diffs = diffsCopy - patchCopy.start1 = patch.start1 - patchCopy.start2 = patch.start2 - patchCopy.length1 = patch.length1 - patchCopy.length2 = patch.length2 - patchesCopy[x] = patchCopy - end - return patchesCopy -end - ---[[ -* Add some padding on text start and end so that edges can match something. -* Intended to be called only from within patch_apply. -* @param {Array.<_new_patch_obj>} patches Array of patch objects. -* @return {string} The padding string added to each side. ---]] -function _patch_addPadding(patches) - local paddingLength = Patch_Margin - local nullPadding = '' - for x = 1, paddingLength do - nullPadding = nullPadding .. strchar(x) - end - - -- Bump all the patches forward. - for x, patch in ipairs(patches) do - patch.start1 = patch.start1 + paddingLength - patch.start2 = patch.start2 + paddingLength - end - - -- Add some padding on start of first diff. - local patch = patches[1] - local diffs = patch.diffs - local firstDiff = diffs[1] - if (firstDiff == nil) or (firstDiff[1] ~= DIFF_EQUAL) then - -- Add nullPadding equality. - tinsert(diffs, 1, { DIFF_EQUAL, nullPadding }) - patch.start1 = patch.start1 - paddingLength -- Should be 0. - patch.start2 = patch.start2 - paddingLength -- Should be 0. - patch.length1 = patch.length1 + paddingLength - patch.length2 = patch.length2 + paddingLength - elseif paddingLength > #firstDiff[2] then - -- Grow first equality. - local extraLength = paddingLength - #firstDiff[2] - firstDiff[2] = strsub(nullPadding, #firstDiff[2] + 1) .. firstDiff[2] - patch.start1 = patch.start1 - extraLength - patch.start2 = patch.start2 - extraLength - patch.length1 = patch.length1 + extraLength - patch.length2 = patch.length2 + extraLength - end - - -- Add some padding on end of last diff. - patch = patches[#patches] - diffs = patch.diffs - local lastDiff = diffs[#diffs] - if (lastDiff == nil) or (lastDiff[1] ~= DIFF_EQUAL) then - -- Add nullPadding equality. - diffs[#diffs + 1] = { DIFF_EQUAL, nullPadding } - patch.length1 = patch.length1 + paddingLength - patch.length2 = patch.length2 + paddingLength - elseif paddingLength > #lastDiff[2] then - -- Grow last equality. - local extraLength = paddingLength - #lastDiff[2] - lastDiff[2] = lastDiff[2] .. strsub(nullPadding, 1, extraLength) - patch.length1 = patch.length1 + extraLength - patch.length2 = patch.length2 + extraLength - end - - return nullPadding -end - ---[[ -* Look through the patches and break up any which are longer than the maximum -* limit of the match algorithm. -* Intended to be called only from within patch_apply. -* @param {Array.<_new_patch_obj>} patches Array of patch objects. ---]] -function _patch_splitMax(patches) - local patch_size = Match_MaxBits - local x = 1 - while true do - local patch = patches[x] - if patch == nil then - return - end - if patch.length1 > patch_size then - local bigpatch = patch - -- Remove the big old patch. - tremove(patches, x) - x = x - 1 - local start1 = bigpatch.start1 - local start2 = bigpatch.start2 - local precontext = '' - while bigpatch.diffs[1] do - -- Create one of several smaller patches. - local patch = _new_patch_obj() - local empty = true - patch.start1 = start1 - #precontext - patch.start2 = start2 - #precontext - if precontext ~= '' then - patch.length1, patch.length2 = #precontext, #precontext - patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, precontext } - end - while bigpatch.diffs[1] and (patch.length1 < patch_size - Patch_Margin) do - local diff_type = bigpatch.diffs[1][1] - local diff_text = bigpatch.diffs[1][2] - if diff_type == DIFF_INSERT then - -- Insertions are harmless. - patch.length2 = patch.length2 + #diff_text - start2 = start2 + #diff_text - patch.diffs[#patch.diffs + 1] = bigpatch.diffs[1] - tremove(bigpatch.diffs, 1) - empty = false - elseif - (diff_type == DIFF_DELETE) - and (#patch.diffs == 1) - and (patch.diffs[1][1] == DIFF_EQUAL) - and (#diff_text > 2 * patch_size) - then - -- This is a large deletion. Let it pass in one chunk. - patch.length1 = patch.length1 + #diff_text - start1 = start1 + #diff_text - empty = false - patch.diffs[#patch.diffs + 1] = { diff_type, diff_text } - tremove(bigpatch.diffs, 1) - else - -- Deletion or equality. - -- Only take as much as we can stomach. - diff_text = strsub(diff_text, 1, patch_size - patch.length1 - Patch_Margin) - patch.length1 = patch.length1 + #diff_text - start1 = start1 + #diff_text - if diff_type == DIFF_EQUAL then - patch.length2 = patch.length2 + #diff_text - start2 = start2 + #diff_text - else - empty = false - end - patch.diffs[#patch.diffs + 1] = { diff_type, diff_text } - if diff_text == bigpatch.diffs[1][2] then - tremove(bigpatch.diffs, 1) - else - bigpatch.diffs[1][2] = strsub(bigpatch.diffs[1][2], #diff_text + 1) - end - end - end - -- Compute the head context for the next patch. - precontext = _diff_text2(patch.diffs) - precontext = strsub(precontext, -Patch_Margin) - -- Append the end context for this patch. - local postcontext = strsub(_diff_text1(bigpatch.diffs), 1, Patch_Margin) - if postcontext ~= '' then - patch.length1 = patch.length1 + #postcontext - patch.length2 = patch.length2 + #postcontext - if patch.diffs[1] and (patch.diffs[#patch.diffs][1] == DIFF_EQUAL) then - patch.diffs[#patch.diffs][2] = patch.diffs[#patch.diffs][2] .. postcontext - else - patch.diffs[#patch.diffs + 1] = { DIFF_EQUAL, postcontext } - end - end - if not empty then - x = x + 1 - tinsert(patches, x, patch) - end - end - end - x = x + 1 - end -end - ---[[ -* Emulate GNU diff's format. -* Header: @@ -382,8 +481,9 @@ -* @return {string} The GNU diff string. ---]] -function _patch_appendText(patch, text) - local coords1, coords2 - local length1, length2 = patch.length1, patch.length2 - local start1, start2 = patch.start1, patch.start2 - local diffs = patch.diffs - - if length1 == 1 then - coords1 = start1 - else - coords1 = ((length1 == 0) and (start1 - 1) or start1) .. ',' .. length1 - end - - if length2 == 1 then - coords2 = start2 - else - coords2 = ((length2 == 0) and (start2 - 1) or start2) .. ',' .. length2 - end - text[#text + 1] = '@@ -' .. coords1 .. ' +' .. coords2 .. ' @@\n' - - local op - -- Escape the body of the patch with %xx notation. - for x, diff in ipairs(patch.diffs) do - local diff_type = diff[1] - if diff_type == DIFF_INSERT then - op = '+' - elseif diff_type == DIFF_DELETE then - op = '-' - elseif diff_type == DIFF_EQUAL then - op = ' ' - end - text[#text + 1] = op .. gsub(diffs[x][2], percentEncode_pattern, percentEncode_replace) .. '\n' - end - - return text -end - --- Expose the API -local _M = {} - -_M.DIFF_DELETE = DIFF_DELETE -_M.DIFF_INSERT = DIFF_INSERT -_M.DIFF_EQUAL = DIFF_EQUAL - -_M.diff_main = diff_main -_M.diff_cleanupSemantic = diff_cleanupSemantic -_M.diff_cleanupEfficiency = diff_cleanupEfficiency -_M.diff_levenshtein = diff_levenshtein -_M.diff_prettyHtml = diff_prettyHtml - -_M.match_main = match_main - -_M.patch_make = patch_make -_M.patch_toText = patch_toText -_M.patch_fromText = patch_fromText -_M.patch_apply = patch_apply - --- Expose some non-API functions as well, for testing purposes etc. -_M.diff_commonPrefix = _diff_commonPrefix -_M.diff_commonSuffix = _diff_commonSuffix -_M.diff_commonOverlap = _diff_commonOverlap -_M.diff_halfMatch = _diff_halfMatch -_M.diff_bisect = _diff_bisect -_M.diff_cleanupMerge = _diff_cleanupMerge -_M.diff_cleanupSemanticLossless = _diff_cleanupSemanticLossless -_M.diff_text1 = _diff_text1 -_M.diff_text2 = _diff_text2 -_M.diff_toDelta = _diff_toDelta -_M.diff_fromDelta = _diff_fromDelta -_M.diff_xIndex = _diff_xIndex -_M.match_alphabet = _match_alphabet -_M.match_bitap = _match_bitap -_M.new_patch_obj = _new_patch_obj -_M.patch_addContext = _patch_addContext -_M.patch_splitMax = _patch_splitMax -_M.patch_addPadding = _patch_addPadding -_M.settings = settings - -return _M diff --git a/tests/diff_spec.lua b/tests/diff_spec.lua index 58e2f4c9..41ed262c 100644 --- a/tests/diff_spec.lua +++ b/tests/diff_spec.lua @@ -229,4 +229,79 @@ describe('CopilotChat.utils.diff', function() assert.is_true(applied) assert.are.same({ 'newstart', 'context' }, result) end) + + it('may confuse similar variable names', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,2 +1,2 @@ +-local x = 1 ++local x = 10 +]] + local original = { + 'local x = 1', + 'local y = 2', + 'local x = 3', + 'local z = 4', + } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ + 'local x = 10', + 'local y = 2', + 'local x = 3', + 'local z = 4', + }, result) + end) + + it('may match wrong substring with partial matches', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,2 +1,2 @@ +-old_value ++new_value +]] + local original = { + 'value', + 'old_value', + 'very_old_value', + } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_false(applied) -- not applied cleanly, but adjusted + assert.are.same({ + 'value', + 'new_value', + 'very_old_value', + }, result) + end) + + it('may apply to wrong instance of identical boilerplate code', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,3 +1,3 @@ + return { +- status = "old" ++ status = "new" +]] + local original = { + 'return {', + ' status = "old"', + '}', + 'return {', + ' status = "old"', + '}', + } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ + 'return {', + ' status = "new"', + '}', + 'return {', + ' status = "old"', + '}', + }, result) + end) end) From fc1655a51455df3435b3766bab9cc85c741ce220 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 07:45:11 +0000 Subject: [PATCH 658/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 539986b7..d071b89f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 19 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -32,11 +32,9 @@ Table of Contents *CopilotChat-table-of-contents* 6. Development |CopilotChat-development| - Setup |CopilotChat-setup| - Contributing |CopilotChat-contributing| -7. Acknowledgments |CopilotChat-acknowledgments| - - diff-match-patch |CopilotChat-diff-match-patch| -8. Contributors |CopilotChat-contributors| -9. Stargazers |CopilotChat-stargazers| -10. Links |CopilotChat-links| +7. Contributors |CopilotChat-contributors| +8. Stargazers |CopilotChat-stargazers| +9. Links |CopilotChat-links| CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. @@ -610,35 +608,23 @@ See CONTRIBUTING.md for detailed guidelines. ============================================================================== -7. Acknowledgments *CopilotChat-acknowledgments* - - -DIFF-MATCH-PATCH *CopilotChat-diff-match-patch* - -CopilotChat.nvim includes diff-match-patch (Lua port) - for diffing and patching -functionality. Copyright 2018 The diff-match-patch Authors. Licensed under the -Apache License 2.0. - - -============================================================================== -8. Contributors *CopilotChat-contributors* +7. Contributors *CopilotChat-contributors* 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻This project follows the all-contributors specification. Contributions of any kind are welcome! ============================================================================== -9. Stargazers *CopilotChat-stargazers* +8. Stargazers *CopilotChat-stargazers* ============================================================================== -10. Links *CopilotChat-links* +9. Links *CopilotChat-links* 1. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg?variant=adaptive From 9f9d587118d647724c854ac19a672a6b412d8c7d Mon Sep 17 00:00:00 2001 From: Max Kharandziuk Date: Wed, 19 Nov 2025 15:37:15 +0100 Subject: [PATCH 659/728] test: add comprehensive unified diff test coverage (#1489) --- lua/CopilotChat/utils/diff.lua | 22 ++- tests/diff_spec.lua | 308 +++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 51cf1887..a67518c9 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -16,9 +16,9 @@ local function parse_hunks(diff_text) local start_old, len_old, start_new, len_new = line:match('@@%s%-(%d+),?(%d*)%s%+(%d+),?(%d*)%s@@') current_hunk = { start_old = tonumber(start_old), - len_old = tonumber(len_old) or 1, + len_old = len_old == '' and 1 or tonumber(len_old), start_new = tonumber(start_new), - len_new = tonumber(len_new) or 1, + len_new = len_new == '' and 1 or tonumber(len_new), old_snippet = {}, new_snippet = {}, } @@ -90,6 +90,24 @@ local function apply_hunk(hunk, content) local lines = vim.split(content, '\n') local start_idx = hunk.start_old + -- Handle insertions (len_old == 0) + if hunk.len_old == 0 then + -- For insertions, start_old indicates where to insert + -- start_old = 0 means insert at beginning + -- start_old = n means insert after line n + if start_idx == 0 then + start_idx = 1 + else + start_idx = start_idx + 1 + end + local new_lines = vim.list_slice(lines, 1, start_idx - 1) + vim.list_extend(new_lines, hunk.new_snippet) + vim.list_extend(new_lines, lines, start_idx, #lines) + -- Insertions are always applied cleanly if we reach this point + return table.concat(new_lines, '\n'), true + end + + -- Handle replacements and deletions (len_old > 0) -- If we have a start line hint, try to find best match within +/- 2 lines if start_idx and start_idx > 0 and start_idx <= #lines then local match_idx = find_best_match(lines, hunk.old_snippet, start_idx, 2) diff --git a/tests/diff_spec.lua b/tests/diff_spec.lua index 41ed262c..21807810 100644 --- a/tests/diff_spec.lua +++ b/tests/diff_spec.lua @@ -304,4 +304,312 @@ describe('CopilotChat.utils.diff', function() '}', }, result) end) + + it('allows adding at very start with zero original lines', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -0,0 +1,2 @@ ++first ++second +]] + local original = { 'x', 'y' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'first', 'second', 'x', 'y' }, result) + end) + + it('handles insertion at end without context', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -3,0 +4,2 @@ ++new1 ++new2 +]] + local original = { 'a', 'b', 'c' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'a', 'b', 'c', 'new1', 'new2' }, result) + end) + + it('supports multiple adjacent hunks modifying contiguous lines', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,1 +1,1 @@ +-a ++x +@@ -2,1 +2,1 @@ +-b ++y +]] + local original = { 'a', 'b', 'c' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'x', 'y', 'c' }, result) + end) + + it('handles diff with trailing newline missing in original', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,1 +1,1 @@ +-old ++new +]] + local original_content = 'old' -- no trailing newline + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'new' }, result) + end) + + it('handles diff ending without newline on addition lines', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,1 +1,2 @@ + old ++new]] + local original = { 'old' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'old', 'new' }, result) + end) + + it('handles hunks with zero-context lines around changes', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -2,0 +3,1 @@ ++added +]] + local original = { 'a', 'b', 'c' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'a', 'b', 'added', 'c' }, result) + end) + + it('handles insertion of identical-to-context line', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,1 +1,2 @@ + context ++context +]] + local original = { 'context', 'other' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ 'context', 'context', 'other' }, result) + end) + + it('rejects hunk with wrong header lengths', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,3 +1,3 @@ + context +-old ++new +]] + local original = { 'context' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Fuzzy matching may still apply despite wrong header lengths + assert.is_not_nil(result) + end) + + it('handles CRLF original with unix diff', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,1 +1,1 @@ +-old ++new +]] + local original_content = 'old\r\n' + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.is_not_nil(result) + assert.is_true(#result >= 1) + end) + + it('handles large insertion with no context', function() + local lines = {} + for i = 1, 10 do + table.insert(lines, '+line' .. i) + end + local diff_text = '--- a/foo.txt\n+++ b/foo.txt\n@@ -4,0 +5,10 @@\n' .. table.concat(lines, '\n') .. '\n' + local original = { 'a', 'b', 'c', 'd', 'e' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + local expected = { 'a', 'b', 'c', 'd' } + for i = 1, 10 do + table.insert(expected, 'line' .. i) + end + table.insert(expected, 'e') + assert.are.same(expected, result) + end) + + it('rejects mismatched deletion ranges', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,3 +0,0 @@ +-old1 +-old2 +-old3 +]] + local original = { 'single' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Fuzzy matching may apply the deletion despite mismatch + assert.is_not_nil(result) + end) + + it('handles mixed operations in one hunk', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,5 +1,4 @@ + context1 +-old + unchanged +-old2 ++new2 + context2 +]] + local original = { 'context1', 'old', 'unchanged', 'old2', 'context2' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ 'context1', 'unchanged', 'new2', 'context2' }, result) + end) + + it('handles leading tabs/spaces inside context lines', function() + local diff_text = [[ +--- a/x ++++ b/x +@@ -1,2 +1,2 @@ + indented +-old ++new +]] + local original = { '\tindented', 'old' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ '\tindented', 'new' }, result) + end) + + it('respects diff markers even if content begins with + or -', function() + local diff_text = [[ +--- a/x ++++ b/x +@@ -1,2 +1,2 @@ +-+literalplus +--literalminus +++literalplus +++literalminus +]] + local original = { '+literalplus', '-literalminus' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ '+literalplus', '+literalminus' }, result) + end) + + it('applies diff despite slight context mismatch with fuzzy matching', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,3 +1,3 @@ + slightly different context +-old ++new +]] + local original = { 'context', 'old', 'other' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Fuzzy matching will replace context lines that don't match + assert.are.same({ 'slightly different context', 'new', 'other' }, result) + end) + + it('applies even when context is completely wrong due to fuzzy matching', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,3 +1,3 @@ + totally wrong line + another wrong line +-old ++new +]] + local original = { 'context1', 'context2', 'old' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Fuzzy matching will replace all old_snippet lines (including wrong context) with new_snippet + assert.are.same({ 'totally wrong line', 'another wrong line', 'new' }, result) + end) + + it('applies with partial context match', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -2,3 +2,3 @@ + matching +-old ++new +]] + local original = { 'first', 'matching', 'old', 'last' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + assert.is_true(applied) + assert.are.same({ 'first', 'matching', 'new', 'last' }, result) + end) + + it('handles context with extra lines not in original', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,5 +1,5 @@ + context1 + context2 + context3 +-old ++new +]] + local original = { 'context1', 'old' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Should fail or apply with fuzzy matching + assert.is_not_nil(result) + end) + + it('fails when deletion target does not exist', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,2 +1,1 @@ + context +-nonexistent +]] + local original = { 'context', 'actual' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Fuzzy matching might still apply or fail + assert.is_not_nil(result) + end) + + it('applies when context lines are in different order', function() + local diff_text = [[ +--- a/foo.txt ++++ b/foo.txt +@@ -1,3 +1,3 @@ + line2 + line1 +-old ++new +]] + local original = { 'line1', 'line2', 'old' } + local result, applied = diff.apply_unified_diff(diff_text, table.concat(original, '\n')) + -- Fuzzy matching should handle reordered context + assert.is_not_nil(result) + end) end) From be3291c5dca648d15c1e3a1433dc7118b22b188a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:38:37 +0100 Subject: [PATCH 660/728] docs: add kharandziuk as a contributor for code (#1492) * 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 7d438502..c4ad3996 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -480,6 +480,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/21695295?v=4", "profile": "https://github.com/garcia5", "contributions": ["code"] + }, + { + "login": "kharandziuk", + "name": "Max Kharandziuk", + "avatar_url": "https://avatars.githubusercontent.com/u/3404755?v=4", + "profile": "https://github.com/kharandziuk", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 89607cbc..6e923106 100644 --- a/README.md +++ b/README.md @@ -619,6 +619,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d CTCHEN
CTCHEN

💻 Tobias Wölfel
Tobias Wölfel

💻 Alexander Garcia
Alexander Garcia

💻 + Max Kharandziuk
Max Kharandziuk

💻 From 91400214254f9e6d601db7495ec2f41e67052dad Mon Sep 17 00:00:00 2001 From: Max Kharandziuk Date: Wed, 19 Nov 2025 23:37:40 +0100 Subject: [PATCH 661/728] fix(diff): implement offset tracking for sequential hunk application (#1493) - Adjust hunk start positions by cumulative offset to match patch utility behavior - Add comprehensive tests for offset logic (additions, deletions, mixed, context) - Ensure unified diff application is robust for multi-hunk scenarios --- lua/CopilotChat/utils/diff.lua | 13 +- tests/diff_spec.lua | 428 +++++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index a67518c9..c0cf2f85 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -143,10 +143,21 @@ function M.apply_unified_diff(diff_text, original_content) local hunks = parse_hunks(diff_text) local new_content = original_content local applied = false + local offset = 0 -- Track cumulative line offset from previous hunks + for _, hunk in ipairs(hunks) do - local patched, ok = apply_hunk(hunk, new_content) + -- Adjust hunk start position based on accumulated offset + local adjusted_hunk = vim.deepcopy(hunk) + if adjusted_hunk.start_old then + adjusted_hunk.start_old = hunk.start_old + offset + end + + local patched, ok = apply_hunk(adjusted_hunk, new_content) new_content = patched applied = applied or ok + + -- Update offset: (new lines added) - (old lines removed) + offset = offset + (#hunk.new_snippet - #hunk.old_snippet) end local new_lines = vim.split(new_content, '\n', { trimempty = true }) diff --git a/tests/diff_spec.lua b/tests/diff_spec.lua index 21807810..bfaa19a4 100644 --- a/tests/diff_spec.lua +++ b/tests/diff_spec.lua @@ -612,4 +612,432 @@ describe('CopilotChat.utils.diff', function() -- Fuzzy matching should handle reordered context assert.is_not_nil(result) end) + + it('adds max_retry_time and cumulative retry logic', function() + local diff_text = [[ +--- original.py ++++ modified.py +@@ -24,6 +24,7 @@ + import time + + retry_statuses = {HTTPStatus.TOO_MANY_REQUESTS, 502, 503, 504} ++ max_retry_time = 120 # Maximum cumulative retry time in seconds + retry_exceptions = ( + httpx.ReadTimeout, + httpx.ConnectTimeout, +@@ -34,6 +35,7 @@ + def deco(fn): + def wrapped(*args, **kwargs): + last_exc = None ++ total_retry_time = 0 # Track cumulative retry time + for attempt in range(retries): + try: + resp = fn(*args, **kwargs) +@@ -43,6 +45,9 @@ + delay = min(max_backoff, backoff * (2**attempt)) * ( + 1 + random.random() * 0.25 + ) ++ if total_retry_time + delay > max_retry_time: ++ raise TimeoutError("Exceeded maximum retry time of 120 seconds") ++ total_retry_time += delay + time.sleep(delay) + continue + +@@ -59,6 +64,9 @@ + delay = min(max_backoff, backoff * (2**attempt)) * ( + 1 + random.random() * 0.25 + ) ++ if total_retry_time + delay > max_retry_time: ++ raise TimeoutError("Exceeded maximum retry time of 120 seconds") ++ total_retry_time += delay + time.sleep(delay) + continue +]] + local original = [[ +import base64 +import json +import logging +import os +import random +from datetime import datetime, time +from http import HTTPStatus + +import geojson +import httpx +from cachetools import TTLCache, cached +from geopy.distance import geodesic +from shapely.geometry import MultiPolygon, Polygon, shape + +logger = logging.getLogger(__name__) + +httpx_client = httpx.Client( + timeout=10.0, + limits=httpx.Limits(max_keepalive_connections=20, max_connections=100), +) + + +def retry_request(retries=10, backoff=1, max_backoff=40.0): + import time + + retry_statuses = {HTTPStatus.TOO_MANY_REQUESTS, 502, 503, 504} + retry_exceptions = ( + httpx.ReadTimeout, + httpx.ConnectTimeout, + httpx.NetworkError, # includes transient connection errors + httpx.RemoteProtocolError, + ) + + def deco(fn): + def wrapped(*args, **kwargs): + last_exc = None + for attempt in range(retries): + try: + resp = fn(*args, **kwargs) + except retry_exceptions as exc: + last_exc = exc + # backoff and retry + delay = min(max_backoff, backoff * (2**attempt)) * ( + 1 + random.random() * 0.25 + ) + time.sleep(delay) + continue + + # Retry on selected HTTP status + if resp.status_code in retry_statuses: + # honor Retry-After if present + ra = resp.headers.get("Retry-After") + if ra: + try: + delay = min(max_backoff, float(ra)) + except ValueError: + delay = min(max_backoff, backoff * (2**attempt)) + else: + delay = min(max_backoff, backoff * (2**attempt)) * ( + 1 + random.random() * 0.25 + ) + time.sleep(delay) + continue + + return resp + + if last_exc: + raise last_exc + return resp + + return wrapped + + return deco +]] + local expected = [[ +import base64 +import json +import logging +import os +import random +from datetime import datetime, time +from http import HTTPStatus + +import geojson +import httpx +from cachetools import TTLCache, cached +from geopy.distance import geodesic +from shapely.geometry import MultiPolygon, Polygon, shape + +logger = logging.getLogger(__name__) + +httpx_client = httpx.Client( + timeout=10.0, + limits=httpx.Limits(max_keepalive_connections=20, max_connections=100), +) + + +def retry_request(retries=10, backoff=1, max_backoff=40.0): + import time + + retry_statuses = {HTTPStatus.TOO_MANY_REQUESTS, 502, 503, 504} + max_retry_time = 120 # Maximum cumulative retry time in seconds + retry_exceptions = ( + httpx.ReadTimeout, + httpx.ConnectTimeout, + httpx.NetworkError, # includes transient connection errors + httpx.RemoteProtocolError, + ) + + def deco(fn): + def wrapped(*args, **kwargs): + last_exc = None + total_retry_time = 0 # Track cumulative retry time + for attempt in range(retries): + try: + resp = fn(*args, **kwargs) + except retry_exceptions as exc: + last_exc = exc + # backoff and retry + delay = min(max_backoff, backoff * (2**attempt)) * ( + 1 + random.random() * 0.25 + ) + if total_retry_time + delay > max_retry_time: + raise TimeoutError("Exceeded maximum retry time of 120 seconds") + total_retry_time += delay + time.sleep(delay) + continue + + # Retry on selected HTTP status + if resp.status_code in retry_statuses: + # honor Retry-After if present + ra = resp.headers.get("Retry-After") + if ra: + try: + delay = min(max_backoff, float(ra)) + except ValueError: + delay = min(max_backoff, backoff * (2**attempt)) + else: + delay = min(max_backoff, backoff * (2**attempt)) * ( + 1 + random.random() * 0.25 + ) + if total_retry_time + delay > max_retry_time: + raise TimeoutError("Exceeded maximum retry time of 120 seconds") + total_retry_time += delay + time.sleep(delay) + continue + + return resp + + if last_exc: + raise last_exc + return resp + + return wrapped + + return deco +]] + local result, applied = diff.apply_unified_diff(diff_text, original) + local expected_lines = vim.split(expected, '\n', { trimempty = true }) + assert.are.same(expected_lines, result) + end) + + -- Tests for offset tracking in sequential hunk application + it('correctly applies offset when first hunk adds lines', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,2 +1,4 @@ + line1 ++added1 ++added2 + line2 +@@ -3,1 +5,1 @@ + line3 +]] + local original = { 'line1', 'line2', 'line3' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'line1', 'added1', 'added2', 'line2', 'line3' }, result) + end) + + it('correctly applies offset when first hunk removes lines', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,3 +1,1 @@ + line1 +-line2 +-line3 +@@ -4,1 +2,1 @@ + line4 +]] + local original = { 'line1', 'line2', 'line3', 'line4' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'line1', 'line4' }, result) + end) + + it('correctly tracks offset through multiple hunks with mixed add/remove', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,1 +1,2 @@ + a ++b +@@ -2,1 +3,1 @@ +-c ++C +@@ -3,1 +4,3 @@ + d ++e ++f +]] + local original = { 'a', 'c', 'd' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'a', 'b', 'C', 'd', 'e', 'f' }, result) + end) + + it('handles offset when hunks are far apart', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -2,1 +2,2 @@ + line2 ++inserted +@@ -10,1 +11,1 @@ +-line10 ++LINE10 +]] + local original = { + 'line1', + 'line2', + 'line3', + 'line4', + 'line5', + 'line6', + 'line7', + 'line8', + 'line9', + 'line10', + } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + local expected = { + 'line1', + 'line2', + 'inserted', + 'line3', + 'line4', + 'line5', + 'line6', + 'line7', + 'line8', + 'line9', + 'LINE10', + } + assert.are.same(expected, result) + end) + + it('applies three consecutive hunks with positive offset accumulation', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,1 +1,2 @@ + a ++b +@@ -2,1 +3,2 @@ + c ++d +@@ -3,1 +5,2 @@ + e ++f +]] + local original = { 'a', 'c', 'e' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'a', 'b', 'c', 'd', 'e', 'f' }, result) + end) + + it('applies three consecutive hunks with negative offset accumulation', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,2 +1,1 @@ +-x + a +@@ -3,2 +2,1 @@ +-y + b +@@ -5,2 +3,1 @@ +-z + c +]] + local original = { 'x', 'a', 'y', 'b', 'z', 'c' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'a', 'b', 'c' }, result) + end) + + it('handles zero-offset hunks (replacements without size change)', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,1 +1,1 @@ +-old1 ++new1 +@@ -2,1 +2,1 @@ +-old2 ++new2 +@@ -3,1 +3,1 @@ +-old3 ++new3 +]] + local original = { 'old1', 'old2', 'old3' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'new1', 'new2', 'new3' }, result) + end) + + it('applies offset correctly when first hunk is pure insertion (len_old=0)', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -0,0 +1,2 @@ ++inserted1 ++inserted2 +@@ -1,1 +3,1 @@ + original +]] + local original = { 'original' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'inserted1', 'inserted2', 'original' }, result) + end) + + it('handles complex offset scenario with interleaved additions and deletions', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,2 +1,1 @@ +-delete1 + keep1 +@@ -3,1 +2,3 @@ + keep2 ++add1 ++add2 +@@ -4,2 +5,1 @@ +-delete2 + keep3 +]] + local original = { 'delete1', 'keep1', 'keep2', 'delete2', 'keep3' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'keep1', 'keep2', 'add1', 'add2', 'keep3' }, result) + end) + + it('offset tracking works with hunks that have context lines', function() + local diff_text = [[ +--- a/test.txt ++++ b/test.txt +@@ -1,3 +1,4 @@ + ctx1 + line1 ++inserted + ctx2 +@@ -5,2 +6,2 @@ + ctx3 +-line2 ++LINE2 +]] + local original = { 'ctx1', 'line1', 'ctx2', 'ctx3', 'line2' } + local original_content = table.concat(original, '\n') + local result, applied = diff.apply_unified_diff(diff_text, original_content) + assert.is_true(applied) + assert.are.same({ 'ctx1', 'line1', 'inserted', 'ctx2', 'ctx3', 'LINE2' }, result) + end) end) From b3d675ee50f25a39d2fb96bd40e43e9ae274ec86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 22:38:03 +0000 Subject: [PATCH 662/728] 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 d071b89f..005bb8ef 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -613,7 +613,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻This project follows the all-contributors specification. Contributions of any kind are welcome! From b6ff587b7e203dbe282ba370929fbbc4d52f13c3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 20 Nov 2025 01:43:41 +0100 Subject: [PATCH 663/728] fix(ui): preserve block content formatting when parsing chat messages (#1495) Remove trimming of block content when parsing chat messages to ensure that formatting, such as leading and trailing whitespace, is preserved. This prevents unintended loss of formatting in code or text blocks within chat sections. --- lua/CopilotChat/ui/chat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 0d2e9bc5..45ac51a6 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -734,7 +734,7 @@ function Chat:parse() message.content = vim.trim(table.concat(message.content, '\n')) if message.section then for _, block in ipairs(message.section.blocks) do - block.content = vim.trim(table.concat(block.content, '\n')) + block.content = table.concat(block.content, '\n') end end From df5376c132382dd47e3e552612940cbf25b3580c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Nov 2025 00:44:01 +0000 Subject: [PATCH 664/728] 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 005bb8ef..134ff24c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 19 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 20 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 02c5cf3a6e030ec81795f16ab5e4f3a8861736db Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 22 Dec 2025 11:19:34 +0100 Subject: [PATCH 665/728] fix(providers): Correctly handle tool calls and responses API output (#1501) Closes #1499 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/providers.lua | 647 ++++++++++++--------------- 1 file changed, 287 insertions(+), 360 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5b4e95f9..aac7b36c 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -196,34 +196,295 @@ local function get_github_models_token(tag) return github_device_flow(tag, '178c6fc778ccc68e1d6a', 'read:user copilot') end ---- Helper function to extract text content from Responses API output parts ----@param parts table Array of content parts from Responses API +--- Prepare input for Responses API +---@param inputs table +---@param opts CopilotChat.config.providers.Options +---@return table +local function prepare_responses_input(inputs, opts) + local instructions = nil + local input_messages = {} + + for _, msg in ipairs(inputs) do + if msg.role == constants.ROLE.SYSTEM then + instructions = instructions and (instructions .. '\n\n' .. msg.content) or msg.content + elseif msg.role == constants.ROLE.TOOL then + table.insert(input_messages, { + type = 'function_call_output', + call_id = msg.tool_call_id, + output = msg.content, + }) + else + table.insert(input_messages, { + role = msg.role, + content = msg.content, + }) + + if msg.tool_calls then + for _, tool_call in ipairs(msg.tool_calls) do + table.insert(input_messages, { + type = 'function_call', + call_id = tool_call.id, + name = tool_call.name, + arguments = tool_call.arguments or '', + }) + end + end + end + end + + local out = { + model = opts.model.id, + stream = opts.model.streaming ~= false, + input = input_messages, + } + + if instructions then + out.instructions = instructions + end + + if opts.tools and opts.model.tools then + out.tools = vim.tbl_map(function(tool) + return { + type = 'function', + name = tool.name, + description = tool.description, + parameters = tool.schema, + } + end, opts.tools) + end + + return out +end + +--- Prepare input for Chat Completions API +---@param inputs table +---@param opts CopilotChat.config.providers.Options +---@return table +local function prepare_chat_input(inputs, opts) + local is_o1 = vim.startswith(opts.model.id, 'o1') + + inputs = vim.tbl_map(function(input) + local output = { + role = (is_o1 and input.role == constants.ROLE.SYSTEM) and constants.ROLE.USER or input.role, + content = input.content, + } + + if input.tool_call_id then + output.tool_call_id = input.tool_call_id + end + + if input.tool_calls then + output.tool_calls = vim.tbl_map(function(tool_call) + return { + id = tool_call.id, + type = 'function', + ['function'] = { + name = tool_call.name, + arguments = tool_call.arguments or nil, + }, + } + end, input.tool_calls) + end + + return output + end, inputs) + + local out = { + messages = inputs, + model = opts.model.id, + stream = opts.model.streaming or false, + } + + if opts.tools and opts.model.tools then + out.tools = vim.tbl_map(function(tool) + return { + type = 'function', + ['function'] = { + name = tool.name, + description = tool.description, + parameters = tool.schema, + }, + } + end, opts.tools) + end + + if not is_o1 then + out.n = 1 + out.top_p = 1 + out.temperature = opts.temperature + end + + if opts.model.max_output_tokens then + out.max_tokens = opts.model.max_output_tokens + end + + return out +end +---@param parts table Array of content parts ---@return string The concatenated text content local function extract_text_from_parts(parts) - local content = '' if not parts or type(parts) ~= 'table' then - return content + return '' end + local content = '' for _, part in ipairs(parts) do - if type(part) == 'table' then - -- Handle different content types from Responses API - if part.type == 'output_text' or part.type == 'text' then + if type(part) == 'string' then + content = content .. part + elseif type(part) == 'table' then + -- Responses API: parts have type field + if part.type == 'text' or part.type == 'output_text' or part.type == 'input_text' then content = content .. (part.text or '') - elseif part.output_text then - -- Handle nested output_text - if type(part.output_text) == 'string' then - content = content .. part.output_text - elseif type(part.output_text) == 'table' and part.output_text.text then - content = content .. part.output_text.text + -- Fallback for simpler structures + elseif part.text then + content = content .. part.text + end + end + end + return content +end + +--- Parse Responses API output (both streaming and non-streaming) +---@param output table Raw API response +---@return CopilotChat.config.providers.Output +local function prepare_responses_output(output) + local content = '' + local reasoning = '' + local finish_reason = nil + local total_tokens = nil + local tool_calls = {} + + -- Handle errors + local error_msg = output.error or (output.response and output.response.error) + if error_msg then + if type(error_msg) == 'table' then + error_msg = error_msg.message or vim.inspect(error_msg) + end + return { + content = '', + reasoning = '', + finish_reason = 'error: ' .. tostring(error_msg), + total_tokens = nil, + tool_calls = {}, + } + end + + -- Handle streaming events + if output.type then + if output.type == 'response.output_text.delta' then + -- Streaming text delta + if output.delta and type(output.delta) == 'string' then + content = output.delta + elseif output.delta and output.delta.text then + content = output.delta.text + end + elseif output.type == 'response.output_item.done' then + -- Complete output item (including tool calls) + local item = output.item + if item and item.type == 'function_call' then + table.insert(tool_calls, { + id = item.call_id or ('tooluse_' .. (#tool_calls + 1)), + index = #tool_calls + 1, + name = item.name or '', + arguments = item.arguments or '', + }) + end + elseif output.type == 'response.completed' or output.type == 'response.done' then + local response = output.response + if response then + if response.reasoning and response.reasoning.summary then + reasoning = response.reasoning.summary + end + if response.usage then + total_tokens = response.usage.total_tokens + end + finish_reason = 'stop' + end + elseif output.type == 'response.failed' then + finish_reason = 'error: ' .. (output.error and output.error.message or 'unknown error') + end + -- Handle non-streaming response + elseif output.response then + local response = output.response + if response.output and #response.output > 0 then + for _, msg in ipairs(response.output) do + if msg.content then + content = content .. extract_text_from_parts(msg.content) + end + if msg.tool_calls then + for i, tool_call in ipairs(msg.tool_calls) do + table.insert(tool_calls, { + id = tool_call.call_id or ('tooluse_' .. i), + index = i, + name = tool_call.name or '', + arguments = tool_call.arguments or '', + }) + end end end - elseif type(part) == 'string' then - content = content .. part end + if response.reasoning and response.reasoning.summary then + reasoning = response.reasoning.summary + end + if response.usage then + total_tokens = response.usage.total_tokens + end + finish_reason = response.status == 'completed' and 'stop' or nil end - return content + return { + content = content, + reasoning = reasoning, + finish_reason = finish_reason, + total_tokens = total_tokens, + tool_calls = tool_calls, + } +end + +--- Parse Chat Completions API output (both streaming and non-streaming) +---@param output table Raw API response +---@return CopilotChat.config.providers.Output +local function prepare_chat_output(output) + local tool_calls = {} + + local choice + if output.choices and #output.choices > 0 then + for _, c in ipairs(output.choices) do + local message = c.message or c.delta + if message and message.tool_calls then + for i, tool_call in ipairs(message.tool_calls) do + local fn = tool_call['function'] + if fn then + local index = tool_call.index or i + local id = utils.empty(tool_call.id) and ('tooluse_' .. index) or tool_call.id + table.insert(tool_calls, { + id = id, + index = index, + name = fn.name, + arguments = fn.arguments or '', + }) + end + end + end + end + choice = output.choices[1] + else + choice = output + end + + local message = choice.message or choice.delta + local content = message and message.content + local reasoning = message and (message.reasoning or message.reasoning_content) + local usage = choice.usage and choice.usage.total_tokens or output.usage and output.usage.total_tokens + local finish_reason = choice.finish_reason or choice.done_reason or output.finish_reason or output.done_reason + + return { + content = content, + reasoning = reasoning, + finish_reason = finish_reason, + total_tokens = usage, + tool_calls = tool_calls, + } end ---@class CopilotChat.config.providers.Options @@ -380,355 +641,23 @@ M.copilot = { end, prepare_input = function(inputs, opts) - local is_o1 = vim.startswith(opts.model.id, 'o1') - - -- Check if this model uses the Responses API if opts.model.use_responses then - -- Prepare input for Responses API - local instructions = nil - local input_messages = {} - - for _, msg in ipairs(inputs) do - if msg.role == constants.ROLE.SYSTEM then - -- Combine system messages as instructions - if instructions then - instructions = instructions .. '\n\n' .. msg.content - else - instructions = msg.content - end - else - -- Include the message in the input array - table.insert(input_messages, { - role = msg.role, - content = msg.content, - }) - end - end - - -- The Responses API expects the input field to be an array of message objects - local out = { - model = opts.model.id, - -- Always request streaming for Responses API (honor model.streaming or default to true) - stream = opts.model.streaming ~= false, - input = input_messages, - } - - -- Add instructions if we have any system messages - if instructions then - out.instructions = instructions - end - - -- Add tools for Responses API if available - if opts.tools and opts.model.tools then - out.tools = vim.tbl_map(function(tool) - return { - type = 'function', - ['function'] = { - name = tool.name, - description = tool.description, - parameters = tool.schema, - strict = true, - }, - } - end, opts.tools) - end - - -- Note: temperature is not supported by Responses API, so we don't include it - - return out - end - - -- Original Chat Completion API logic - inputs = vim.tbl_map(function(input) - local output = { - role = input.role, - content = input.content, - } - - if is_o1 then - if input.role == constants.ROLE.SYSTEM then - output.role = constants.ROLE.USER - end - end - - if input.tool_call_id then - output.tool_call_id = input.tool_call_id - end - - if input.tool_calls then - output.tool_calls = vim.tbl_map(function(tool_call) - return { - id = tool_call.id, - type = 'function', - ['function'] = { - name = tool_call.name, - arguments = tool_call.arguments or nil, - }, - } - end, input.tool_calls) - end - - return output - end, inputs) - - local out = { - messages = inputs, - model = opts.model.id, - stream = opts.model.streaming or false, - } - - if opts.tools and opts.model.tools then - out.tools = vim.tbl_map(function(tool) - return { - type = 'function', - ['function'] = { - name = tool.name, - description = tool.description, - parameters = tool.schema, - }, - } - end, opts.tools) - end - - if not is_o1 then - out.n = 1 - out.top_p = 1 - out.temperature = opts.temperature + return prepare_responses_input(inputs, opts) end - - if opts.model.max_output_tokens then - out.max_tokens = opts.model.max_output_tokens - end - - return out + return prepare_chat_input(inputs, opts) end, prepare_output = function(output, opts) - -- Check if this model uses the Responses API if opts and opts.model and opts.model.use_responses then - -- Handle Responses API output format - local content = '' - local reasoning = '' - local finish_reason = nil - local total_tokens = 0 - local tool_calls = {} - - -- Check for error in response - if output.error then - -- Surface the error as a finish reason to stop processing - local error_msg = output.error - if type(error_msg) == 'table' then - error_msg = error_msg.message or vim.inspect(error_msg) - end - return { - content = '', - reasoning = '', - finish_reason = 'error: ' .. tostring(error_msg), - total_tokens = nil, - tool_calls = {}, - } - end - - if output.type then - -- This is a streaming response from Responses API - if output.type == 'response.created' or output.type == 'response.in_progress' then - -- In-progress events, we don't have content yet - return { - content = '', - reasoning = '', - finish_reason = nil, - total_tokens = nil, - tool_calls = {}, - } - elseif output.type == 'response.completed' then - -- Completed response: do NOT resend content here to avoid duplication. - -- Only signal finish and capture usage/reasoning. - local response = output.response - if response then - if response.reasoning and response.reasoning.summary then - reasoning = response.reasoning.summary - end - if response.usage then - total_tokens = response.usage.total_tokens - end - finish_reason = 'stop' - end - return { - content = '', - reasoning = reasoning, - finish_reason = finish_reason, - total_tokens = total_tokens, - tool_calls = {}, - } - elseif output.type == 'response.content.delta' or output.type == 'response.output_text.delta' then - -- Streaming content delta - if output.delta then - if type(output.delta) == 'string' then - content = output.delta - elseif type(output.delta) == 'table' then - if output.delta.content then - content = output.delta.content - elseif output.delta.output_text then - content = extract_text_from_parts({ output.delta.output_text }) - elseif output.delta.text then - content = output.delta.text - end - end - end - elseif output.type == 'response.delta' then - -- Handle response.delta with nested output_text - if output.delta and output.delta.output_text then - content = extract_text_from_parts({ output.delta.output_text }) - end - elseif output.type == 'response.content.done' or output.type == 'response.output_text.done' then - -- Terminal content event; keep streaming open until response.completed provides usage info - finish_reason = nil - elseif output.type == 'response.error' then - -- Handle error event - local error_msg = output.error - if type(error_msg) == 'table' then - error_msg = error_msg.message or vim.inspect(error_msg) - end - finish_reason = 'error: ' .. tostring(error_msg) - elseif output.type == 'response.tool_call.delta' then - -- Handle tool call delta events - if output.delta and output.delta.tool_calls then - for _, tool_call in ipairs(output.delta.tool_calls) do - local id = tool_call.id or ('tooluse_' .. (tool_call.index or 1)) - local existing_call = nil - for _, tc in ipairs(tool_calls) do - if tc.id == id then - existing_call = tc - break - end - end - if not existing_call then - table.insert(tool_calls, { - id = id, - index = tool_call.index or #tool_calls + 1, - name = tool_call.name or '', - arguments = tool_call.arguments or '', - }) - else - -- Append arguments - existing_call.arguments = existing_call.arguments .. (tool_call.arguments or '') - end - end - end - end - elseif output.response then - -- Non-streaming response or final response - local response = output.response - - -- Check for error in the response object - if response.error then - local error_msg = response.error - if type(error_msg) == 'table' then - error_msg = error_msg.message or vim.inspect(error_msg) - end - return { - content = '', - reasoning = '', - finish_reason = 'error: ' .. tostring(error_msg), - total_tokens = nil, - tool_calls = {}, - } - end - - if response.output and #response.output > 0 then - for _, msg in ipairs(response.output) do - if msg.content and #msg.content > 0 then - content = content .. extract_text_from_parts(msg.content) - end - -- Extract tool calls from output messages - if msg.tool_calls then - for i, tool_call in ipairs(msg.tool_calls) do - local id = tool_call.id or ('tooluse_' .. i) - table.insert(tool_calls, { - id = id, - index = tool_call.index or i, - name = tool_call.name or '', - arguments = tool_call.arguments or '', - }) - end - end - end - end - - if response.reasoning and response.reasoning.summary then - reasoning = response.reasoning.summary - end - - if response.usage then - total_tokens = response.usage.total_tokens - end - - finish_reason = response.status == 'completed' and 'stop' or nil - end - - return { - content = content, - reasoning = reasoning, - finish_reason = finish_reason, - total_tokens = total_tokens, - tool_calls = tool_calls, - } + return prepare_responses_output(output) end - - -- Original Chat Completion API logic - local tool_calls = {} - - local choice - if output.choices and #output.choices > 0 then - for _, choice in ipairs(output.choices) do - local message = choice.message or choice.delta - if message and message.tool_calls then - for i, tool_call in ipairs(message.tool_calls) do - local fn = tool_call['function'] - if fn then - local index = tool_call.index or i - local id = utils.empty(tool_call.id) and ('tooluse_' .. index) or tool_call.id - table.insert(tool_calls, { - id = id, - index = index, - name = fn.name, - arguments = fn.arguments or '', - }) - end - end - end - end - - choice = output.choices[1] - else - choice = output - end - - local message = choice.message or choice.delta - local content = message and message.content - local reasoning = message and (message.reasoning or message.reasoning_content) - local usage = choice.usage and choice.usage.total_tokens - if not usage then - usage = output.usage and output.usage.total_tokens - end - local finish_reason = choice.finish_reason or choice.done_reason or output.finish_reason or output.done_reason - - return { - content = content, - reasoning = reasoning, - finish_reason = finish_reason, - total_tokens = usage, - tool_calls = tool_calls, - } + return prepare_chat_output(output) end, get_url = function(opts) - -- Check if this model uses the Responses API if opts and opts.model and opts.model.use_responses then return 'https://api.githubcopilot.com/responses' end - - -- Default to Chat Completion API return 'https://api.githubcopilot.com/chat/completions' end, } @@ -755,17 +684,15 @@ M.github_models = { return vim .iter(response.body) :map(function(model) - local max_output_tokens = model.limits.max_output_tokens - local max_input_tokens = model.limits.max_input_tokens return { id = model.id, name = model.name, - tokenizer = 'o200k_base', - max_input_tokens = max_input_tokens, - max_output_tokens = max_output_tokens, - streaming = vim.tbl_contains(model.capabilities, 'streaming'), - tools = vim.tbl_contains(model.capabilities, 'tool-calling'), - reasoning = vim.tbl_contains(model.capabilities, 'reasoning'), + tokenizer = 'o200k_base', -- GitHub Models doesn't expose tokenizer info + max_input_tokens = model.limits and model.limits.max_input_tokens, + max_output_tokens = model.limits and model.limits.max_output_tokens, + streaming = model.capabilities and vim.tbl_contains(model.capabilities, 'streaming') or false, + tools = model.capabilities and vim.tbl_contains(model.capabilities, 'tool-calling') or false, + reasoning = model.capabilities and vim.tbl_contains(model.capabilities, 'reasoning') or false, version = model.version, } end) From 850c969a500857b895f263acc675bb93cdf9589c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Dec 2025 10:19:50 +0000 Subject: [PATCH 666/728] 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 134ff24c..5f1cd3a7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 November 20 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 December 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From e243fc74f3e81c0a744f34d91b958a9a7cbde5ea Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 22 Dec 2025 11:20:21 +0100 Subject: [PATCH 667/728] docs: remove chat.response() from README (#1502) It was removed. Closes #1497 Signed-off-by: Tomas Slusny --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6e923106..209034a1 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,6 @@ local chat = require("CopilotChat") -- Basic Chat Functions chat.ask(prompt, config) -- Ask a question with optional config -chat.response() -- Get the last response text -- Window Management chat.open(config) -- Open chat window with optional config From ed94e56ee8292f5df351e17709ff4b178ca84200 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Dec 2025 10:20:38 +0000 Subject: [PATCH 668/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 5f1cd3a7..1830fb8a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -471,7 +471,6 @@ CORE *CopilotChat-core* -- Basic Chat Functions chat.ask(prompt, config) -- Ask a question with optional config - chat.response() -- Get the last response text -- Window Management chat.open(config) -- Open chat window with optional config From d4c9ebef6e3a0df268cdf3f70e958fb7b7bb000b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 31 Dec 2025 12:03:27 +0100 Subject: [PATCH 669/728] feat: add .emmyrc.json for LuaJIT runtime and workspace config (#1505) Add .emmyrc.json to configure EmmyLua with LuaJIT runtime and set require patterns for Lua files. Also includes $VIMRUNTIME in the workspace library for better Neovim integration. --- .emmyrc.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .emmyrc.json diff --git a/.emmyrc.json b/.emmyrc.json new file mode 100644 index 00000000..5f9691d9 --- /dev/null +++ b/.emmyrc.json @@ -0,0 +1,9 @@ +{ + "runtime": { + "version": "LuaJIT", + "requirePattern": ["lua/?.lua", "lua/?/init.lua"] + }, + "workspace": { + "library": ["$VIMRUNTIME"] + } +} From 9db5d3eaafe9fc3c91ce9ecfc416de2798982487 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 Dec 2025 11:03:46 +0000 Subject: [PATCH 670/728] 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 1830fb8a..f47ea878 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 December 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 December 31 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 4fac5689ef07511d99be85bc0d10d12937ebc375 Mon Sep 17 00:00:00 2001 From: Antonio Cheong Date: Fri, 9 Jan 2026 16:06:55 +0000 Subject: [PATCH 671/728] Remove myself from funding & add Tomas instead I have not been involved in a very long time and it feels wrong to have a link to my profile in the sidebar. This plugin is now pretty much maintained by @deathbeam so it probably fits better. --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ce9dcccd..4b3b3fbc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -github: [acheong08, jellydn] +github: [deathbeam, jellydn] From 21bdecb25aa72119d11d7fc08c7e0ce323f1b540 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jan 2026 16:13:44 +0000 Subject: [PATCH 672/728] 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 f47ea878..132166c0 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 December 31 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 January 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 90ebb5072a380c705d8c33427c8685754ef83807 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 18 Jan 2026 16:53:12 +0100 Subject: [PATCH 673/728] feat(config): support multiple custom instruction files (#1510) Add `instruction_files` option to configuration, allowing users to specify multiple custom instruction files to be loaded from the current working directory. The prompt resolution logic now iterates over all configured instruction files and includes their contents if present. This makes it easier to manage and extend custom Copilot instructions across different projects. --- lua/CopilotChat/config.lua | 7 +++++++ lua/CopilotChat/prompts.lua | 24 +++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b78ddf61..83d82dc2 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -43,6 +43,7 @@ ---@field log_level 'trace'|'debug'|'info'|'warn'|'error'|'fatal'? ---@field proxy string? ---@field allow_insecure boolean? +---@field instruction_files table? ---@field selection 'visual'|'unnamed'|nil ---@field chat_autocomplete boolean? ---@field log_path string? @@ -105,6 +106,12 @@ return { proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + -- Instruction files to look for in current working directory + instruction_files = { + '.github/copilot-instructions.md', + 'AGENTS.MD', + }, + selection = 'visual', -- Selection source chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 21eb6cd4..e8e9ed28 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -12,16 +12,22 @@ local WORD_WITH_INPUT_UNQUOTED = WORD .. ':?([^%s`]*)' --- Find custom instructions in the current working directory. ---@param cwd string +---@param config CopilotChat.config.Config ---@return table -local function find_custom_instructions(cwd) +local function find_custom_instructions(cwd, config) local out = {} - local copilot_instructions_path = vim.fs.joinpath(cwd, '.github', 'copilot-instructions.md') - local copilot_instructions = files.read_file(copilot_instructions_path) - if copilot_instructions then - table.insert(out, { - filename = copilot_instructions_path, - content = vim.trim(copilot_instructions), - }) + local files_to_check = {} + for _, relpath in ipairs(config.instruction_files or {}) do + table.insert(files_to_check, vim.fs.joinpath(cwd, relpath)) + end + for _, path in ipairs(files_to_check) do + local content = files.read_file(path) + if content then + table.insert(out, { + filename = path, + content = vim.trim(content), + }) + end end return out end @@ -314,7 +320,7 @@ function M.resolve_prompt(prompt, config) end local custom_instructions = vim.trim(require('CopilotChat.instructions.custom_instructions')) - for _, instruction in ipairs(find_custom_instructions(source.cwd())) do + for _, instruction in ipairs(find_custom_instructions(source.cwd(), config)) do config.system_prompt = vim.trim(config.system_prompt) .. '\n' .. custom_instructions:gsub('{FILENAME}', instruction.filename):gsub('{CONTENT}', instruction.content) From 068e2570ff94bf734d0333eaf567a41dad061f43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 15:53:29 +0000 Subject: [PATCH 674/728] 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 132166c0..79b1c94f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 January 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 January 18 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 07dcc188bc488b2dafa9324bd42088640bee3d19 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 18 Jan 2026 18:55:34 +0100 Subject: [PATCH 675/728] fix(config): correct AGENTS.md filename casing (#1511) The instruction file 'AGENTS.MD' was renamed to 'AGENTS.md' to match the actual file name and ensure it is properly detected in the working directory. This resolves issues on case-sensitive file systems. --- lua/CopilotChat/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 83d82dc2..1e0adda3 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -109,7 +109,7 @@ return { -- Instruction files to look for in current working directory instruction_files = { '.github/copilot-instructions.md', - 'AGENTS.MD', + 'AGENTS.md', }, selection = 'visual', -- Selection source From 1225fe849d17d4a7893b12a400c608a2554149f4 Mon Sep 17 00:00:00 2001 From: Xinyu Xiang <149765160+pxwg@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:54:33 +0800 Subject: [PATCH 676/728] feat(copilot): optimize copilot quota usage for tool calling * update: new function in copilot provider for agent initiator * refactor: remove initiator into prepare_input function --- lua/CopilotChat/client.lua | 9 ++++++++- lua/CopilotChat/config/providers.lua | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 93e1c91d..19ceca73 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -511,7 +511,14 @@ function Client:ask(opts) end local headers = self:authenticate(provider_name) - local request = provider.prepare_input(generate_ask_request(opts.system_prompt, history, generated_messages), options) + + local request, extra_headers = + provider.prepare_input(generate_ask_request(opts.system_prompt, history, generated_messages), options) + + if extra_headers then + headers = vim.tbl_extend('force', headers, extra_headers) + end + local is_stream = request.stream local args = { diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index aac7b36c..896d782d 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -641,10 +641,21 @@ M.copilot = { end, prepare_input = function(inputs, opts) + local request if opts.model.use_responses then - return prepare_responses_input(inputs, opts) + request = prepare_responses_input(inputs, opts) + else + request = prepare_chat_input(inputs, opts) end - return prepare_chat_input(inputs, opts) + + if inputs and #inputs > 0 then + local last_msg = inputs[#inputs] + if last_msg.role == constants.ROLE.TOOL then + return request, { ['x-initiator'] = 'agent' } + end + end + + return request end, prepare_output = function(output, opts) From 8e96dd3b04413e331afeca4ae0d65ac2d7960363 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Feb 2026 11:54:51 +0000 Subject: [PATCH 677/728] 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 79b1c94f..3464df4a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 January 18 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 February 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 6933b82e0b6dac53274cd8bff8fcd986a2070d77 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:56:55 +0100 Subject: [PATCH 678/728] docs: add pxwg as a contributor for code (#1521) * 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 c4ad3996..d7be9b1f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -487,6 +487,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/3404755?v=4", "profile": "https://github.com/kharandziuk", "contributions": ["code"] + }, + { + "login": "pxwg", + "name": "Xinyu Xiang", + "avatar_url": "https://avatars.githubusercontent.com/u/149765160?v=4", + "profile": "https://github.com/pxwg", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 209034a1..48f26e73 100644 --- a/README.md +++ b/README.md @@ -619,6 +619,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Tobias Wölfel
Tobias Wölfel

💻 Alexander Garcia
Alexander Garcia

💻 Max Kharandziuk
Max Kharandziuk

💻 + Xinyu Xiang
Xinyu Xiang

💻 From 5c5e6c2f0b81c1ae69f0d80ede010839993019e8 Mon Sep 17 00:00:00 2001 From: junqizhang Date: Mon, 2 Feb 2026 11:02:29 -0500 Subject: [PATCH 679/728] fix: support file paths with spaces in block headers (#1504) The block header pattern used %S+ (non-whitespace characters) to match file paths, which failed when paths contained spaces. This prevented code blocks from being parsed and made diff acceptance () non-functional for files in directories with spaces. Changes: - Add new pattern with lazy match (.-) to support paths with spaces - Keep original pattern as fallback for compatibility - Pattern now correctly parses headers like: python path=/path/with spaces/file.py start_line=1 end_line=10 Fixes the issue where blocks count remained 0 even when Treesitter correctly identified block_header and block_content nodes. Tested with paths containing spaces and verified that: - Blocks are now correctly parsed and added to message.section.blocks - Diff acceptance mappings (accept_diff, show_diff, etc.) now work - Backward compatibility maintained for paths without spaces --- lua/CopilotChat/ui/chat.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 45ac51a6..db1abd27 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -39,6 +39,7 @@ local function match_block_header(header) end local patterns = { + '^(%w+)%s+path=(.-)%s+start_line=(%d+)%s+end_line=(%d+)$', '^(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', '^(%w+)$', } From 967dc0a7bd424ddfe7bf9ac1ecb5d3ac1bc9a83b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Feb 2026 16:02:52 +0000 Subject: [PATCH 680/728] 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 3464df4a..158c58b6 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -612,7 +612,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 99a11909a690d756ad864a0f496769d023f2f78b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:04:42 +0100 Subject: [PATCH 681/728] docs: add junqizhang as a contributor for code (#1523) * docs: update .all-contributorsrc [skip ci] * docs: update README.md [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, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index d7be9b1f..7711217f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -494,6 +494,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/149765160?v=4", "profile": "https://github.com/pxwg", "contributions": ["code"] + }, + { + "login": "junqizhang", + "name": "junqizhang", + "avatar_url": "https://avatars.githubusercontent.com/u/22600124?v=4", + "profile": "https://github.com/junqizhang", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 48f26e73..f99de93b 100644 --- a/README.md +++ b/README.md @@ -621,6 +621,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Max Kharandziuk
Max Kharandziuk

💻 Xinyu Xiang
Xinyu Xiang

💻 + + junqizhang
junqizhang

💻 + From 4d5dc1c3a68841c7cba088db351f38954f9a11d0 Mon Sep 17 00:00:00 2001 From: Xinyu Xiang <149765160+pxwg@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:23:06 +0800 Subject: [PATCH 682/728] feat(copilot): Support "Auto" model mode (Smart Model Selection) similar to VS Code (#1518) * update: add auto into model completions * update: add auto model selection module * fix: remove unnecessary notifications * ui: show current model at header * refactor: remove function to provider configs * fix: rename duplicate function name * fix: remove route_model provider to a better position * fix: add the 'auto' model option as an model in copilot provider * refactor: move model selector to specific provider * cleanup some stuff Signed-off-by: Tomas Slusny --------- Signed-off-by: Tomas Slusny Co-authored-by: Tomas Slusny --- lua/CopilotChat/client.lua | 16 ++++++++++ lua/CopilotChat/config/providers.lua | 47 ++++++++++++++++++++++++++-- lua/CopilotChat/ui/chat.lua | 3 ++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 19ceca73..98bbf388 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -319,6 +319,16 @@ function Client:ask(opts) error('Provider not found: ' .. provider_name) end + if provider.resolve_model then + local headers = self:authenticate(provider_name) + local resolved_model = provider.resolve_model(headers, opts.model) + opts.model = resolved_model + model_config = models[opts.model] + if not model_config then + error('Resolved model not found: ' .. opts.model) + end + end + local options = { model = vim.tbl_extend('force', model_config, { id = opts.model:gsub(':' .. provider_name .. '$', ''), @@ -389,6 +399,7 @@ function Client:ask(opts) local errored = nil local finished = false local token_count = 0 + local out_model = nil local response_content_buffer = stringbuffer() local response_reasoning_buffer = stringbuffer() @@ -451,6 +462,10 @@ function Client:ask(opts) response_reasoning_buffer:put(out.reasoning) end + if out.model then + out_model = out.model + end + if opts.on_progress then opts.on_progress({ role = constants.ROLE.ASSISTANT, @@ -589,6 +604,7 @@ function Client:ask(opts) content = response_text, reasoning = response_reasoning, tool_calls = #tool_calls:values() > 0 and tool_calls:values() or nil, + model = out_model, }, token_count = token_count, token_max_count = max_tokens, diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 896d782d..3df37667 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -197,7 +197,7 @@ local function get_github_models_token(tag) end --- Prepare input for Responses API ----@param inputs table +---@param inputs CopilotChat.client.Message[] ---@param opts CopilotChat.config.providers.Options ---@return table local function prepare_responses_input(inputs, opts) @@ -257,7 +257,7 @@ local function prepare_responses_input(inputs, opts) end --- Prepare input for Chat Completions API ----@param inputs table +---@param inputs CopilotChat.client.Message[] ---@param opts CopilotChat.config.providers.Options ---@return table local function prepare_chat_input(inputs, opts) @@ -353,6 +353,7 @@ local function prepare_responses_output(output) local finish_reason = nil local total_tokens = nil local tool_calls = {} + local model = nil -- Handle errors local error_msg = output.error or (output.response and output.response.error) @@ -366,6 +367,7 @@ local function prepare_responses_output(output) finish_reason = 'error: ' .. tostring(error_msg), total_tokens = nil, tool_calls = {}, + model = nil, } end @@ -398,6 +400,9 @@ local function prepare_responses_output(output) if response.usage then total_tokens = response.usage.total_tokens end + if response.model then + model = response.model + end finish_reason = 'stop' end elseif output.type == 'response.failed' then @@ -429,6 +434,9 @@ local function prepare_responses_output(output) if response.usage then total_tokens = response.usage.total_tokens end + if response.model then + model = response.model + end finish_reason = response.status == 'completed' and 'stop' or nil end @@ -438,6 +446,7 @@ local function prepare_responses_output(output) finish_reason = finish_reason, total_tokens = total_tokens, tool_calls = tool_calls, + model = model, } end @@ -477,6 +486,7 @@ local function prepare_chat_output(output) local reasoning = message and (message.reasoning or message.reasoning_content) local usage = choice.usage and choice.usage.total_tokens or output.usage and output.usage.total_tokens local finish_reason = choice.finish_reason or choice.done_reason or output.finish_reason or output.done_reason + local model = choice.model or output.model return { content = content, @@ -484,6 +494,7 @@ local function prepare_chat_output(output) finish_reason = finish_reason, total_tokens = usage, tool_calls = tool_calls, + model = model, } end @@ -498,13 +509,15 @@ end ---@field finish_reason string? ---@field total_tokens number? ---@field tool_calls table +---@field model string? ---@class CopilotChat.config.providers.Provider ---@field disabled nil|boolean ---@field get_headers nil|fun():table,number? ---@field get_info nil|fun(headers:table):string[] ---@field get_models nil|fun(headers:table):table ----@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table +---@field resolve_model nil|fun(headers:table, model: string):string +---@field prepare_input nil|fun(inputs:CopilotChat.client.Message[], opts:CopilotChat.config.providers.Options):table,table? ---@field prepare_output nil|fun(output:table, opts:CopilotChat.config.providers.Options):CopilotChat.config.providers.Output ---@field get_url nil|fun(opts:CopilotChat.config.providers.Options):string @@ -529,6 +542,7 @@ M.copilot = { ['Editor-Version'] = EDITOR_VERSION, ['Editor-Plugin-Version'] = 'CopilotChat.nvim/*', ['Copilot-Integration-Id'] = 'vscode-chat', + ['x-github-api-version'] = '2025-10-01', }, response.body.expires_at end, @@ -637,9 +651,36 @@ M.copilot = { end end + -- Auto model selector + table.insert(models, { + id = 'auto', + name = 'Auto (Copilot)', + description = 'Auto selects the best model for your request.', + }) + return models end, + resolve_model = function(headers, model) + if model ~= 'auto' then + return model + end + + local url = 'https://api.githubcopilot.com/models/session' + local response, err = curl.post(url, { + headers = headers, + body = { auto_mode = { model_hints = { 'auto' } } }, + json_response = true, + json_request = true, + }) + + if err then + error(err) + end + + return response.body.selected_model + end, + prepare_input = function(inputs, opts) local request if opts.model.use_responses then diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index db1abd27..04fa5095 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -771,6 +771,9 @@ function Chat:render() -- Overlay section header with nice display local header_value = self.headers[message.role] local header_line = message.section.start_line - 2 + if message.model then + header_value = header_value .. ' (' .. message.model .. ')' + end vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, header_line, 0, { conceal = '', From 69199d46b56f67a226789da256264c6291c4e63d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Feb 2026 23:15:33 +0000 Subject: [PATCH 683/728] 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 158c58b6..2bf50eda 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -612,7 +612,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻This project follows the all-contributors specification. Contributions of any kind are welcome! From b7313ea5be03405cb988f69491becc8c8b5d98ac Mon Sep 17 00:00:00 2001 From: Calum Lynch <89159592+Tlunch@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:49:03 +0000 Subject: [PATCH 684/728] fix(prompts): avoid %20 being treated as special sequence when relacing dirname (#1525) * Wrap source.cwd in function tackles ticket #1524 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * CWD function handed directly #1524 Assumption that source.cwd function will not accept any arguments in the future as it currently doesn't allows simplification --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- lua/CopilotChat/prompts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index e8e9ed28..71ac08be 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -343,7 +343,7 @@ function M.resolve_prompt(prompt, config) config.system_prompt = config.system_prompt:gsub('{OS_NAME}', vim.uv.os_uname().sysname) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) - config.system_prompt = config.system_prompt:gsub('{DIR}', source.cwd()) + config.system_prompt = config.system_prompt:gsub('{DIR}', source.cwd) end return config, prompt From 7cac6a24a6853d459efdd3af3d5f69bc9fe49226 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Feb 2026 10:49:22 +0000 Subject: [PATCH 685/728] 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 2bf50eda..2b19f3d1 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 February 02 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 February 12 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 743d6005fb412c85309d3f3aa45f18f3a2fb2098 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:49:48 +0100 Subject: [PATCH 686/728] docs: add Tlunch as a contributor for code (#1526) * 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 7711217f..30b1babb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -501,6 +501,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/22600124?v=4", "profile": "https://github.com/junqizhang", "contributions": ["code"] + }, + { + "login": "Tlunch", + "name": "Calum Lynch", + "avatar_url": "https://avatars.githubusercontent.com/u/89159592?v=4", + "profile": "http://card.calumhub.xyz", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f99de93b..ba899180 100644 --- a/README.md +++ b/README.md @@ -623,6 +623,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d junqizhang
junqizhang

💻 + Calum Lynch
Calum Lynch

💻 From b20108ac925d17d221ec171f17a23a1ad20c4289 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:35:15 +0100 Subject: [PATCH 687/728] [pre-commit.ci] pre-commit autoupdate (#1532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.3.1 → v2.4.0](https://github.com/JohnnyMorganz/StyLua/compare/v2.3.1...v2.4.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 ac8e688c..b2eca2d3 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: v2.3.1 + rev: v2.4.0 hooks: - id: stylua-github From ecea24a9a067c6f176b8e13eb11251c739caf055 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 20:35:38 +0000 Subject: [PATCH 688/728] 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 2b19f3d1..75d92a7b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 February 12 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 March 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -612,7 +612,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻This project follows the all-contributors specification. Contributions of any kind are welcome! From c5aa16708694c6f56c89362d3dfcec8aec585783 Mon Sep 17 00:00:00 2001 From: sirjls Date: Mon, 23 Mar 2026 13:25:40 +0100 Subject: [PATCH 689/728] feat: support dynamic `*.business.githubcopilot.com` base URL resolution (#1536) Co-authored-by: ssparreb --- lua/CopilotChat/config/providers.lua | 57 ++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 3df37667..08887410 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -196,6 +196,22 @@ local function get_github_models_token(tag) return github_device_flow(tag, '178c6fc778ccc68e1d6a', 'read:user copilot') end +--- Resolve the Copilot API base URL from token endpoint response. +--- Falls back to the default api.githubcopilot.com if no business endpoint is found. +---@param token_body table The decoded JSON body from the token endpoint +---@return string base_url The base URL (no trailing slash) +local function resolve_copilot_base_url(token_body) + -- The token response may include an `endpoints` table with an `api` field + -- pointing to the correct base URL for business/enterprise accounts, + -- e.g. https://api.business.githubcopilot.com + if token_body and token_body.endpoints and token_body.endpoints.api then + local url = token_body.endpoints.api + -- Strip trailing slash if present + return url:gsub('/$', '') + end + return 'https://api.githubcopilot.com' +end + --- Prepare input for Responses API ---@param inputs CopilotChat.client.Message[] ---@param opts CopilotChat.config.providers.Options @@ -537,12 +553,19 @@ M.copilot = { error(err) end + -- Resolve the base URL from the token response so that business/enterprise + -- accounts using *.business.githubcopilot.com are handled automatically. + local base_url = resolve_copilot_base_url(response.body) + return { ['Authorization'] = 'Bearer ' .. response.body.token, ['Editor-Version'] = EDITOR_VERSION, ['Editor-Plugin-Version'] = 'CopilotChat.nvim/*', ['Copilot-Integration-Id'] = 'vscode-chat', ['x-github-api-version'] = '2025-10-01', + -- Store the resolved base URL in a custom header so that get_models, + -- resolve_model, and get_url can read it without making another request. + ['x-copilot-base-url'] = base_url, }, response.body.expires_at end, @@ -598,9 +621,16 @@ M.copilot = { end, get_models = function(headers) - local response, err = curl.get('https://api.githubcopilot.com/models', { + -- Use the resolved base URL carried in the custom header, falling back to + -- the default if it is absent (e.g. during tests or manual calls). + local base_url = headers['x-copilot-base-url'] or 'https://api.githubcopilot.com' + + -- Build request headers without our internal routing header. + local request_headers = vim.tbl_extend('force', headers, { ['x-copilot-base-url'] = nil }) + + local response, err = curl.get(base_url .. '/models', { json_response = true, - headers = headers, + headers = request_headers, }) if err then @@ -628,6 +658,9 @@ M.copilot = { policy = not model['policy'] or model['policy']['state'] == 'enabled', version = model.version, use_responses = use_responses, + -- Carry the base URL into the model so get_url and resolve_model + -- can use it without needing access to the headers again. + base_url = base_url, } end) :totable() @@ -643,8 +676,8 @@ M.copilot = { for _, model in ipairs(models) do if not model.policy then - pcall(curl.post, 'https://api.githubcopilot.com/models/' .. model.id .. '/policy', { - headers = headers, + pcall(curl.post, base_url .. '/models/' .. model.id .. '/policy', { + headers = request_headers, json_request = true, body = { state = 'enabled' }, }) @@ -656,6 +689,7 @@ M.copilot = { id = 'auto', name = 'Auto (Copilot)', description = 'Auto selects the best model for your request.', + base_url = base_url, }) return models @@ -666,9 +700,12 @@ M.copilot = { return model end - local url = 'https://api.githubcopilot.com/models/session' + local base_url = headers['x-copilot-base-url'] or 'https://api.githubcopilot.com' + local request_headers = vim.tbl_extend('force', headers, { ['x-copilot-base-url'] = nil }) + + local url = base_url .. '/models/session' local response, err = curl.post(url, { - headers = headers, + headers = request_headers, body = { auto_mode = { model_hints = { 'auto' } } }, json_response = true, json_request = true, @@ -707,10 +744,14 @@ M.copilot = { end, get_url = function(opts) + -- Use the base URL stored on the model (populated by get_models), falling + -- back to the default for backwards compatibility. + local base_url = (opts and opts.model and opts.model.base_url) or 'https://api.githubcopilot.com' + if opts and opts.model and opts.model.use_responses then - return 'https://api.githubcopilot.com/responses' + return base_url .. '/responses' end - return 'https://api.githubcopilot.com/chat/completions' + return base_url .. '/chat/completions' end, } From 7c263e2e14000781baa622a7b5ebb14a9203f32f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 12:26:01 +0000 Subject: [PATCH 690/728] 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 75d92a7b..653a5bca 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 March 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 March 23 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9fec6c3c2a4069b1f649e116c10e6fbcf1463764 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:32:24 +0100 Subject: [PATCH 691/728] docs: add sirjls as a contributor for code (#1537) * 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 30b1babb..84757b79 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -508,6 +508,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/89159592?v=4", "profile": "http://card.calumhub.xyz", "contributions": ["code"] + }, + { + "login": "sirjls", + "name": "sirjls", + "avatar_url": "https://avatars.githubusercontent.com/u/270346599?v=4", + "profile": "https://github.com/sirjls", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ba899180..9a54c75e 100644 --- a/README.md +++ b/README.md @@ -624,6 +624,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d junqizhang
junqizhang

💻 Calum Lynch
Calum Lynch

💻 + sirjls
sirjls

💻 From d4d63a2542f41ec9cc5baca4799f244554ee8cfd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 29 Mar 2026 20:21:02 +0200 Subject: [PATCH 692/728] fix(ui): prevent errors when restoring invalid window (#1539) - Add a check in Overlay:restore to ensure the window is valid before attempting to set the buffer, preventing errors if the window was closed. - Scope the close keymap in chat buffer to the buffer only, avoiding global keymap pollution. Closes #1535 --- lua/CopilotChat/ui/chat.lua | 2 +- lua/CopilotChat/ui/overlay.lua | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 04fa5095..8744c97d 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -161,7 +161,7 @@ local Chat = class(function(self, config, on_buf_create) function(bufnr) vim.keymap.set('n', config.mappings.close.normal, function() self.chat_overlay:restore(self.winnr, self.bufnr) - end) + end, { buffer = bufnr }) vim.api.nvim_create_autocmd({ 'BufHidden', 'BufDelete' }, { buffer = bufnr, diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 547394b8..36589ccd 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -116,6 +116,10 @@ function Overlay:restore(winnr, bufnr) self.on_hide(self.bufnr) end + if not vim.api.nvim_win_is_valid(winnr) then + return + end + vim.api.nvim_win_set_buf(winnr, bufnr) if self.cursor then From aeb6ebbdd9b7662114d873da299de06019fcb68a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 29 Mar 2026 18:21:26 +0000 Subject: [PATCH 693/728] 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 653a5bca..151bea3a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 March 23 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 March 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -612,7 +612,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻This project follows the all-contributors specification. Contributions of any kind are welcome! From ca9a42863b0963e14f4830a2193d297716ce5ed3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 29 Mar 2026 20:23:51 +0200 Subject: [PATCH 694/728] fix(utils): handle 1xx HTTP status codes in curl utils (#1540) Update the curl utility to treat 1xx (informational) HTTP status codes, such as 100 (Continue), as non-errors in both GET and POST requests. Previously, only 2xx (success) codes were considered valid, which could cause issues with streaming or intermediate responses. This change improves compatibility with APIs that use informational status codes. Closes #1508 --- lua/CopilotChat/utils/curl.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/utils/curl.lua b/lua/CopilotChat/utils/curl.lua index 87e9b89d..2c2cf60e 100644 --- a/lua/CopilotChat/utils/curl.lua +++ b/lua/CopilotChat/utils/curl.lua @@ -48,7 +48,10 @@ M.get = async.wrap(function(url, opts, callback) args.callback = function(response) log.debug('GET response:', response) - if response and not vim.startswith(tostring(response.status), '20') then + -- HTTP status codes: 1xx (informational), 2xx (success) + -- Status 100 (Continue) is common with streaming responses + local status_str = tostring(response.status) + if response and not vim.startswith(status_str, '1') and not vim.startswith(status_str, '20') then callback(response, response.body) return end @@ -96,7 +99,10 @@ M.post = async.wrap(function(url, opts, callback) log.debug('Failed to remove temp file:', temp_file_path, err) end end - if response and not vim.startswith(tostring(response.status), '20') then + -- HTTP status codes: 1xx (informational), 2xx (success) + -- Status 100 (Continue) is common with streaming responses + local status_str = tostring(response.status) + if response and not vim.startswith(status_str, '1') and not vim.startswith(status_str, '20') then callback(response, response.body) return end From d2d2574863529cb76b62b028cb5c3196ef5796d6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 29 Mar 2026 20:30:57 +0200 Subject: [PATCH 695/728] refactor(files): simplify file filtering and default to text (#1541) - Remove filetype-based filtering from file list, now only filters out empty entries. - Add simple binary detection in get_file by rejecting files with null bytes. - filetype() now defaults to 'text' if detection fails, letting content validation handle unreadable files. This improves robustness and performance by avoiding unnecessary filetype checks and handling binary files more gracefully. Closes #1533 --- lua/CopilotChat/resources.lua | 4 ++++ lua/CopilotChat/utils/files.lua | 26 ++++++++++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lua/CopilotChat/resources.lua b/lua/CopilotChat/resources.lua index 57e12e33..22c97c4b 100644 --- a/lua/CopilotChat/resources.lua +++ b/lua/CopilotChat/resources.lua @@ -27,6 +27,10 @@ function M.get_file(filename) if not content or content == '' then return nil end + -- Simple binary detection: reject files with null bytes + if content:find('\0') then + return nil + end data = { content = content, _modified = modified, diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua index 28184585..7da2be10 100644 --- a/lua/CopilotChat/utils/files.lua +++ b/lua/CopilotChat/utils/files.lua @@ -9,23 +9,11 @@ M.scan_args = { } local function filter_files(files, max_count) - local filetype = require('plenary.filetype') - + -- Filter out empty entries files = vim.tbl_filter(function(file) - if file == nil or file == '' then - return false - end - - local ft = filetype.detect(file, { - fs_access = false, - }) - - if ft == '' or not ft then - return false - end - - return true + return file ~= nil and file ~= '' end, files) + if max_count and max_count > 0 then files = vim.list_slice(files, 1, max_count) end @@ -268,7 +256,13 @@ function M.filetype(filename) }) if ft == '' or not ft and not vim.in_fast_event() then - return vim.filetype.match({ filename = filename }) + ft = vim.filetype.match({ filename = filename }) + end + + -- If filetype still not detected, default to 'text' + -- Let content validation handle whether it's actually readable + if not ft or ft == '' then + return 'text' end return ft From 13f727d0b8d84d0acc15dbadce7b02a322879900 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:51:27 +0200 Subject: [PATCH 696/728] [pre-commit.ci] pre-commit autoupdate (#1544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.4.0 → v2.4.1](https://github.com/JohnnyMorganz/StyLua/compare/v2.4.0...v2.4.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 b2eca2d3..ae6b39ed 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: v2.4.0 + rev: v2.4.1 hooks: - id: stylua-github From 0b3133ffbb470b1616c47170b544d0b9a3bbcf5b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Apr 2026 20:51:50 +0000 Subject: [PATCH 697/728] 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 151bea3a..61b1f9f5 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 March 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 April 06 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 734353f14cbcaeb726e3f190324135a39dce056b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 15 Apr 2026 15:37:29 +0200 Subject: [PATCH 698/728] docs: update architecture and add AGENTS.md (#1546) - Add AGENTS.md with detailed project overview, layout, and gotchas. - Revise CONTRIBUTING.md to clarify core, UI, features, and utilities. - Expand descriptions for each module and directory. - Improve structure for easier onboarding and contribution. --- AGENTS.md | 75 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 112 +++++++++++++++++++++++++++--------------------- 2 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..560393bb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,75 @@ +# AGENTS.md + +## Overview + +Neovim plugin (pure Lua) providing GitHub Copilot Chat integration. Requires Neovim 0.10.0+, curl 8.0.0+, plenary.nvim. + +## Commands + +```bash +# Run tests (headless Neovim + plenary test harness) +make test + +# Format check (what CI runs) +stylua --check . +``` + +`make test` runs `nvim --headless --clean -u ./scripts/test.lua`, which clones plenary.nvim into `.dependencies/` on first run, then executes all `tests/*_spec.lua` files via plenary's busted-style harness. + +## Project layout + +``` +plugin/CopilotChat.lua — Neovim plugin entry: commands, highlights, autocmds +lua/CopilotChat/ + init.lua — Main module: setup(), ask(), open/close/toggle, save/load + client.lua — Copilot API client (auth, streaming, tool calls) + config.lua — Default configuration schema + config/ — Sub-configs: functions, mappings, prompts, providers + constants.lua — Shared constants (roles, etc.) + completion.lua — Completion source + functions.lua — Built-in functions/tools exposed to the LLM + prompts.lua — Built-in prompt definitions + resources.lua — Resource handling + select.lua — Selection strategies (visual, buffer, diagnostics, git diff) + tiktoken.lua — Token counting via native tiktoken lib + health.lua — :checkhealth integration + notify.lua — Notification utilities + instructions/ — System prompt templates injected into LLM conversations (not agent guidance) + ui/ — Chat window, overlay, spinner + utils.lua — General utilities + utils/ — Utility modules: class, curl, diff, files, orderedmap, stringbuffer +queries/ — Treesitter queries for copilot-chat filetype +tests/ — Plenary busted-style specs (*_spec.lua) +scripts/ + test.lua — Test runner bootstrap (sets up plenary) + minimal.lua — Minimal reproduction config +doc/CopilotChat.txt — Auto-generated vimdoc (do NOT edit; generated from README by panvimdoc in CI) +``` + +## Style and formatting + +- **Lua formatter:** StyLua — 2-space indent, 120 column width, single quotes preferred, Unix line endings. Config in `.stylua.toml`. +- **Pre-commit hooks:** Prettier (markdown/json/yaml) + StyLua (Lua). CI will fail if StyLua check fails. +- **No linter** (no luacheck/selene configured). +- Type annotations use EmmyLua/LuaCATS `---@class`, `---@param`, `---@return` style. + +## Testing + +- Framework: plenary.nvim busted-style (`describe`, `it`, `before_each`, `after_each`, `assert`). +- Test files live in `tests/` and must be named `*_spec.lua`. +- CI runs tests against Neovim nightly with LuaJIT 2.1 and LuaRocks 3.12.2. +- Tests are unit-level (class, diff, utils, orderedmap, stringbuffer, functions, init). No integration tests requiring Copilot auth. + +## CI and releases + +- CI (`ci.yml`): lint (StyLua) + test (plenary) on all PRs; vimdoc generation on main only. +- Releases via release-please (`simple` type). Version tracked in `version.txt`. +- `doc/CopilotChat.txt` is auto-committed by CI — do not edit manually. +- `CHANGELOG.md` is managed by release-please — do not edit manually. + +## Key gotchas + +- The module is loaded as `require('CopilotChat')` (capital C's) — this matches the `lua/CopilotChat/` directory name. Case matters. +- `init.lua` uses lazy self-initialization via `__index` metamethod — accessing any field triggers `setup()` if not already called. +- `.dependencies/` is gitignored and auto-populated by the test runner (plenary clone). +- `build/` is gitignored and holds downloaded tiktoken native libraries. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3be55bac..9fe83edc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,67 +50,83 @@ Go to the CopilotChat.nvim in your GitHub account, select your branch, and click ![structure.drawio](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/e7517736-0152-47a3-8cb9-36a5dffcb6cc) -### Main components +### Core -- [init.lua](/lua/CopilotChat/init.lua): This file initializes Copilot Chat - plugin. It includes functions for appending to the chat window, showing help, - completing, getting selection, opening and closing the chat window, asking - questions to the Copilot model, resetting the chat window, enabling/disabling - debug, and setting up the plugin. +- [init.lua](/lua/CopilotChat/init.lua): Main module. Plugin initialization + (`setup()`), chat lifecycle (`ask()`, `open()`, `close()`, `toggle()`, + `reset()`), save/load, and sticky prompt processing. -- [config.lua](/lua/CopilotChat/config.lua): This file contains default - configuration for Copilot Chat plugin. +- [client.lua](/lua/CopilotChat/client.lua): Copilot API client. Handles + authentication, model listing, streaming requests, and tool call execution. -- [copilot.lua](/lua/CopilotChat/copilot.lua): This file contains the core - functionality of the Copilot. It includes functions for generating unique IDs, - finding configuration paths, authenticating, asking questions to the Copilot, - generating embeddings, and managing the running job. +- [config.lua](/lua/CopilotChat/config.lua): Default configuration schema. -- [chat.lua](/lua/CopilotChat/chat.lua): This file manages the chat window. It - includes functions for creating, validating, appending to, clearing, opening, - closing, and focusing on the chat window. +- [config/](/lua/CopilotChat/config/): Sub-configs for + [functions](/lua/CopilotChat/config/functions.lua), + [mappings](/lua/CopilotChat/config/mappings.lua), + [prompts](/lua/CopilotChat/config/prompts.lua), and + [providers](/lua/CopilotChat/config/providers.lua). -- [diff.lua](/lua/CopilotChat/diff.lua): This file manages the diff window. It - includes functions for creating, validating, showing, and restoring the diff - window. +- [constants.lua](/lua/CopilotChat/constants.lua): Shared constants (plugin + name, roles). -- [select.lua](/lua/CopilotChat/select.lua): This file contains functions for - selecting and processing different types of data such as visual selection, - unnamed register, whole buffer, current line, diagnostics, and git diff. +### Chat and UI -- [context.lua](/lua/CopilotChat/context.lua): This file is responsible for - building an outline for a buffer and finding items for a query. It uses spatial - distance and relatedness to rank data. +- [ui/chat.lua](/lua/CopilotChat/ui/chat.lua): Chat window management. + Creating, appending to, clearing, opening, closing, and focusing the chat + window. Handles fold expressions and section parsing. -- [actions.lua](/lua/CopilotChat/actions.lua): This file manages the actions - that can be performed. It includes functions for getting help actions, prompt - actions, and picking an action from a list of actions using `vim.ui.select`. +- [ui/overlay.lua](/lua/CopilotChat/ui/overlay.lua): Overlay buffer used for + displaying diff previews and other transient content. -- [tiktoken.lua](/lua/CopilotChat/tiktoken.lua): This file manages integration - with Tiktoken library and is used for counting tokens. It includes functions - for setting up Tiktoken, checking its availability, encoding prompts, and - counting prompts. +- [ui/spinner.lua](/lua/CopilotChat/ui/spinner.lua): Loading spinner indicator + for the chat window. -- [health.lua](/lua/CopilotChat/health.lua): This file checks the health of the - plugin by checking if commands exist, checking if Lua libraries are installed, - and checking if a Treesitter parsers are available. +### Features -- [spinner.lua](/lua/CopilotChat/spinner.lua): This file manages a spinner that - is used for indicating loading status in chat window. +- [prompts.lua](/lua/CopilotChat/prompts.lua): Prompt resolution, custom + instruction loading, system prompt building, and sticky/resource/tool + parsing from user input. -- [utils.lua](/lua/CopilotChat/utils.lua): This file contains utility functions - for creating classes, getting the log file path, checking if the current - version of Neovim is stable, and joining multiple async functions. +- [functions.lua](/lua/CopilotChat/functions.lua): Built-in functions/tools + exposed to the LLM (e.g., file editing, searching). -- [debuginfo.lua](/lua/CopilotChat/debuginfo.lua): This file is used for - creating `:CopilotChatDebugInfo` command. +- [resources.lua](/lua/CopilotChat/resources.lua): Resource handling for file + and URL content retrieval with caching. -### Integrations +- [completion.lua](/lua/CopilotChat/completion.lua): Completion source for the + chat window (`@tools`, `/prompts`, `#resources`, `$models`). -- [telescope.lua](/lua/CopilotChat/integrations/telescope.lua): This file - integrates the Telescope plugin with CopilotChat. It includes a function for - picking an action from a list of actions. +- [select.lua](/lua/CopilotChat/select.lua): Selection strategies for providing + context (visual selection, buffer, diagnostics, git diff, etc.). -- [fzflua.lua](/lua/CopilotChat/integrations/fzflua.lua): This file integrates - the fzf-lua plugin with CopilotChat. It includes a function for picking an - action from a list of actions. +- [tiktoken.lua](/lua/CopilotChat/tiktoken.lua): Token counting via native + tiktoken library. + +- [instructions/](/lua/CopilotChat/instructions/): System prompt templates + injected into LLM conversations (edit formats, tool use instructions, custom + instructions wrapper). + +### Utilities + +- [utils.lua](/lua/CopilotChat/utils.lua): General utility functions. + +- [utils/](/lua/CopilotChat/utils/): Utility modules — + [class.lua](/lua/CopilotChat/utils/class.lua) (OOP helper), + [curl.lua](/lua/CopilotChat/utils/curl.lua) (HTTP requests), + [diff.lua](/lua/CopilotChat/utils/diff.lua) (unified diff parsing and + application), + [files.lua](/lua/CopilotChat/utils/files.lua) (file I/O and filetype + detection), + [orderedmap.lua](/lua/CopilotChat/utils/orderedmap.lua) (insertion-ordered + map), + [stringbuffer.lua](/lua/CopilotChat/utils/stringbuffer.lua) (efficient string + concatenation). + +### Other + +- [health.lua](/lua/CopilotChat/health.lua): `:checkhealth` integration. + Verifies commands, libraries, and Treesitter parsers. + +- [notify.lua](/lua/CopilotChat/notify.lua): Pub/sub notification system for + status and message events. From e16829011dd39d415b1f62e572055df36ec0486e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Apr 2026 13:37:53 +0000 Subject: [PATCH 699/728] 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 61b1f9f5..39c4d170 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,5 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2026 April 06 +*CopilotChat.txt* + For NVIM v0.8.0 Last change: 2026 April 15 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -191,7 +192,7 @@ CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* - gc Show info about current chat - gh Show help message - [!WARNING] Some plugins (e.g. `copilot.vim`) may also map common keys like + [!WARNING] Some plugins (e.g. `copilot.vim`) may also map common keys like `` in insert mode. To avoid conflicts, disable Copilot’s default `` mapping with: >lua @@ -341,11 +342,11 @@ Types of copilot highlights: - `CopilotChatSelection` - Selection highlight in source buffer - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help text in chat buffer -- `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) -- `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`) -- `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) -- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) -- `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) +- `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) +- `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`) +- `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) +- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) +- `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) From 4759ecdd307d36503f30920411121b664188ef4c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 15 Apr 2026 16:06:34 +0200 Subject: [PATCH 700/728] fix(core): resolve various bugs and improve robustness (#1547) - Fix incorrect format string in RESOURCE_SHORT_FORMAT - Correct tool call key usage and merging in client - Use correct filetype to mimetype conversion in functions - Simplify and fix buffer finding/creation logic in mappings - Correct logic for loading GitHub token from gh CLI - Fix prompt selection and sticky line processing in init - Add notify.clear to setup for proper listener management - Add clear method to notify to reset listeners - Fix tool call highlighting in chat UI - Use proper BufEnter autocmd invocation in overlay - Fix diff hunk variable naming in utils.diff - Remove unnecessary quoting of grep patterns in utils.files These changes address several bugs, improve code clarity, and enhance the reliability of buffer, prompt, and notification handling. Closes #1515 Closes #1455 --- lua/CopilotChat/client.lua | 9 ++++----- lua/CopilotChat/config/functions.lua | 2 +- lua/CopilotChat/config/mappings.lua | 30 ++++++++++++---------------- lua/CopilotChat/config/providers.lua | 7 ++++--- lua/CopilotChat/init.lua | 23 ++++++++++++++------- lua/CopilotChat/notify.lua | 5 +++++ lua/CopilotChat/ui/chat.lua | 4 ++-- lua/CopilotChat/ui/overlay.lua | 4 +++- lua/CopilotChat/utils/diff.lua | 6 +++--- lua/CopilotChat/utils/files.lua | 4 ++-- 10 files changed, 53 insertions(+), 41 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 98bbf388..e2801844 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -65,7 +65,7 @@ local orderedmap = require('CopilotChat.utils.orderedmap') local stringbuffer = require('CopilotChat.utils.stringbuffer') --- Constants -local RESOURCE_SHORT_FORMAT = '# %s\n```%s start_line=% end_line=%s\n%s\n```' +local RESOURCE_SHORT_FORMAT = '# %s\n```%s start_line=%s end_line=%s\n%s\n```' local RESOURCE_LONG_FORMAT = '# %s\n```%s path=%s start_line=%s end_line=%s\n%s\n```' local CACHE_TTL = 300 -- 5 minutes @@ -445,9 +445,10 @@ function Client:ask(opts) if out.tool_calls then for _, tool_call in ipairs(out.tool_calls) do - local val = tool_calls:get(tool_call.index) + local key = tool_call.id or tool_call.index + local val = tool_calls:get(key) if not val then - tool_calls:set(tool_call.index, tool_call) + tool_calls:set(key, tool_call) else val.arguments = val.arguments .. tool_call.arguments end @@ -573,12 +574,10 @@ function Client:ask(opts) end error(error_msg) - return end if errored then error(errored) - return end local response_text = response_content_buffer:tostring() diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index f9063047..4d849bbe 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -247,7 +247,7 @@ return { { uri = 'neovim://selection', name = selection.filename, - mimetype = files.mimetype_to_filetype(selection.filetype), + mimetype = files.filetype_to_mimetype(selection.filetype), data = data, annotations = { start_line = selection.start_line, diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index fb4b6388..a72529f9 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -14,23 +14,19 @@ local function prepare_diff_buffer(filename, source) filename = vim.api.nvim_buf_get_name(source.bufnr) end + -- Try to find matching buffer first local diff_bufnr = nil - - -- If buffer is not found, try to load it - if not diff_bufnr then - -- Try to find matching buffer first - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if files.filename_same(vim.api.nvim_buf_get_name(buf), filename) then - diff_bufnr = buf - break - end + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if files.filename_same(vim.api.nvim_buf_get_name(buf), filename) then + diff_bufnr = buf + break end + end - -- If still not found, create a new buffer - if not diff_bufnr then - diff_bufnr = vim.fn.bufadd(filename) - vim.fn.bufload(diff_bufnr) - end + -- If not found, create a new buffer + if not diff_bufnr then + diff_bufnr = vim.fn.bufadd(filename) + vim.fn.bufload(diff_bufnr) end -- If source exists, update it to point to the diff buffer @@ -243,10 +239,10 @@ return { }) end end - - vim.fn.setqflist(items) - vim.cmd('copen') end + + vim.fn.setqflist(items) + vim.cmd('copen') end, }, diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 08887410..c817f6f8 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -183,7 +183,7 @@ local function get_github_models_token(tag) end -- loading token from gh cli if available - if vim.fn.executable('gh') == 0 then + if vim.fn.executable('gh') == 1 then local result = utils.system({ 'gh', 'auth', 'token', '-h', 'github.com' }) if result and result.code == 0 and result.stdout then local gh_token = vim.trim(result.stdout) @@ -400,9 +400,10 @@ local function prepare_responses_output(output) -- Complete output item (including tool calls) local item = output.item if item and item.type == 'function_call' then + local index = output.output_index or (#tool_calls + 1) table.insert(tool_calls, { - id = item.call_id or ('tooluse_' .. (#tool_calls + 1)), - index = #tool_calls + 1, + id = item.call_id or ('tooluse_' .. index), + index = index, name = item.name or '', arguments = item.arguments or '', }) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9125bb4a..ac7302bc 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -22,8 +22,12 @@ local M = setmetatable({}, { -- Lazy initialize local initialized = rawget(t, 'initialized') if not initialized then - rawset(t, 'initialized', true) - rawget(t, 'setup')() + local ok, err = pcall(rawget(t, 'setup')) + if ok then + rawset(t, 'initialized', true) + else + require('plenary.log').error('CopilotChat setup failed:', err) + end end return rawget(t, key) @@ -54,8 +58,12 @@ local function process_sticky(prompt, config) end -- Find sticky lines in new prompt to remove them + in_code_block = false for i, line in ipairs(lines) do - if vim.startswith(line, '> ') then + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then table.insert(sticky_indices, i) end end @@ -349,8 +357,8 @@ end --- Select a prompt template to use. ---@param config CopilotChat.config.Shared? function M.select_prompt(config) - local prompts = prompts.list_prompts() - local keys = vim.tbl_keys(prompts) + local prompt_list = prompts.list_prompts() + local keys = vim.tbl_keys(prompt_list) table.sort(keys) local choices = vim @@ -358,8 +366,8 @@ function M.select_prompt(config) :map(function(name) return { name = name, - description = prompts[name].description, - prompt = prompts[name].prompt, + description = prompt_list[name].description, + prompt = prompt_list[name].prompt, } end) :filter(function(choice) @@ -696,6 +704,7 @@ function M.setup(config) end) -- Initialize chat + require('CopilotChat.notify').clear() if M.chat then M.chat:close() M.chat:delete() diff --git a/lua/CopilotChat/notify.lua b/lua/CopilotChat/notify.lua index 99aa499a..b15b209a 100644 --- a/lua/CopilotChat/notify.lua +++ b/lua/CopilotChat/notify.lua @@ -32,4 +32,9 @@ function M.listen(event_name, callback) table.insert(M.listeners[event_name], callback) end +--- Clear all listeners +function M.clear() + M.listeners = {} +end + return M diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 8744c97d..0f3b4871 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -863,10 +863,10 @@ function Chat:render() -- Highlight tools in the last user message local assistant_msg = self:get_message(constants.ROLE.ASSISTANT) if assistant_msg and assistant_msg.tool_calls and #assistant_msg.tool_calls > 0 then - for i, line in ipairs(utils.split_lines(message.content)) do + for j, line in ipairs(utils.split_lines(message.content)) do for _, tool_call in ipairs(assistant_msg.tool_calls) do if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then - local l = message.section.start_line + i + local l = message.section.start_line + j vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatAnnotationHeader', l, 0, #line) if not utils.empty(tool_call.arguments) then vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l, 0, { diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 36589ccd..32157685 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -128,7 +128,9 @@ function Overlay:restore(winnr, bufnr) -- Manually trigger BufEnter event as nvim_win_set_buf does not trigger it vim.schedule(function() - vim.cmd(string.format('doautocmd BufEnter %s', bufnr)) + if vim.api.nvim_buf_is_valid(bufnr) then + vim.api.nvim_exec_autocmds('BufEnter', { buffer = bufnr }) + end end) end diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index c0cf2f85..23199930 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -161,16 +161,16 @@ function M.apply_unified_diff(diff_text, original_content) end local new_lines = vim.split(new_content, '\n', { trimempty = true }) - local hunks = vim.diff( + local diff_hunks = vim.diff( original_content, new_content, { algorithm = 'myers', ctxlen = 10, interhunkctxlen = 10, ignore_whitespace_change = true, result_type = 'indices' } ) - if not hunks or #hunks == 0 then + if not diff_hunks or #diff_hunks == 0 then return new_lines, applied, nil, nil end local first, last - for _, hunk in ipairs(hunks) do + for _, hunk in ipairs(diff_hunks) do local hunk_start = hunk[1] local hunk_end = hunk[1] + hunk[2] - 1 if not first or hunk_start < first then diff --git a/lua/CopilotChat/utils/files.lua b/lua/CopilotChat/utils/files.lua index 7da2be10..24b7b003 100644 --- a/lua/CopilotChat/utils/files.lua +++ b/lua/CopilotChat/utils/files.lua @@ -164,7 +164,7 @@ M.grep = async.wrap(function(path, opts, callback) if opts.pattern then table.insert(cmd, '-e') - table.insert(cmd, "'" .. opts.pattern .. "'") + table.insert(cmd, opts.pattern) end elseif vim.fn.executable('grep') == 1 then table.insert(cmd, 'grep') @@ -172,7 +172,7 @@ M.grep = async.wrap(function(path, opts, callback) if opts.pattern then table.insert(cmd, '-e') - table.insert(cmd, "'" .. opts.pattern .. "'") + table.insert(cmd, opts.pattern) end end From 969c220c76ad8cbf65463849fca67d390067d7f3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 15 Apr 2026 16:11:11 +0200 Subject: [PATCH 701/728] fix(config): include hidden files in file enum glob (#1548) The file enumeration function now includes hidden files by setting `hidden = true` in the glob options. This ensures that all files, including dotfiles, are considered during enumeration. Closes #1516 --- lua/CopilotChat/config/functions.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 4d849bbe..991759dd 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -64,6 +64,7 @@ return { enum = function(source) return files.glob(source.cwd(), { max_count = 0, + hidden = true, }) end, }, From 8b58670b69eb85f764b653081b42c9ed147583a1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 15 Apr 2026 16:17:36 +0200 Subject: [PATCH 702/728] fix(init): simplify lazy initialization logic (#1549) Refactors the lazy initialization in the CopilotChat main module to call the setup function directly and set the 'initialized' flag before invocation. Removes the pcall error handling and logging, as setup should not fail silently. This makes the initialization logic clearer and more predictable. --- lua/CopilotChat/init.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ac7302bc..9cea5d2d 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -22,12 +22,8 @@ local M = setmetatable({}, { -- Lazy initialize local initialized = rawget(t, 'initialized') if not initialized then - local ok, err = pcall(rawget(t, 'setup')) - if ok then - rawset(t, 'initialized', true) - else - require('plenary.log').error('CopilotChat setup failed:', err) - end + rawset(t, 'initialized', true) + rawget(t, 'setup')() end return rawget(t, key) From 4f643c7ecd395e06c3648c706fff8121e195eaa1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 15 Apr 2026 22:56:17 +0200 Subject: [PATCH 703/728] fix(prompts): use ordered map for enabled tools (#1551) And add notify test See #1550 Signed-off-by: Tomas Slusny --- lua/CopilotChat/prompts.lua | 7 +- tests/notify_spec.lua | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 tests/notify_spec.lua diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 71ac08be..461a0770 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -3,6 +3,7 @@ local constants = require('CopilotChat.constants') local functions = require('CopilotChat.functions') local notify = require('CopilotChat.notify') local files = require('CopilotChat.utils.files') +local orderedmap = require('CopilotChat.utils.orderedmap') local utils = require('CopilotChat.utils') local WORD = '([^%s:]+)' @@ -66,7 +67,7 @@ function M.resolve_tools(prompt, config) tools[tool.name] = tool end - local enabled_tools = {} + local enabled_tools = orderedmap() local tool_matches = utils.to_table(config.tools) -- Check for @tool pattern to find enabled tools @@ -82,12 +83,12 @@ function M.resolve_tools(prompt, config) for _, match in ipairs(tool_matches) do for name, tool in pairs(config.functions) do if name == match or tool.group == match then - table.insert(enabled_tools, tools[name]) + enabled_tools:set(name, tools[name]) end end end - return enabled_tools, prompt + return enabled_tools:values(), prompt end --- Call and resolve function calls from the prompt. diff --git a/tests/notify_spec.lua b/tests/notify_spec.lua new file mode 100644 index 00000000..f0064056 --- /dev/null +++ b/tests/notify_spec.lua @@ -0,0 +1,126 @@ +local notify = require('CopilotChat.notify') + +describe('CopilotChat.notify', function() + before_each(function() + -- Clear all listeners before each test + notify.clear() + end) + + describe('publish and listen', function() + it('calls listener when event is published', function() + local called = false + local received_data = nil + + notify.listen('test_event', function(data) + called = true + received_data = data + end) + + notify.publish('test_event', 'test_data') + + assert.is_true(called) + assert.equals('test_data', received_data) + end) + + it('supports multiple listeners for same event', function() + local count = 0 + + notify.listen('test_event', function(data) + count = count + 1 + end) + notify.listen('test_event', function(data) + count = count + 10 + end) + + notify.publish('test_event', 'data') + + assert.equals(11, count) + end) + + it('does not call listeners for different events', function() + local called = false + + notify.listen('event_a', function(data) + called = true + end) + + notify.publish('event_b', 'data') + + assert.is_false(called) + end) + + it('passes correct data to listeners', function() + local received = nil + + notify.listen('test_event', function(data) + received = data + end) + + notify.publish('test_event', { foo = 'bar', num = 123 }) + + assert.are.same({ foo = 'bar', num = 123 }, received) + end) + + it('handles nil and empty data', function() + local received = 'not_called' + + notify.listen('test_event', function(data) + received = data + end) + + notify.publish('test_event', nil) + assert.is_nil(received) + + notify.publish('test_event', '') + assert.equals('', received) + end) + + it('handles publishing to events with no listeners', function() + -- Should not error + assert.has_no.errors(function() + notify.publish('nonexistent_event', 'data') + end) + end) + end) + + describe('clear', function() + it('removes all listeners', function() + local called = false + + notify.listen('test_event', function(data) + called = true + end) + + notify.clear() + notify.publish('test_event', 'data') + + assert.is_false(called) + end) + + it('allows adding new listeners after clear', function() + local called = false + + notify.listen('test_event', function(data) + called = true + end) + notify.clear() + + notify.listen('test_event', function(data) + called = true + end) + notify.publish('test_event', 'data') + + assert.is_true(called) + end) + end) + + describe('constants', function() + it('defines STATUS constant', function() + assert.equals('status', notify.STATUS) + end) + + it('defines MESSAGE constant', function() + assert.equals('message', notify.MESSAGE) + end) + end) +end) From 1372a57cf387533f5ba64986c551ade9b4213443 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 16 Apr 2026 00:24:21 +0200 Subject: [PATCH 704/728] fix(provider,client): improve tool call merging and filtering (#1552) - Refactor tool call merging logic to use id, index, or name as key, ensuring more robust accumulation of streaming tool call deltas. - Update tool call filtering to exclude entries without a name, preventing incomplete tool calls from being returned. - Remove fallback default values for id, index, and name in provider output, ensuring only valid tool call data is included. - Minor formatting adjustment in init.lua. Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 25 ++++++++++++++++++++----- lua/CopilotChat/config/providers.lua | 18 +++++++----------- lua/CopilotChat/init.lua | 1 + 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index e2801844..e8383b8c 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -445,12 +445,22 @@ function Client:ask(opts) if out.tool_calls then for _, tool_call in ipairs(out.tool_calls) do - local key = tool_call.id or tool_call.index - local val = tool_calls:get(key) - if not val then + local key = tool_call.id or tool_call.index or tool_call.name or (#tool_calls:values() + 1) + local existing = tool_calls:get(key) + + if not existing then tool_calls:set(key, tool_call) else - val.arguments = val.arguments .. tool_call.arguments + existing.arguments = existing.arguments .. tool_call.arguments + if tool_call.id then + existing.id = tool_call.id + end + if tool_call.index then + existing.index = tool_call.index + end + if tool_call.name then + existing.name = tool_call.name + end end end end @@ -597,12 +607,17 @@ function Client:ask(opts) response_reasoning = response_reasoning_buffer:tostring() end + -- Filter out tool calls that don't have names (streaming deltas used only for accumulation) + local final_tool_calls = vim.tbl_filter(function(tc) + return tc.name ~= nil + end, tool_calls:values()) + return { message = { role = constants.ROLE.ASSISTANT, content = response_text, reasoning = response_reasoning, - tool_calls = #tool_calls:values() > 0 and tool_calls:values() or nil, + tool_calls = #final_tool_calls > 0 and final_tool_calls or nil, model = out_model, }, token_count = token_count, diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index c817f6f8..548f6abd 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -397,14 +397,12 @@ local function prepare_responses_output(output) content = output.delta.text end elseif output.type == 'response.output_item.done' then - -- Complete output item (including tool calls) local item = output.item if item and item.type == 'function_call' then - local index = output.output_index or (#tool_calls + 1) table.insert(tool_calls, { - id = item.call_id or ('tooluse_' .. index), - index = index, - name = item.name or '', + id = item.call_id, + index = output.output_index, + name = item.name, arguments = item.arguments or '', }) end @@ -436,9 +434,9 @@ local function prepare_responses_output(output) if msg.tool_calls then for i, tool_call in ipairs(msg.tool_calls) do table.insert(tool_calls, { - id = tool_call.call_id or ('tooluse_' .. i), + id = tool_call.call_id, index = i, - name = tool_call.name or '', + name = tool_call.name, arguments = tool_call.arguments or '', }) end @@ -481,11 +479,9 @@ local function prepare_chat_output(output) for i, tool_call in ipairs(message.tool_calls) do local fn = tool_call['function'] if fn then - local index = tool_call.index or i - local id = utils.empty(tool_call.id) and ('tooluse_' .. index) or tool_call.id table.insert(tool_calls, { - id = id, - index = index, + id = tool_call.id, + index = tool_call.index or i, name = fn.name, arguments = fn.arguments or '', }) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9cea5d2d..1b7d0570 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -546,6 +546,7 @@ function M.ask(prompt, config) M.chat:add_message(response, true) M.chat.token_count = token_count M.chat.token_max_count = token_max_count + finish() end end)) From ed28b01dd333bd69741c7df23e01cbe0bce2b557 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 16 Apr 2026 00:39:57 +0200 Subject: [PATCH 705/728] fix(client): ensure tool call keys are strings (#1553) Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index e8383b8c..585e01aa 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -445,7 +445,7 @@ function Client:ask(opts) if out.tool_calls then for _, tool_call in ipairs(out.tool_calls) do - local key = tool_call.id or tool_call.index or tool_call.name or (#tool_calls:values() + 1) + local key = tostring(tool_call.index or tool_call.id or tool_call.name or #tool_calls:values() + 1) local existing = tool_calls:get(key) if not existing then From bcfbbc2ff60246cb1abfabe7e58a7f03efc26a9b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 16 Apr 2026 03:59:45 +0200 Subject: [PATCH 706/728] fix(prompt): use correct variable in select_prompt (#1555) Replaces usage of the undefined 'prompts' variable with 'prompt_list' in the select_prompt function. This ensures the correct prompt data is used when invoking the ask method, preventing potential runtime errors. Closes #1554 --- 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 1b7d0570..b5052845 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -378,7 +378,7 @@ function M.select_prompt(config) end, }, function(choice) if choice then - M.ask(prompts[choice.name].prompt, vim.tbl_extend('force', prompts[choice.name], config or {})) + M.ask(prompt_list[choice.name].prompt, vim.tbl_extend('force', prompt_list[choice.name], config or {})) end end) end From d1d767c3a5b80ab8f581d567f9921fda061f6ce2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 02:00:06 +0000 Subject: [PATCH 707/728] 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 39c4d170..24b6300d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 April 15 + For NVIM v0.8.0 Last change: 2026 April 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 1af80ec73e3f7eabd4e9bd9595646acf46a0b192 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 16 Apr 2026 10:31:49 +0200 Subject: [PATCH 708/728] feat(trusted-tools): auto-execute trusted tool calls (#1556) Add `trusted_tools` config option to allow automatic execution of trusted tool calls without manual approval. By default, all tool calls require approval, but users can now trust specific tools, groups, or all tools. Update README and type annotations to document the new behavior. Trusted tools are determined by function definition, group, or name. Closes #1534 --- README.md | 176 ++++++++++++++++++--------- lua/CopilotChat/config.lua | 2 + lua/CopilotChat/config/functions.lua | 1 + lua/CopilotChat/config/mappings.lua | 23 ---- lua/CopilotChat/init.lua | 137 +++++++++++++++++++-- lua/CopilotChat/prompts.lua | 144 ++++++++++++++-------- lua/CopilotChat/ui/chat.lua | 23 ++-- lua/CopilotChat/ui/overlay.lua | 10 +- 8 files changed, 359 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 9a54c75e..b37e2cfe 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. - 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your [GitHub Copilot settings](https://github.com/settings/copilot/features) and the models provided by GitHub's API. -- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with your explicit approval +- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with manual approval or automatic execution for trusted tools - 🔒 **Privacy First** - Only shares what you explicitly request - no background data collection - 📝 **Interactive Chat** - Interactive UI with completion, diffs, and quickfix integration - 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context @@ -92,7 +92,7 @@ EOF # Core Concepts - **Resources** (`#`) - Add specific content (files, git diffs, URLs) to your prompt -- **Tools** (`@`) - Give LLM access to functions it can call with your approval +- **Tools** (`@`) - Give LLM access to functions it can call during the chat, with manual approval by default - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks @@ -114,7 +114,15 @@ EOF > You are a helpful coding assistant ``` -When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`, `glob`, `grep`, `gitdiff` etc. You'll see the proposed function call and can approve/reject it before execution. +When you use `@copilot`, the LLM can call functions from the `copilot` group such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`. + +- By default, proposed tool calls wait for your approval. +- You can configure `trusted_tools` to automatically run specific tools or groups. +- Resources added with `#...` are resolved immediately and shared as context. +- Tool call results are sent back to the model as plain output, while manual resources keep their `##` references in chat. + +> [!WARNING] +> `trusted_tools = true` allows the model to run every enabled tool without asking. Only use it if you fully trust the tool set and workspace. # Usage @@ -136,21 +144,20 @@ When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`, ## Chat Key Mappings -| Insert | Normal | Action | -| ------- | ------- | ------------------------------------------ | -| `` | - | Trigger/accept completion menu for tokens | -| `` | `q` | Close the chat window | -| `` | `` | Reset and clear the chat window | -| `` | `` | Submit the current prompt | -| - | `grr` | Toggle sticky prompt for line under cursor | -| `` | `` | Accept nearest diff | -| - | `gj` | Jump to section of nearest diff | -| - | `gqa` | Add all answers from chat to quickfix list | -| - | `gqd` | Add all diffs from chat to quickfix list | -| - | `gy` | Yank nearest diff to register | -| - | `gd` | Show diff between source and nearest diff | -| - | `gc` | Show info about current chat | -| - | `gh` | Show help message | +| Insert | Normal | Action | +| ------- | ------- | ----------------------------------------- | +| `` | - | Trigger/accept completion menu for tokens | +| `` | `q` | Close the chat window | +| `` | `` | Reset and clear the chat window | +| `` | `` | Submit the current prompt | +| `` | `` | Accept nearest diff | +| - | `gj` | Jump to section of nearest diff | +| - | `gqa` | Add all answers from chat to quickfix | +| - | `gqd` | Add all diffs from chat to quickfix | +| - | `gy` | Yank nearest diff to register | +| - | `gd` | Show diff between source and nearest diff | +| - | `gc` | Show info about current chat | +| - | `gh` | Show help message | > [!WARNING] > Some plugins (e.g. `copilot.vim`) may also map common keys like `` in insert mode. @@ -167,23 +174,24 @@ When you use `@copilot`, the LLM can call functions like `bash`, `edit`, `file`, All predefined functions belong to the `copilot` group. -| Function | Type | Description | Example Usage | -| ----------- | -------- | ------------------------------------------------------ | -------------------- | -| `bash` | Tool | Executes a bash command and returns output | `@copilot` only | -| `buffer` | Resource | Retrieves content from buffer(s) with diagnostics | `#buffer:active` | -| `clipboard` | Resource | Provides access to system clipboard content | `#clipboard` | -| `edit` | Tool | Applies a unified diff to a file | `@copilot` only | -| `file` | Resource | Reads content from a specified file path | `#file:path/to/file` | -| `gitdiff` | Resource | Retrieves git diff information | `#gitdiff:staged` | -| `glob` | Resource | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | -| `grep` | Resource | Searches for a pattern across files in workspace | `#grep:TODO` | -| `selection` | Resource | Includes the current visual selection with diagnostics | `#selection` | -| `url` | Resource | Fetches content from a specified URL | `#url:https://...` | +| Function | Manual `#...` | Description | Example Usage | +| ----------- | ------------- | ------------------------------------------------------ | -------------------- | +| `bash` | No | Executes a bash command and returns output | `@copilot` | +| `buffer` | Yes | Retrieves content from buffer(s) with diagnostics | `#buffer:active` | +| `clipboard` | Yes | Provides access to system clipboard content | `#clipboard` | +| `edit` | No | Applies a unified diff to a file | `@copilot` | +| `file` | Yes | Reads content from a specified file path | `#file:path/to/file` | +| `gitdiff` | Yes | Retrieves git diff information | `#gitdiff:staged` | +| `glob` | Yes | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | +| `grep` | Yes | Searches for a pattern across files in workspace | `#grep:TODO` | +| `selection` | Yes | Includes the current visual selection with diagnostics | `#selection` | +| `url` | Yes | Fetches content from a specified URL | `#url:https://...` | + +`#...` resolves a function immediately and adds its output as chat context. -**Type Legend:** +`@copilot` shares the enabled functions with the model so it can choose when to call them. -- **Resource**: Can be used manually via `#function` syntax -- **Tool**: Can only be called by LLM via `@copilot` (for safety/complexity reasons) +Only `bash` and `edit` are tool-only. The rest can be used both as manual resources and as callable tools. ## Predefined Prompts @@ -209,6 +217,7 @@ Most users only need to configure a few options: { model = 'gpt-4.1', -- AI model to use temperature = 0.1, -- Lower = focused, higher = creative + trusted_tools = nil, -- Require approval for all tool calls window = { layout = 'vertical', -- 'vertical', 'horizontal', 'float' width = 0.5, -- 50% of screen width @@ -241,12 +250,14 @@ Most users only need to configure a few options: } ``` +`window.layout` also supports `'replace'` to reuse the current window. + ## Buffer Behavior ```lua -- Auto-command to customize chat buffer behavior vim.api.nvim_create_autocmd('BufEnter', { - pattern = 'copilot-*', + pattern = 'copilot-chat', callback = function() vim.opt_local.relativenumber = false vim.opt_local.number = false @@ -278,6 +289,7 @@ Types of copilot highlights: - `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) - `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) +- `CopilotChatAnnotationHeader` - Annotation header highlight in chat buffer ## Prompts @@ -304,14 +316,44 @@ Define your own prompts in the configuration: ## Functions +Use `trusted_tools` to control which tool calls are executed automatically: + +```lua +{ + trusted_tools = nil, -- default: require approval for all tool calls + + -- trust all functions in a group + -- trusted_tools = 'copilot', + + -- trust specific functions by name or groups by name + -- trusted_tools = { 'file', 'glob', 'grep' }, + + -- trust every enabled tool call + -- trusted_tools = true, +} +``` + +A tool is trusted when any of these match: + +- Its function definition sets `trusted = true` +- Its function name appears in `trusted_tools` +- Its function group appears in `trusted_tools` +- `trusted_tools = true` + +For most setups, trusting a few read-only functions such as `file`, `glob`, or `grep` is safer than trusting everything. + +> [!WARNING] +> Trusted tools run without asking for confirmation. Be especially careful with tools like `bash` and `edit`, which can change your workspace. + Define your own functions in the configuration with input handling and schema: ```lua { functions = { birthday = { - description = "Retrieves birthday information for a person", - uri = "birthday://{name}", + description = 'Retrieves birthday information for a person', + uri = 'birthday://{name}', + trusted = false, schema = { type = 'object', required = { 'name' }, @@ -329,14 +371,16 @@ Define your own functions in the configuration with input handling and schema: uri = 'birthday://' .. input.name, mimetype = 'text/plain', data = input.name .. ' birthday info', - } + }, } - end - } + end, + }, } } ``` +If a function has a `uri`, it can be used manually with `#birthday:Alice`. Functions without a `uri` are tool-only and can only be called by the model. + ## Providers Add custom AI providers: @@ -345,9 +389,9 @@ Add custom AI providers: { providers = { my_provider = { - get_url = function(opts) return "https://api.example.com/chat" end, - get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end, - get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end, + get_url = function(opts) return 'https://api.example.com/chat' end, + get_headers = function() return { ['Authorization'] = 'Bearer ' .. api_key } end, + get_models = function() return { { id = 'gpt-4.1', name = 'GPT-4.1 model' } } end, prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, } @@ -363,7 +407,7 @@ Add custom AI providers: disabled?: boolean, -- Optional: Extra info about the provider displayed in info panel - get_info?(): string[] + get_info?(headers: table): string[] -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, @@ -379,20 +423,23 @@ Add custom AI providers: -- Optional: Get available models get_models?(headers: table): table, + + -- Optional: Resolve a user-facing model id to a provider model id + resolve_model?(headers: table, model: string): string, } ``` **Built-in providers:** - `copilot` - GitHub Copilot (default) -- `github_models` - GitHub Marketplace models (disabled by default) +- `github_models` - GitHub Models (disabled by default) # API Reference ## Core ```lua -local chat = require("CopilotChat") +local chat = require('CopilotChat') -- Basic Chat Functions chat.ask(prompt, config) -- Ask a question with optional config @@ -422,7 +469,7 @@ chat.log_level(level) -- Set log level (debug, info, etc.) You can also access the chat window UI methods through the `chat.chat` object: ```lua -local window = require("CopilotChat").chat +local window = require('CopilotChat').chat -- Chat UI State window:visible() -- Check if chat window is visible @@ -441,8 +488,8 @@ window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window -- Source Management -window.get_source() -- Get the current source buffer and window -window.set_source(winnr) -- Set the source window +window:get_source() -- Get the current source buffer and window +window:set_source(winnr) -- Set the source window -- Navigation window:follow() -- Move cursor to end of chat content @@ -455,10 +502,11 @@ window:overlay(opts) -- Show overlay with specified options ## Prompt parser ```lua -local parser = require("CopilotChat.prompts") +local parser = require('CopilotChat.prompts') parser.resolve_prompt() -- Resolve prompt references -parser.resolve_tools() -- Resolve tools that are available for automatic use by LLM +parser.resolve_tools() -- Resolve tools shared with the model via @... +parser.resolve_functions() -- Resolve manual function/resource references via #... parser.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) ``` @@ -466,22 +514,26 @@ parser.resolve_model() -- Resolve model from prompt (WARN: async, requi ```lua -- Open chat, ask a question and handle response -require("CopilotChat").open() -require("CopilotChat").ask("#buffer Explain this code", { +require('CopilotChat').open() +require('CopilotChat').ask('#buffer Explain this code', { callback = function(response) - vim.notify("Got response: " .. response:sub(1, 50) .. "...") - return response + vim.notify('Got response: ' .. vim.trim(response.content):sub(1, 50) .. '...') end, }) -- Save and load chat history -require("CopilotChat").save("my_debugging_session") -require("CopilotChat").load("my_debugging_session") +require('CopilotChat').save('my_debugging_session') +require('CopilotChat').load('my_debugging_session') -- Use custom sticky and model -require("CopilotChat").ask("How can I optimize this?", { - model = "gpt-4.1", - sticky = {"#buffer", "#gitdiff:staged"} +require('CopilotChat').ask('How can I optimize this?', { + model = 'gpt-4.1', + sticky = { '#buffer', '#gitdiff:staged' }, +}) + +-- Automatically trust a small read-only tool set +require('CopilotChat').setup({ + trusted_tools = { 'file', 'glob', 'grep' }, }) ``` @@ -512,6 +564,12 @@ To run tests: make test ``` +To run the same formatting check as CI: + +```bash +stylua --check . +``` + ## Contributing 1. Fork the repository diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 1e0adda3..a54a66f3 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -19,6 +19,7 @@ ---@field tools string|table|nil ---@field resources string|table|nil ---@field sticky string|table|nil +---@field trusted_tools boolean|string|table|nil ---@field diff 'block'|'unified'? ---@field language string? ---@field temperature number? @@ -64,6 +65,7 @@ return { tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). resources = 'selection', -- Default resources to share with LLM (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + trusted_tools = nil, -- Trust tool calls from specific functions or groups, or all trusted tools when true (e.g., {'buffer', 'file'} or 'copilot'). diff = 'block', -- Default diff format to use, 'block' or 'unified'. language = 'English', -- Default language to use for answers diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 991759dd..0ec22b10 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -44,6 +44,7 @@ end ---@field description string? ---@field schema table? ---@field group string? +---@field trusted boolean? ---@field uri string? ---@field resolve fun(input: table, source: CopilotChat.ui.chat.Source):CopilotChat.client.Resource[] diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index a72529f9..c3028f8a 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -290,7 +290,6 @@ return { async.run(function() local config, prompt = prompts.resolve_prompt(message.content) local system_prompt = config.system_prompt - local resolved_resources = prompts.resolve_functions(prompt, config) local selected_tools = prompts.resolve_tools(prompt, config) local selected_model = prompts.resolve_model(prompt, config) local infos = client:info() @@ -357,28 +356,6 @@ return { table.insert(lines, '') end - if not utils.empty(resolved_resources) then - table.insert(lines, '**Resources**') - table.insert(lines, '') - end - - for _, resource in ipairs(resolved_resources) do - local resource_lines = vim.split(resource.data, '\n') - local preview = vim.list_slice(resource_lines, 1, math.min(10, #resource_lines)) - local header = string.format('**%s** (%s lines)', resource.name or resource.uri, #resource_lines) - if #resource_lines > 10 then - header = header .. ' (truncated)' - end - - table.insert(lines, header) - table.insert(lines, '```' .. files.mimetype_to_filetype(resource.mimetype)) - for _, line in ipairs(preview) do - table.insert(lines, line) - end - table.insert(lines, '```') - table.insert(lines, '') - end - chat:overlay({ text = vim.trim(table.concat(lines, '\n')) .. '\n', }) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b5052845..f6a2cc3b 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -2,6 +2,7 @@ local async = require('plenary.async') local log = require('plenary.log') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') +local functions = require('CopilotChat.functions') local prompts = require('CopilotChat.prompts') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') @@ -30,6 +31,37 @@ local M = setmetatable({}, { end, }) +---@param config CopilotChat.config.Shared +---@param tool_name string +---@return boolean +local function is_trusted_tool(config, tool_name) + local tool_spec = config.functions[tool_name] + if not tool_spec then + return false + end + + if tool_spec.trusted then + return true + end + + local trusted_tools = config.trusted_tools + if trusted_tools == true then + return true + end + + for _, trusted_pattern in ipairs(utils.to_table(trusted_tools)) do + if tool_name == trusted_pattern then + return true + end + + if tool_spec.group == trusted_pattern then + return true + end + end + + return false +end + --- Process sticky values from prompt and config --- Extracts stickies from prompt, adds config-based stickies, stores them, returns clean prompt ---@param prompt string @@ -116,7 +148,7 @@ end --- Finish writing to chat buffer. ---@param start_of_chat boolean? -local function finish(start_of_chat) +local function finish(start_of_chat, remaining_tool_calls) if start_of_chat then local sticky = {} if M.config.sticky then @@ -128,8 +160,11 @@ local function finish(start_of_chat) end local prompt_content = '' - local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) - local tool_calls = assistant_message and assistant_message.tool_calls or {} + local tool_calls = remaining_tool_calls + if not tool_calls then + local assistant_message = M.chat:get_message(constants.ROLE.ASSISTANT) + tool_calls = assistant_message and assistant_message.tool_calls or {} + end local current_sticky = M.chat:get_sticky() if not utils.empty(current_sticky) then @@ -430,16 +465,9 @@ function M.ask(prompt, config) config, prompt = prompts.resolve_prompt(prompt, config) local system_prompt = config.system_prompt or '' local selected_tools, prompt = prompts.resolve_tools(prompt, config) - local resolved_resources, resolved_tools, resolved_stickies, prompt = prompts.resolve_functions(prompt, config) + local resolved_resources, resolved_tools, prompt = prompts.resolve_functions(prompt, config) local selected_model, prompt = prompts.resolve_model(prompt, config) - -- Store resolved stickies to chat - local current_sticky = M.chat:get_sticky() - for _, sticky in ipairs(resolved_stickies) do - table.insert(current_sticky, sticky) - end - M.chat:set_sticky(current_sticky) - prompt = vim.trim(prompt) if not config.headless then @@ -547,6 +575,93 @@ function M.ask(prompt, config) M.chat.token_count = token_count M.chat.token_max_count = token_max_count + -- Execute trusted tool calls automatically + if response.tool_calls and #response.tool_calls > 0 then + local trusted_tool_calls = {} + local untrusted_tool_calls = {} + + for _, tool_call in ipairs(response.tool_calls) do + if is_trusted_tool(config, tool_call.name) then + table.insert(trusted_tool_calls, tool_call) + else + table.insert(untrusted_tool_calls, tool_call) + end + end + + if #trusted_tool_calls > 0 then + async.run(handle_error(config, function() + local trusted_tool_results = {} + local source = M.chat:get_source() + + for _, tool_call in ipairs(trusted_tool_calls) do + local input = {} + if not utils.empty(tool_call.arguments) then + input = utils.json_decode(tool_call.arguments) + end + + local ok, output = prompts.execute_tool_call(tool_call.name, input, config, source) + local result = prompts.format_tool_output(ok, output) + + table.insert(trusted_tool_results, { + id = tool_call.id, + result = result, + }) + end + + if not utils.empty(trusted_tool_results) then + utils.schedule_main() + for _, tool in ipairs(trusted_tool_results) do + M.chat:add_message({ + id = tool.id, + role = constants.ROLE.TOOL, + tool_call_id = tool.id, + content = '\n' .. tool.result .. '\n', + }) + end + + if #untrusted_tool_calls > 0 then + finish(nil, untrusted_tool_calls) + else + local continue_response = client:ask({ + headless = config.headless, + history = M.chat:get_messages(), + resources = resolved_resources, + tools = selected_tools, + system_prompt = system_prompt, + model = selected_model, + temperature = config.temperature, + on_progress = vim.schedule_wrap(function(message) + if not config.headless then + M.chat:add_message(message) + end + end), + }) + + if continue_response then + local continue_message = continue_response.message + continue_message.content = vim.trim(continue_message.content) + if utils.empty(continue_message.content) then + continue_message.content = '' + else + continue_message.content = '\n' .. continue_message.content .. '\n' + end + + utils.schedule_main() + M.chat:add_message(continue_message, true) + M.chat.token_count = continue_response.token_count + M.chat.token_max_count = continue_response.token_max_count + end + + finish() + end + else + finish() + end + end)) + return + end + end + finish() end end)) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 461a0770..f5c76525 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -91,10 +91,67 @@ function M.resolve_tools(prompt, config) return enabled_tools:values(), prompt end +--- Execute a tool call and return the raw output. +---@param name string Tool name +---@param input table|string Input arguments +---@param config CopilotChat.config.Shared +---@param source CopilotChat.client.Source +---@return boolean ok +---@return any output +---@async +function M.execute_tool_call(name, input, config, source) + local tool = config.functions[name] + if not tool or not tool.resolve then + return false, 'Tool not found: ' .. name + end + + local schema = nil + for _, t in ipairs(functions.parse_tools(config.functions)) do + if t.name == name then + schema = t.schema + break + end + end + + local ok, output + if config.stop_on_function_failure then + output = tool.resolve(functions.parse_input(input, schema), source) + ok = true + else + ok, output = pcall(tool.resolve, functions.parse_input(input, schema), source) + end + + return ok, output +end + +--- Format tool output as plain text. +---@param ok boolean +---@param output any +---@return string +function M.format_tool_output(ok, output) + local result = '' + if not ok then + result = utils.make_string(output) + elseif type(output) ~= 'table' then + result = utils.make_string(output) + else + for _, content in ipairs(output) do + if content then + local data = content.data or content.uri + if data then + result = result .. (utils.empty(result) and '' or '\n') .. data + end + end + end + end + + return result +end + --- Call and resolve function calls from the prompt. ---@param prompt string? ---@param config CopilotChat.config.Shared? ----@return table, table, table, string +---@return table, table, string ---@async function M.resolve_functions(prompt, config) config, prompt = M.resolve_prompt(prompt, config) @@ -102,11 +159,6 @@ function M.resolve_functions(prompt, config) local chat = require('CopilotChat').chat local source = chat:get_source() - local tools = {} - for _, tool in ipairs(functions.parse_tools(config.functions)) do - tools[tool.name] = tool - end - if config.resources then local resources = utils.to_table(config.resources) local lines = utils.split_lines(prompt) @@ -119,7 +171,6 @@ function M.resolve_functions(prompt, config) local resolved_resources = {} local resolved_tools = {} - local resolved_stickies = {} local tool_calls = {} utils.schedule_main() @@ -199,58 +250,51 @@ function M.resolve_functions(prompt, config) return nil end - local schema = tools[name] and tools[name].schema or nil - local ok, output - if config.stop_on_function_failure then - output = tool.resolve(functions.parse_input(input, schema), source) - ok = true - else - ok, output = pcall(tool.resolve, functions.parse_input(input, schema), source) + local ok, output = M.execute_tool_call(name, input, config, source) + + if tool_id then + table.insert(resolved_tools, { + id = tool_id, + result = M.format_tool_output(ok, output), + }) + + return '' end - local result = '' if not ok then - result = utils.make_string(output) - else - for _, content in ipairs(output) do - if content then - local content_out = nil - if content.uri then - if - not vim.tbl_contains(resolved_resources, function(resource) - return resource.uri == content.uri - end, { predicate = true }) - then - content_out = '##' .. content.uri - table.insert(resolved_resources, content) - end - - if tool_id then - table.insert(resolved_stickies, '##' .. content.uri) - end - else - content_out = content.data + return utils.make_string(output) + end + + if type(output) ~= 'table' then + return utils.make_string(output) + end + + local result = '' + for _, content in ipairs(output) do + if content then + local content_out = nil + if content.uri then + if + not vim.tbl_contains(resolved_resources, function(resource) + return resource.uri == content.uri + end, { predicate = true }) + then + content_out = '##' .. content.uri + table.insert(resolved_resources, content) end + else + content_out = content.data + end - if content_out then - if not utils.empty(result) then - result = result .. '\n' - end - result = result .. content_out + if content_out then + if not utils.empty(result) then + result = result .. '\n' end + result = result .. content_out end end end - if tool_id then - table.insert(resolved_tools, { - id = tool_id, - result = result, - }) - - return '' - end - return result end @@ -266,7 +310,7 @@ function M.resolve_functions(prompt, config) end end - return resolved_resources, resolved_tools, resolved_stickies, prompt + return resolved_resources, resolved_tools, prompt end --- Resolve the final prompt and config from prompt template. diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 0f3b4871..f20d08b3 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -99,9 +99,9 @@ end ---@field content string ---@class CopilotChat.ui.chat.Section ----@field start_line number ----@field end_line number ----@field blocks table +---@field start_line integer +---@field end_line integer +---@field blocks CopilotChat.ui.chat.Block[] ---@class CopilotChat.ui.chat.Message : CopilotChat.client.Message ---@field id string? @@ -113,7 +113,7 @@ end --- @field cwd fun():string ---@class CopilotChat.ui.chat.Chat : CopilotChat.ui.overlay.Overlay ----@field winnr number? +---@field winnr integer? ---@field config CopilotChat.config.Shared ---@field token_count number? ---@field token_max_count number? @@ -125,7 +125,7 @@ end ---@field private chat_overlay CopilotChat.ui.overlay.Overlay ---@field private last_changedtick number? ---@field private source CopilotChat.ui.chat.Source ----@field private sticky table +---@field private sticky string[] local Chat = class(function(self, config, on_buf_create) Overlay.init(self, 'copilot-chat', utils.key_to_info('show_help', config.mappings.show_help), on_buf_create) @@ -243,7 +243,7 @@ function Chat:get_block(role, cursor) end --- Get list of all chat messages ----@return table +---@return CopilotChat.ui.chat.Message[] function Chat:get_messages() self:parse() return self.messages:values() @@ -269,7 +269,12 @@ function Chat:get_message(role, cursor) for _, message in ipairs(messages) do local section = message.section local matches_role = not role or message.role == role - if matches_role and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then + if + matches_role + and section + and section.start_line <= cursor_line + and section.start_line > max_line_below_cursor + then max_line_below_cursor = section.start_line closest_message = message end @@ -288,13 +293,13 @@ function Chat:get_message(role, cursor) end --- Get the current sticky array. ----@return table +---@return string[] function Chat:get_sticky() return self.sticky end --- Set the sticky array. ----@param sticky table +---@param sticky string[] function Chat:set_sticky(sticky) self.sticky = sticky end diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 32157685..ace646c4 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -2,7 +2,7 @@ local utils = require('CopilotChat.utils') local class = require('CopilotChat.utils.class') ---@class CopilotChat.ui.overlay.Overlay : Class ----@field bufnr number? +---@field bufnr integer? ---@field protected name string ---@field protected help string ---@field private cursor integer[]? @@ -23,11 +23,11 @@ end) --- Show the overlay buffer ---@param text string ----@param winnr number +---@param winnr integer ---@param filetype? string ---@param syntax string? ----@param on_show? fun(bufnr: number) ----@param on_hide? fun(bufnr: number) +---@param on_show? fun(bufnr: integer) +---@param on_hide? fun(bufnr: integer) function Overlay:show(text, winnr, filetype, syntax, on_show, on_hide) if not text or text == '' then return @@ -75,7 +75,7 @@ function Overlay:delete() end --- Create the overlay buffer ----@return number +---@return integer ---@protected function Overlay:create() local bufnr = vim.api.nvim_create_buf(false, true) From be326102bdc5aa365b373489e2302b39d1fdbff3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 08:32:10 +0000 Subject: [PATCH 709/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 179 ++++++++++++++++++++++++++++++-------------- 1 file changed, 121 insertions(+), 58 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 24b6300d..55ceca49 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -41,7 +41,7 @@ CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. - 🤖 **Multiple AI Models** - GitHub Copilot (including GPT-4o, Gemini 2.5 Pro, Claude 4 Sonnet, Claude 3.7 Sonnet, Claude 3.5 Sonnet, o3-mini, o4-mini) + custom providers (Ollama, Mistral.ai). The exact list of available models depends on your GitHub Copilot settings and the models provided by GitHub’s API. -- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with your explicit approval +- 🔧 **Tool Calling** - LLM can call workspace functions (file reading, git operations, search) with manual approval or automatic execution for trusted tools - 🔒 **Privacy First** - Only shares what you explicitly request - no background data collection - 📝 **Interactive Chat** - Interactive UI with completion, diffs, and quickfix integration - 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context @@ -126,7 +126,7 @@ VIM-PLUG *CopilotChat-vim-plug* 2. Core Concepts *CopilotChat-core-concepts* - **Resources** (`#`) - Add specific content (files, git diffs, URLs) to your prompt -- **Tools** (`@`) - Give LLM access to functions it can call with your approval +- **Tools** (`@`) - Give LLM access to functions it can call during the chat, with manual approval by default - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks @@ -149,10 +149,17 @@ EXAMPLES *CopilotChat-examples* > You are a helpful coding assistant < -When you use `@copilot`, the LLM can call functions like `bash`, `edit`, -`file`, `glob`, `grep`, `gitdiff` etc. You’ll see the proposed function call -and can approve/reject it before execution. +When you use `@copilot`, the LLM can call functions from the `copilot` group +such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`. +- By default, proposed tool calls wait for your approval. +- You can configure `trusted_tools` to automatically run specific tools or groups. +- Resources added with `#...` are resolved immediately and shared as context. +- Tool call results are sent back to the model as plain output, while manual resources keep their `##` references in chat. + + + [!WARNING] `trusted_tools = true` allows the model to run every enabled tool + without asking. Only use it if you fully trust the tool set and workspace. ============================================================================== 3. Usage *CopilotChat-usage* @@ -177,16 +184,15 @@ COMMANDS *CopilotChat-commands* CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* Insert Normal Action - -------- -------- -------------------------------------------- + -------- -------- ------------------------------------------- - Trigger/accept completion menu for tokens q Close the chat window Reset and clear the chat window Submit the current prompt - - grr Toggle sticky prompt for line under cursor Accept nearest diff - gj Jump to section of nearest diff - - gqa Add all answers from chat to quickfix list - - gqd Add all diffs from chat to quickfix list + - gqa Add all answers from chat to quickfix + - gqd Add all diffs from chat to quickfix - gy Yank nearest diff to register - gd Show diff between source and nearest diff - gc Show info about current chat @@ -205,39 +211,44 @@ PREDEFINED FUNCTIONS *CopilotChat-predefined-functions* All predefined functions belong to the `copilot` group. - ------------------------------------------------------------------------------------- - Function Type Description Example Usage - ----------- ---------- ----------------------------------------- -------------------- - bash Tool Executes a bash command and returns @copilot only - output + ---------------------------------------------------------------------------------- + Function Manual Description Example Usage + #... + ----------- --------- --------------------------------------- -------------------- + bash No Executes a bash command and returns @copilot + output - buffer Resource Retrieves content from buffer(s) with #buffer:active - diagnostics + buffer Yes Retrieves content from buffer(s) with #buffer:active + diagnostics - clipboard Resource Provides access to system clipboard #clipboard - content + clipboard Yes Provides access to system clipboard #clipboard + content - edit Tool Applies a unified diff to a file @copilot only + edit No Applies a unified diff to a file @copilot - file Resource Reads content from a specified file path #file:path/to/file + file Yes Reads content from a specified file #file:path/to/file + path - gitdiff Resource Retrieves git diff information #gitdiff:staged + gitdiff Yes Retrieves git diff information #gitdiff:staged - glob Resource Lists filenames matching a pattern in #glob:**/*.lua - workspace + glob Yes Lists filenames matching a pattern in #glob:**/*.lua + workspace - grep Resource Searches for a pattern across files in #grep:TODO - workspace + grep Yes Searches for a pattern across files in #grep:TODO + workspace - selection Resource Includes the current visual selection #selection - with diagnostics + selection Yes Includes the current visual selection #selection + with diagnostics - url Resource Fetches content from a specified URL #url:https://... - ------------------------------------------------------------------------------------- -**Type Legend:** + url Yes Fetches content from a specified URL #url:https://... + ---------------------------------------------------------------------------------- +`#...` resolves a function immediately and adds its output as chat context. -- **Resource**: Can be used manually via `#function` syntax -- **Tool**: Can only be called by LLM via `@copilot` (for safety/complexity reasons) +`@copilot` shares the enabled functions with the model so it can choose when to +call them. + +Only `bash` and `edit` are tool-only. The rest can be used both as manual +resources and as callable tools. PREDEFINED PROMPTS *CopilotChat-predefined-prompts* @@ -276,6 +287,7 @@ Most users only need to configure a few options: { model = 'gpt-4.1', -- AI model to use temperature = 0.1, -- Lower = focused, higher = creative + trusted_tools = nil, -- Require approval for all tool calls window = { layout = 'vertical', -- 'vertical', 'horizontal', 'float' width = 0.5, -- 50% of screen width @@ -309,13 +321,15 @@ WINDOW & APPEARANCE *CopilotChat-window-&-appearance* } < +`window.layout` also supports `'replace'` to reuse the current window. + BUFFER BEHAVIOR *CopilotChat-buffer-behavior* >lua -- Auto-command to customize chat buffer behavior vim.api.nvim_create_autocmd('BufEnter', { - pattern = 'copilot-*', + pattern = 'copilot-chat', callback = function() vim.opt_local.relativenumber = false vim.opt_local.number = false @@ -348,6 +362,7 @@ Types of copilot highlights: - `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) - `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) +- `CopilotChatAnnotationHeader` - Annotation header highlight in chat buffer PROMPTS *CopilotChat-prompts* @@ -376,14 +391,45 @@ Define your own prompts in the configuration: FUNCTIONS *CopilotChat-functions* +Use `trusted_tools` to control which tool calls are executed automatically: + +>lua + { + trusted_tools = nil, -- default: require approval for all tool calls + + -- trust all functions in a group + -- trusted_tools = 'copilot', + + -- trust specific functions by name or groups by name + -- trusted_tools = { 'file', 'glob', 'grep' }, + + -- trust every enabled tool call + -- trusted_tools = true, + } +< + +A tool is trusted when any of these match: + +- Its function definition sets `trusted = true` +- Its function name appears in `trusted_tools` +- Its function group appears in `trusted_tools` +- `trusted_tools = true` + +For most setups, trusting a few read-only functions such as `file`, `glob`, or +`grep` is safer than trusting everything. + + + [!WARNING] Trusted tools run without asking for confirmation. Be especially + careful with tools like `bash` and `edit`, which can change your workspace. Define your own functions in the configuration with input handling and schema: >lua { functions = { birthday = { - description = "Retrieves birthday information for a person", - uri = "birthday://{name}", + description = 'Retrieves birthday information for a person', + uri = 'birthday://{name}', + trusted = false, schema = { type = 'object', required = { 'name' }, @@ -401,14 +447,17 @@ Define your own functions in the configuration with input handling and schema: uri = 'birthday://' .. input.name, mimetype = 'text/plain', data = input.name .. ' birthday info', - } + }, } - end - } + end, + }, } } < +If a function has a `uri`, it can be used manually with `#birthday:Alice`. +Functions without a `uri` are tool-only and can only be called by the model. + PROVIDERS *CopilotChat-providers* @@ -418,9 +467,9 @@ Add custom AI providers: { providers = { my_provider = { - get_url = function(opts) return "https://api.example.com/chat" end, - get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end, - get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end, + get_url = function(opts) return 'https://api.example.com/chat' end, + get_headers = function() return { ['Authorization'] = 'Bearer ' .. api_key } end, + get_models = function() return { { id = 'gpt-4.1', name = 'GPT-4.1 model' } } end, prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, } @@ -436,7 +485,7 @@ Add custom AI providers: disabled?: boolean, -- Optional: Extra info about the provider displayed in info panel - get_info?(): string[] + get_info?(headers: table): string[] -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, @@ -452,13 +501,16 @@ Add custom AI providers: -- Optional: Get available models get_models?(headers: table): table, + + -- Optional: Resolve a user-facing model id to a provider model id + resolve_model?(headers: table, model: string): string, } < **Built-in providers:** - `copilot` - GitHub Copilot (default) -- `github_models` - GitHub Marketplace models (disabled by default) +- `github_models` - GitHub Models (disabled by default) ============================================================================== @@ -468,7 +520,7 @@ Add custom AI providers: CORE *CopilotChat-core* >lua - local chat = require("CopilotChat") + local chat = require('CopilotChat') -- Basic Chat Functions chat.ask(prompt, config) -- Ask a question with optional config @@ -499,7 +551,7 @@ CHAT WINDOW *CopilotChat-chat-window* You can also access the chat window UI methods through the `chat.chat` object: >lua - local window = require("CopilotChat").chat + local window = require('CopilotChat').chat -- Chat UI State window:visible() -- Check if chat window is visible @@ -518,8 +570,8 @@ You can also access the chat window UI methods through the `chat.chat` object: window:finish() -- Finish writing to chat window -- Source Management - window.get_source() -- Get the current source buffer and window - window.set_source(winnr) -- Set the source window + window:get_source() -- Get the current source buffer and window + window:set_source(winnr) -- Set the source window -- Navigation window:follow() -- Move cursor to end of chat content @@ -533,10 +585,11 @@ You can also access the chat window UI methods through the `chat.chat` object: PROMPT PARSER *CopilotChat-prompt-parser* >lua - local parser = require("CopilotChat.prompts") + local parser = require('CopilotChat.prompts') parser.resolve_prompt() -- Resolve prompt references - parser.resolve_tools() -- Resolve tools that are available for automatic use by LLM + parser.resolve_tools() -- Resolve tools shared with the model via @... + parser.resolve_functions() -- Resolve manual function/resource references via #... parser.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) < @@ -545,22 +598,26 @@ EXAMPLE USAGE *CopilotChat-example-usage* >lua -- Open chat, ask a question and handle response - require("CopilotChat").open() - require("CopilotChat").ask("#buffer Explain this code", { + require('CopilotChat').open() + require('CopilotChat').ask('#buffer Explain this code', { callback = function(response) - vim.notify("Got response: " .. response:sub(1, 50) .. "...") - return response + vim.notify('Got response: ' .. vim.trim(response.content):sub(1, 50) .. '...') end, }) -- Save and load chat history - require("CopilotChat").save("my_debugging_session") - require("CopilotChat").load("my_debugging_session") + require('CopilotChat').save('my_debugging_session') + require('CopilotChat').load('my_debugging_session') -- Use custom sticky and model - require("CopilotChat").ask("How can I optimize this?", { - model = "gpt-4.1", - sticky = {"#buffer", "#gitdiff:staged"} + require('CopilotChat').ask('How can I optimize this?', { + model = 'gpt-4.1', + sticky = { '#buffer', '#gitdiff:staged' }, + }) + + -- Automatically trust a small read-only tool set + require('CopilotChat').setup({ + trusted_tools = { 'file', 'glob', 'grep' }, }) < @@ -595,6 +652,12 @@ To run tests: make test < +To run the same formatting check as CI: + +>bash + stylua --check . +< + CONTRIBUTING *CopilotChat-contributing* From e73cb24a65da405675b5d9097388ee8c0b9e04ed Mon Sep 17 00:00:00 2001 From: Vladimir Kolchurin <18503099+kolchurinvv@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:39:40 +0200 Subject: [PATCH 710/728] fix(providers): no top_p n and temperature for -codex gpt models * providers - no top_p n and temperature for -codex gpt models * Update lua/CopilotChat/config/providers.lua * Update lua/CopilotChat/config/providers.lua --------- Co-authored-by: Tomas Slusny --- lua/CopilotChat/config/providers.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 548f6abd..fce20e3f 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -278,6 +278,7 @@ end ---@return table local function prepare_chat_input(inputs, opts) local is_o1 = vim.startswith(opts.model.id, 'o1') + local is_codex = opts.model.id:find('codex') ~= nil inputs = vim.tbl_map(function(input) local output = { @@ -324,7 +325,7 @@ local function prepare_chat_input(inputs, opts) end, opts.tools) end - if not is_o1 then + if not is_o1 and not is_codex then out.n = 1 out.top_p = 1 out.temperature = opts.temperature From ee142480a5f13226d1d2e6b9bff1642f0b175795 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:40:22 +0200 Subject: [PATCH 711/728] docs: add kolchurinvv as a contributor for code (#1557) * 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 84757b79..e502fd86 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -515,6 +515,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/270346599?v=4", "profile": "https://github.com/sirjls", "contributions": ["code"] + }, + { + "login": "kolchurinvv", + "name": "Vladimir Kolchurin", + "avatar_url": "https://avatars.githubusercontent.com/u/18503099?v=4", + "profile": "https://github.com/kolchurinvv", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b37e2cfe..d552f3af 100644 --- a/README.md +++ b/README.md @@ -683,6 +683,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d junqizhang
junqizhang

💻 Calum Lynch
Calum Lynch

💻 sirjls
sirjls

💻 + Vladimir Kolchurin
Vladimir Kolchurin

💻 From 3afd032553da5c70ee934e290db40793d138f180 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 16 Apr 2026 19:43:05 +0200 Subject: [PATCH 712/728] docs(readme): improve resource and tool usage docs (#1558) * docs(readme): improve resource and tool usage docs - Add tip for using to autocomplete resources and options - Clarify and expand resource examples for buffer, file, gitdiff, and url - Add section on tool usage with markdown examples - Update key mappings table and add pro tip for usage - Clarify tool trust configuration and recommend safe defaults - Improve descriptions and available options for predefined functions - General rewording and formatting for clarity and usability Closes #1530 Closes #1488 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 | 155 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 91 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index d552f3af..62364cf3 100644 --- a/README.md +++ b/README.md @@ -97,32 +97,8 @@ EOF - **Models** (`$`) - Specify which AI model to use for the chat - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -## Examples - -```markdown -# Add specific file to context - -#file:src/main.lua - -# Give LLM access to workspace tools - -@copilot What files are in this project? - -# Sticky prompt that persists - -> #buffer:active -> You are a helpful coding assistant -``` - -When you use `@copilot`, the LLM can call functions from the `copilot` group such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`. - -- By default, proposed tool calls wait for your approval. -- You can configure `trusted_tools` to automatically run specific tools or groups. -- Resources added with `#...` are resolved immediately and shared as context. -- Tool call results are sent back to the model as plain output, while manual resources keep their `##` references in chat. - -> [!WARNING] -> `trusted_tools = true` allows the model to run every enabled tool without asking. Only use it if you fully trust the tool set and workspace. +> [!TIP] +> Press `` after typing `#` or `@` to see available options and auto-complete. This is the easiest way to discover what's available! # Usage @@ -144,54 +120,54 @@ When you use `@copilot`, the LLM can call functions from the `copilot` group suc ## Chat Key Mappings -| Insert | Normal | Action | -| ------- | ------- | ----------------------------------------- | -| `` | - | Trigger/accept completion menu for tokens | -| `` | `q` | Close the chat window | -| `` | `` | Reset and clear the chat window | -| `` | `` | Submit the current prompt | -| `` | `` | Accept nearest diff | -| - | `gj` | Jump to section of nearest diff | -| - | `gqa` | Add all answers from chat to quickfix | -| - | `gqd` | Add all diffs from chat to quickfix | -| - | `gy` | Yank nearest diff to register | -| - | `gd` | Show diff between source and nearest diff | -| - | `gc` | Show info about current chat | -| - | `gh` | Show help message | - -> [!WARNING] -> Some plugins (e.g. `copilot.vim`) may also map common keys like `` in insert mode. -> To avoid conflicts, disable Copilot's default `` mapping with: +| Insert | Normal | Action | +| ------- | ------- | ---------------------------------------------------- | +| `` | - | **Autocomplete resources/files/options** (use this!) | +| `` | `q` | Close the chat window | +| `` | `` | Reset and clear the chat window | +| `` | `` | Submit the current prompt | +| `` | `` | Accept nearest diff | +| - | `gj` | Jump to section of nearest diff | +| - | `gqa` | Add all answers from chat to quickfix | +| - | `gqd` | Add all diffs from chat to quickfix | +| - | `gy` | Yank nearest diff to register | +| - | `gd` | Show diff between source and nearest diff | +| - | `gc` | Show info about current chat | +| - | `gh` | Show help message | + +**💡 Pro tip:** After typing `#`, `@`, `#buffer:`, or `#file:`, press `` to see available options. This is the fastest way to work! + +> [!NOTE] +> **Tab key not working?** Some plugins (e.g. `copilot.vim`) also map `` in insert mode. +> To fix conflicts, disable the other plugin's `` mapping: > > ```lua +> -- For copilot.vim > vim.g.copilot_no_tab_map = true > vim.keymap.set('i', '', 'copilot#Accept("\\")', { expr = true, replace_keycodes = false }) > ``` > -> You can also customize CopilotChat keymaps in your config. +> Or customize CopilotChat keymaps in your config. ## Predefined Functions All predefined functions belong to the `copilot` group. -| Function | Manual `#...` | Description | Example Usage | -| ----------- | ------------- | ------------------------------------------------------ | -------------------- | -| `bash` | No | Executes a bash command and returns output | `@copilot` | -| `buffer` | Yes | Retrieves content from buffer(s) with diagnostics | `#buffer:active` | -| `clipboard` | Yes | Provides access to system clipboard content | `#clipboard` | -| `edit` | No | Applies a unified diff to a file | `@copilot` | -| `file` | Yes | Reads content from a specified file path | `#file:path/to/file` | -| `gitdiff` | Yes | Retrieves git diff information | `#gitdiff:staged` | -| `glob` | Yes | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | -| `grep` | Yes | Searches for a pattern across files in workspace | `#grep:TODO` | -| `selection` | Yes | Includes the current visual selection with diagnostics | `#selection` | -| `url` | Yes | Fetches content from a specified URL | `#url:https://...` | - -`#...` resolves a function immediately and adds its output as chat context. - -`@copilot` shares the enabled functions with the model so it can choose when to call them. - -Only `bash` and `edit` are tool-only. The rest can be used both as manual resources and as callable tools. +| Function | Manual `#...` | Description | Available Options | +| ----------- | ------------- | ------------------------------------------------------ | --------------------------------------------------------------------- | +| `bash` | No | Executes a bash command and returns output | Tool-only (use `@copilot`) | +| `buffer` | Yes | Retrieves content from buffer(s) with diagnostics | `active`, `visible`, `listed`, `quickfix`, buffer number, or filename | +| `clipboard` | Yes | Provides access to system clipboard content | No options | +| `edit` | No | Applies a unified diff to a file | Tool-only (use `@copilot`) | +| `file` | Yes | Reads content from a specified file path | Any file path (use `` for completion) | +| `gitdiff` | Yes | Retrieves git diff information | `unstaged` (default), `staged`, or commit SHA | +| `glob` | Yes | Lists filenames matching a pattern in workspace | Any glob pattern (default: `**/*`) | +| `grep` | Yes | Searches for a pattern across files in workspace | Any search pattern | +| `selection` | Yes | Includes the current visual selection with diagnostics | No options | +| `url` | Yes | Fetches content from a specified URL | Any HTTPS URL | + +- **`#`** - Embeds output directly in your message (e.g., `#buffer:listed`, `#file:src/main.lua`) +- **`@`** - Makes function(s) available for LLM to call when needed (e.g., `@copilot`, `@file`) ## Predefined Prompts @@ -205,6 +181,55 @@ Only `bash` and `edit` are tool-only. The rest can be used both as manual resour | `Tests` | Generate tests for selected code | | `Commit` | Generate commit message with commitizen convention from staged changes | +## Resource Usage + +```markdown +# Current buffer + +#buffer:active + +# All open buffers (replaces old #buffers) + +#buffer:listed + +# All visible buffers + +#buffer:visible + +# Specific file + +#file:src/main.lua + +# Git changes + +#gitdiff:staged + +# URL content + +#url:https://example.com/docs +``` + +## Tool Usage + +When you use `@copilot`, the LLM can call functions from the `copilot` group such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`. + +```markdown +# Give LLM access to workspace tools + +@copilot What files are in this project? + +# Sticky context with tools + +> #buffer:listed +> @copilot +> Refactor the authentication code +``` + +By default, tool calls require manual approval. Configure `trusted_tools` to automatically run specific tools (see [Functions](#functions)). + +> [!WARNING] +> `trusted_tools = true` allows the model to run every enabled tool without asking. Only use it if you fully trust the tool set and workspace. + # Configuration For all available configuration options, see [`lua/CopilotChat/config.lua`](lua/CopilotChat/config.lua). @@ -333,6 +358,8 @@ Use `trusted_tools` to control which tool calls are executed automatically: } ``` +**How tool trust works:** + A tool is trusted when any of these match: - Its function definition sets `trusted = true` @@ -340,7 +367,7 @@ A tool is trusted when any of these match: - Its function group appears in `trusted_tools` - `trusted_tools = true` -For most setups, trusting a few read-only functions such as `file`, `glob`, or `grep` is safer than trusting everything. +**Recommended setup:** Trust read-only functions like `file`, `glob`, or `grep` for a smoother workflow without compromising safety. > [!WARNING] > Trusted tools run without asking for confirmation. Be especially careful with tools like `bash` and `edit`, which can change your workspace. From 997903d05c082b76588ef6364d7282360dc9ac0d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 17:43:29 +0000 Subject: [PATCH 713/728] chore(doc): auto generate docs --- doc/CopilotChat.txt | 181 ++++++++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 67 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 55ceca49..e80f9739 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -11,12 +11,13 @@ Table of Contents *CopilotChat-table-of-contents* - lazy.nvim |CopilotChat-lazy.nvim| - vim-plug |CopilotChat-vim-plug| 2. Core Concepts |CopilotChat-core-concepts| - - Examples |CopilotChat-examples| 3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Chat Key Mappings |CopilotChat-chat-key-mappings| - Predefined Functions |CopilotChat-predefined-functions| - Predefined Prompts |CopilotChat-predefined-prompts| + - Resource Usage |CopilotChat-resource-usage| + - Tool Usage |CopilotChat-tool-usage| 4. Configuration |CopilotChat-configuration| - Quick Setup |CopilotChat-quick-setup| - Window & Appearance |CopilotChat-window-&-appearance| @@ -132,34 +133,8 @@ VIM-PLUG *CopilotChat-vim-plug* - **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -EXAMPLES *CopilotChat-examples* - ->markdown - # Add specific file to context - - #file:src/main.lua - - # Give LLM access to workspace tools - - @copilot What files are in this project? - - # Sticky prompt that persists - - > #buffer:active - > You are a helpful coding assistant -< - -When you use `@copilot`, the LLM can call functions from the `copilot` group -such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`. - -- By default, proposed tool calls wait for your approval. -- You can configure `trusted_tools` to automatically run specific tools or groups. -- Resources added with `#...` are resolved immediately and shared as context. -- Tool call results are sent back to the model as plain output, while manual resources keep their `##` references in chat. - - - [!WARNING] `trusted_tools = true` allows the model to run every enabled tool - without asking. Only use it if you fully trust the tool set and workspace. + [!TIP] Press `` after typing `#` or `@` to see available options and + auto-complete. This is the easiest way to discover what’s available! ============================================================================== 3. Usage *CopilotChat-usage* @@ -183,72 +158,89 @@ COMMANDS *CopilotChat-commands* CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* + ------------------------------------------------------------------------- Insert Normal Action - -------- -------- ------------------------------------------- - - Trigger/accept completion menu for tokens + -------- -------- ------------------------------------------------------- + - Autocomplete resources/files/options (use this!) + q Close the chat window + Reset and clear the chat window + Submit the current prompt + Accept nearest diff + - gj Jump to section of nearest diff + - gqa Add all answers from chat to quickfix + - gqd Add all diffs from chat to quickfix + - gy Yank nearest diff to register + - gd Show diff between source and nearest diff + - gc Show info about current chat + - gh Show help message + ------------------------------------------------------------------------- +**💡 Pro tip:** After typing `#`, `@`, `#buffer:`, or `#file:`, press `` +to see available options. This is the fastest way to work! + - [!WARNING] Some plugins (e.g. `copilot.vim`) may also map common keys like - `` in insert mode. To avoid conflicts, disable Copilot’s default `` - mapping with: + [!NOTE] **Tab key not working?** Some plugins (e.g. `copilot.vim`) also map + `` in insert mode. To fix conflicts, disable the other plugin’s `` + mapping: >lua + -- For copilot.vim vim.g.copilot_no_tab_map = true vim.keymap.set('i', '', 'copilot#Accept("\\")', { expr = true, replace_keycodes = false }) < - You can also customize CopilotChat keymaps in your config. + Or customize CopilotChat keymaps in your config. PREDEFINED FUNCTIONS *CopilotChat-predefined-functions* All predefined functions belong to the `copilot` group. - ---------------------------------------------------------------------------------- - Function Manual Description Example Usage - #... - ----------- --------- --------------------------------------- -------------------- - bash No Executes a bash command and returns @copilot - output - - buffer Yes Retrieves content from buffer(s) with #buffer:active - diagnostics - - clipboard Yes Provides access to system clipboard #clipboard - content + --------------------------------------------------------------------------------- + Function Manual Description Available Options + #... + ----------- -------- -------------------------- --------------------------------- + bash No Executes a bash command Tool-only (use @copilot) + and returns output - edit No Applies a unified diff to a file @copilot + buffer Yes Retrieves content from active, visible, listed, + buffer(s) with diagnostics quickfix, buffer number, or + filename - file Yes Reads content from a specified file #file:path/to/file - path + clipboard Yes Provides access to system No options + clipboard content - gitdiff Yes Retrieves git diff information #gitdiff:staged + edit No Applies a unified diff to Tool-only (use @copilot) + a file - glob Yes Lists filenames matching a pattern in #glob:**/*.lua - workspace + file Yes Reads content from a Any file path (use for + specified file path completion) - grep Yes Searches for a pattern across files in #grep:TODO - workspace + gitdiff Yes Retrieves git diff unstaged (default), staged, or + information commit SHA - selection Yes Includes the current visual selection #selection - with diagnostics + glob Yes Lists filenames matching a Any glob pattern (default: **/*) + pattern in workspace - url Yes Fetches content from a specified URL #url:https://... - ---------------------------------------------------------------------------------- -`#...` resolves a function immediately and adds its output as chat context. + grep Yes Searches for a pattern Any search pattern + across files in workspace -`@copilot` shares the enabled functions with the model so it can choose when to -call them. + selection Yes Includes the current No options + visual selection with + diagnostics -Only `bash` and `edit` are tool-only. The rest can be used both as manual -resources and as callable tools. + url Yes Fetches content from a Any HTTPS URL + specified URL + --------------------------------------------------------------------------------- +- **#** - Embeds output directly in your message (e.g., `#buffer:listed`, `#file:src/main.lua`) +- **@** - Makes function(s) available for LLM to call when needed (e.g., `@copilot`, `@file`) PREDEFINED PROMPTS *CopilotChat-predefined-prompts* @@ -272,6 +264,59 @@ PREDEFINED PROMPTS *CopilotChat-predefined-prompts* changes ------------------------------------------------------------------------- +RESOURCE USAGE *CopilotChat-resource-usage* + +>markdown + # Current buffer + + #buffer:active + + # All open buffers (replaces old #buffers) + + #buffer:listed + + # All visible buffers + + #buffer:visible + + # Specific file + + #file:src/main.lua + + # Git changes + + #gitdiff:staged + + # URL content + + #url:https://example.com/docs +< + + +TOOL USAGE *CopilotChat-tool-usage* + +When you use `@copilot`, the LLM can call functions from the `copilot` group +such as `bash`, `edit`, `file`, `glob`, `grep`, and `gitdiff`. + +>markdown + # Give LLM access to workspace tools + + @copilot What files are in this project? + + # Sticky context with tools + + > #buffer:listed + > @copilot + > Refactor the authentication code +< + +By default, tool calls require manual approval. Configure `trusted_tools` to +automatically run specific tools (see |CopilotChat-functions|). + + + [!WARNING] `trusted_tools = true` allows the model to run every enabled tool + without asking. Only use it if you fully trust the tool set and workspace. + ============================================================================== 4. Configuration *CopilotChat-configuration* @@ -408,6 +453,8 @@ Use `trusted_tools` to control which tool calls are executed automatically: } < +**How tool trust works:** + A tool is trusted when any of these match: - Its function definition sets `trusted = true` @@ -415,8 +462,8 @@ A tool is trusted when any of these match: - Its function group appears in `trusted_tools` - `trusted_tools = true` -For most setups, trusting a few read-only functions such as `file`, `glob`, or -`grep` is safer than trusting everything. +**Recommended setup:** Trust read-only functions like `file`, `glob`, or `grep` +for a smoother workflow without compromising safety. [!WARNING] Trusted tools run without asking for confirmation. Be especially @@ -676,7 +723,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻Vladimir Kolchurin💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 5378035c2eb4089de245473dec1768e0986543bd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 16 Apr 2026 21:30:52 +0200 Subject: [PATCH 714/728] refactor(utils): move notify module to utils directory (#1559) Relocate the notify module from the root CopilotChat directory to lua/CopilotChat/utils/notify.lua for better organization and consistency. Update all require statements to use the new path. No functional changes; this is a structural refactor to improve codebase maintainability. Signed-off-by: Tomas Slusny --- CONTRIBUTING.md | 17 ++++++----------- lua/CopilotChat/client.lua | 2 +- lua/CopilotChat/config/providers.lua | 2 +- lua/CopilotChat/init.lua | 2 +- lua/CopilotChat/prompts.lua | 2 +- lua/CopilotChat/tiktoken.lua | 2 +- lua/CopilotChat/ui/chat.lua | 2 +- lua/CopilotChat/ui/spinner.lua | 2 +- lua/CopilotChat/{ => utils}/notify.lua | 0 tests/notify_spec.lua | 2 +- 10 files changed, 14 insertions(+), 19 deletions(-) rename lua/CopilotChat/{ => utils}/notify.lua (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fe83edc..13013e9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,22 +111,17 @@ Go to the CopilotChat.nvim in your GitHub account, select your branch, and click - [utils.lua](/lua/CopilotChat/utils.lua): General utility functions. -- [utils/](/lua/CopilotChat/utils/): Utility modules — +- [utils/](/lua/CopilotChat/utils/): Utility modules [class.lua](/lua/CopilotChat/utils/class.lua) (OOP helper), [curl.lua](/lua/CopilotChat/utils/curl.lua) (HTTP requests), - [diff.lua](/lua/CopilotChat/utils/diff.lua) (unified diff parsing and - application), - [files.lua](/lua/CopilotChat/utils/files.lua) (file I/O and filetype - detection), - [orderedmap.lua](/lua/CopilotChat/utils/orderedmap.lua) (insertion-ordered - map), - [stringbuffer.lua](/lua/CopilotChat/utils/stringbuffer.lua) (efficient string - concatenation). + [diff.lua](/lua/CopilotChat/utils/diff.lua) (unified diff parsing and application), + [files.lua](/lua/CopilotChat/utils/files.lua) (file I/O and filetype detection), + [notify.lua](/lua/CopilotChat/utils/notify.lua) (pub/sub notification system for status and message events) + [orderedmap.lua](/lua/CopilotChat/utils/orderedmap.lua) (insertion-ordered map), + [stringbuffer.lua](/lua/CopilotChat/utils/stringbuffer.lua) (efficient string concatenation). ### Other - [health.lua](/lua/CopilotChat/health.lua): `:checkhealth` integration. Verifies commands, libraries, and Treesitter parsers. -- [notify.lua](/lua/CopilotChat/notify.lua): Pub/sub notification system for - status and message events. diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 585e01aa..7bbc65d8 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -55,7 +55,7 @@ local log = require('plenary.log') local constants = require('CopilotChat.constants') -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index fce20e3f..44233f3d 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,7 +1,7 @@ local log = require('plenary.log') local plenary_utils = require('plenary.async.util') local constants = require('CopilotChat.constants') -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') local files = require('CopilotChat.utils.files') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f6a2cc3b..e4dfdc3a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -816,7 +816,7 @@ function M.setup(config) end) -- Initialize chat - require('CopilotChat.notify').clear() + require('CopilotChat.utils.notify').clear() if M.chat then M.chat:close() M.chat:delete() diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index f5c76525..7c4e60ce 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -1,7 +1,7 @@ local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') local functions = require('CopilotChat.functions') -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') local files = require('CopilotChat.utils.files') local orderedmap = require('CopilotChat.utils.orderedmap') local utils = require('CopilotChat.utils') diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index abe1ce1d..f7ea0de7 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,4 +1,4 @@ -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') local utils = require('CopilotChat.utils') local curl = require('CopilotChat.utils.curl') local class = require('CopilotChat.utils.class') diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index f20d08b3..b77c290b 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -1,7 +1,7 @@ local Overlay = require('CopilotChat.ui.overlay') local Spinner = require('CopilotChat.ui.spinner') local constants = require('CopilotChat.constants') -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') local utils = require('CopilotChat.utils') local class = require('CopilotChat.utils.class') local orderedmap = require('CopilotChat.utils.orderedmap') diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index 44c77b80..06091a16 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -1,4 +1,4 @@ -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') local utils = require('CopilotChat.utils') local class = require('CopilotChat.utils.class') diff --git a/lua/CopilotChat/notify.lua b/lua/CopilotChat/utils/notify.lua similarity index 100% rename from lua/CopilotChat/notify.lua rename to lua/CopilotChat/utils/notify.lua diff --git a/tests/notify_spec.lua b/tests/notify_spec.lua index f0064056..020c9391 100644 --- a/tests/notify_spec.lua +++ b/tests/notify_spec.lua @@ -1,4 +1,4 @@ -local notify = require('CopilotChat.notify') +local notify = require('CopilotChat.utils.notify') describe('CopilotChat.notify', function() before_each(function() From 6fe5dfc0959639361f85df9f51105d114a456c5d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 26 Apr 2026 18:33:35 +0200 Subject: [PATCH 715/728] chore: fix number types in lua docs (#1561) * chore: fix number types in lua docs 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> --- CONTRIBUTING.md | 1 - lua/CopilotChat/select.lua | 18 +++++++++--------- lua/CopilotChat/ui/chat.lua | 4 ++-- lua/CopilotChat/utils/diff.lua | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13013e9b..393f93c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,4 +124,3 @@ Go to the CopilotChat.nvim in your GitHub account, select your branch, and click - [health.lua](/lua/CopilotChat/health.lua): `:checkhealth` integration. Verifies commands, libraries, and Treesitter parsers. - diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 425bf2a5..84722e9d 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,10 +1,10 @@ ---@class CopilotChat.select.Selection ---@field content string ----@field start_line number ----@field end_line number +---@field start_line integer +---@field end_line integer ---@field filename string ---@field filetype string ----@field bufnr number +---@field bufnr integer local log = require('plenary.log') local utils = require('CopilotChat.utils') @@ -51,7 +51,7 @@ function M.marks() end --- Highlight selection in target buffer or clear it ----@param bufnr number +---@param bufnr integer ---@param clear boolean? function M.highlight(bufnr, clear) local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') @@ -76,7 +76,7 @@ function M.highlight(bufnr, clear) end --- Get the selection from the target buffer ----@param bufnr number +---@param bufnr integer ---@return CopilotChat.select.Selection? function M.get(bufnr) if not utils.buf_valid(bufnr) then @@ -113,10 +113,10 @@ function M.get(bufnr) end --- Sets the selection to specific lines in buffer or clears it ----@param bufnr number ----@param winnr number? ----@param start_line number? ----@param end_line number? +---@param bufnr integer +---@param winnr integer? +---@param start_line integer? +---@param end_line integer? function M.set(bufnr, winnr, start_line, end_line) if not utils.buf_valid(bufnr) then return diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index b77c290b..7f14475e 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -108,8 +108,8 @@ end ---@field section CopilotChat.ui.chat.Section? --- @class CopilotChat.ui.chat.Source ---- @field bufnr number? ---- @field winnr number? +--- @field bufnr integer? +--- @field winnr integer? --- @field cwd fun():string ---@class CopilotChat.ui.chat.Chat : CopilotChat.ui.overlay.Overlay diff --git a/lua/CopilotChat/utils/diff.lua b/lua/CopilotChat/utils/diff.lua index 23199930..6a2384a6 100644 --- a/lua/CopilotChat/utils/diff.lua +++ b/lua/CopilotChat/utils/diff.lua @@ -138,7 +138,7 @@ end --- Apply unified diff to a table of lines and return new lines ---@param diff_text string ---@param original_content string ----@return table, boolean, integer, integer +---@return string[], boolean, integer?, integer? function M.apply_unified_diff(diff_text, original_content) local hunks = parse_hunks(diff_text) local new_content = original_content From 137d3bc527518f5ea982c43c43084496732365c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Apr 2026 16:33:54 +0000 Subject: [PATCH 716/728] 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 e80f9739..ec5ad985 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 April 16 + For NVIM v0.8.0 Last change: 2026 April 26 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 7cdc9c1a3fb85954852008a5a5c103394e0ed502 Mon Sep 17 00:00:00 2001 From: Mihamina Rakotomandimby Date: Tue, 26 May 2026 02:04:40 +0300 Subject: [PATCH 717/728] Display copilot multipliers in model list (#1567) * debug: temporary log file to make sure how is the response * feat(providers): add model billing multiplier support Expose the 'multiplier' field from model billing configuration into the provider definitions. This change allows the plugin to track and display usage cost multipliers for specific LLM models. Changes include: - Updating providers.lua to pull 'multiplier' from billing data. - Updating init.lua to propagate the multiplier to the model selection UI. - Adding a visual indicator in the model picker to show the multiplier. Notable behavior change: The model selection menu will now display billing multipliers, such as '[x2]', next to supported models. Testing: Verify the model selector displays the multiplier correctly when a model has billing information defined: :lua print(vim.inspect(require('CopilotChat.config.providers').copilot)) Check that models without billing info remain unaffected. * feat(model): add fallback indicator for missing multipliers Update the model selection interface to explicitly display 'x not specified' when a model multiplier is undefined. Previously, missing multipliers resulted in no indicator being shown. This change ensures visual consistency in the status line or UI components that list model configurations. Notable change: - The string 'x not specified' will now appear in the indicators table when a multiplier is nil. Testing: - Open the model selection menu and verify that models without a defined multiplier now show the fallback text instead of being empty. * refactor(config): remove debug logging and clean up UI helpers Refactored the provider and UI logic for model selection. What changed: - Removed the log.info call for Copilot models in providers.lua as it added unnecessary noise to the logs. - Simplified the 'select_model' logic in init.lua by removing the conditional fallback for missing multipliers. Notable behavior changes: - Models without a multiplier will no longer show 'x not specified' in the picker list. Testing: - Check that the model picker still renders correctly. - Verify that logs are clean during model fetching. * [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/config/providers.lua | 1 + lua/CopilotChat/init.lua | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 44233f3d..ea5eea71 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -655,6 +655,7 @@ M.copilot = { tools = model.capabilities.supports.tool_calls, policy = not model['policy'] or model['policy']['state'] == 'enabled', version = model.version, + multiplier = model.billing and model.billing.multiplier or nil, use_responses = use_responses, -- Carry the base URL into the model so get_url and resolve_model -- can use it without needing access to the headers again. diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e4dfdc3a..feba3754 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -343,6 +343,7 @@ function M.select_model() streaming = model.streaming, tools = model.tools, reasoning = model.reasoning, + multiplier = model.multiplier, selected = model.id == M.config.model, } end, models) @@ -358,6 +359,9 @@ function M.select_model() out = '* ' .. out end + if item.multiplier ~= nil then + table.insert(indicators, 'x' .. tostring(item.multiplier)) + end if item.provider then table.insert(indicators, item.provider) end From 2db7b404110f92e6d9197fee9cb9a708ae205a10 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 May 2026 23:05:03 +0000 Subject: [PATCH 718/728] 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 ec5ad985..3c4d451a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 April 26 + For NVIM v0.8.0 Last change: 2026 May 25 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 84a3968442c00257251bad7d593d84faace95fb1 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:49:24 -0400 Subject: [PATCH 719/728] fix: use gpt-5-mini as default model (#1568) Update default model from 'gpt-4.1' to 'gpt-5-mini' across the repo. Modified README, generated docs, provider example, and the main config (luas/CopilotChat/config.lua) so examples and defaults reflect the new model. This ensures the plugin and docs use the newer model by default. Co-authored-by: Michael Rosenfeld --- README.md | 8 ++++---- doc/CopilotChat.txt | 8 ++++---- lua/CopilotChat/config.lua | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 62364cf3..1f7c884a 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ Most users only need to configure a few options: ```lua { - model = 'gpt-4.1', -- AI model to use + model = 'gpt-5-mini', -- AI model to use temperature = 0.1, -- Lower = focused, higher = creative trusted_tools = nil, -- Require approval for all tool calls window = { @@ -311,7 +311,7 @@ Types of copilot highlights: - `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) - `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`) - `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) -- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) +- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-5-mini`) - `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) - `CopilotChatAnnotationHeader` - Annotation header highlight in chat buffer @@ -418,7 +418,7 @@ Add custom AI providers: my_provider = { get_url = function(opts) return 'https://api.example.com/chat' end, get_headers = function() return { ['Authorization'] = 'Bearer ' .. api_key } end, - get_models = function() return { { id = 'gpt-4.1', name = 'GPT-4.1 model' } } end, + get_models = function() return { { id = 'gpt-5-mini', name = 'GPT-5 mini model' } } end, prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, } @@ -554,7 +554,7 @@ require('CopilotChat').load('my_debugging_session') -- Use custom sticky and model require('CopilotChat').ask('How can I optimize this?', { - model = 'gpt-4.1', + model = 'gpt-5-mini', sticky = { '#buffer', '#gitdiff:staged' }, }) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3c4d451a..38754722 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -330,7 +330,7 @@ Most users only need to configure a few options: >lua { - model = 'gpt-4.1', -- AI model to use + model = 'gpt-5-mini', -- AI model to use temperature = 0.1, -- Lower = focused, higher = creative trusted_tools = nil, -- Require approval for all tool calls window = { @@ -404,7 +404,7 @@ Types of copilot highlights: - `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`) - `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`) - `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`) -- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`) +- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-5-mini`) - `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`) - `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) - `CopilotChatAnnotationHeader` - Annotation header highlight in chat buffer @@ -516,7 +516,7 @@ Add custom AI providers: my_provider = { get_url = function(opts) return 'https://api.example.com/chat' end, get_headers = function() return { ['Authorization'] = 'Bearer ' .. api_key } end, - get_models = function() return { { id = 'gpt-4.1', name = 'GPT-4.1 model' } } end, + get_models = function() return { { id = 'gpt-5-mini', name = 'GPT-5 mini model' } } end, prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, } @@ -658,7 +658,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* -- Use custom sticky and model require('CopilotChat').ask('How can I optimize this?', { - model = 'gpt-4.1', + model = 'gpt-5-mini', sticky = { '#buffer', '#gitdiff:staged' }, }) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index a54a66f3..96c584f3 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -61,7 +61,7 @@ return { system_prompt = require('CopilotChat.config.prompts').COPILOT_INSTRUCTIONS.system_prompt, -- System prompt to use (can be specified manually in prompt via /). - model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). + model = 'gpt-5-mini', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). resources = 'selection', -- Default resources to share with LLM (can be specified manually in prompt via #). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). From e407da3726bcaa96e7a6316e50f6c3fb2d4feb9f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Jun 2026 13:49:52 +0000 Subject: [PATCH 720/728] 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 38754722..f8826145 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 May 25 + For NVIM v0.8.0 Last change: 2026 June 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 2caf36bedcbb1efb507995c55833683095be22a7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:52:09 +0200 Subject: [PATCH 721/728] docs: add RoseSecurity as a contributor for doc, and code (#1569) * 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 e502fd86..cd8c81e2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -522,6 +522,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/18503099?v=4", "profile": "https://github.com/kolchurinvv", "contributions": ["code"] + }, + { + "login": "RoseSecurity", + "name": "RoseSecurity", + "avatar_url": "https://avatars.githubusercontent.com/u/72598486?v=4", + "profile": "https://rosesecurity.dev", + "contributions": ["doc", "code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1f7c884a..324b2acb 100644 --- a/README.md +++ b/README.md @@ -711,6 +711,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Calum Lynch
Calum Lynch

💻 sirjls
sirjls

💻 Vladimir Kolchurin
Vladimir Kolchurin

💻 + RoseSecurity
RoseSecurity

📖 💻 From 702c6e4091fcb8590e36d79e31248066f36557cc Mon Sep 17 00:00:00 2001 From: Abhraneel Mukherjee <121384410+abhr-0@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:46:11 +0530 Subject: [PATCH 722/728] fix(chat): render the chat ui on `InsertEnter` (#1570) --- lua/CopilotChat/ui/chat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 7f14475e..dba7bfbd 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -610,7 +610,7 @@ function Chat:create() end end) - vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { + vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertEnter', 'InsertLeave' }, { buffer = bufnr, callback = function() utils.debounce('chat-parse-' .. bufnr, function() From e0f8a52dca70d948a316c8e93c098f8f87707711 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Jun 2026 07:16:36 +0000 Subject: [PATCH 723/728] 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 f8826145..755adf18 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 June 01 + For NVIM v0.8.0 Last change: 2026 June 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -723,7 +723,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻Vladimir Kolchurin💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻Vladimir Kolchurin💻RoseSecurity📖 💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 2025f92022bef434de0511c5157a19974a9377a4 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:17:21 +0200 Subject: [PATCH 724/728] docs: add abhr-0 as a contributor for code (#1571) * 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 cd8c81e2..04915e9e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -529,6 +529,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/72598486?v=4", "profile": "https://rosesecurity.dev", "contributions": ["doc", "code"] + }, + { + "login": "abhr-0", + "name": "Abhraneel Mukherjee", + "avatar_url": "https://avatars.githubusercontent.com/u/121384410?v=4", + "profile": "https://github.com/abhr-0", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 324b2acb..434cae00 100644 --- a/README.md +++ b/README.md @@ -712,6 +712,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d sirjls
sirjls

💻 Vladimir Kolchurin
Vladimir Kolchurin

💻 RoseSecurity
RoseSecurity

📖 💻 + Abhraneel Mukherjee
Abhraneel Mukherjee

💻 From 33d1bbed8659a45cafa5a9dd8df31c6e9294b87d Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:53:06 +0000 Subject: [PATCH 725/728] Add `pullfrog.yml` workflow --- .github/workflows/pullfrog.yml | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/pullfrog.yml diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml new file mode 100644 index 00000000..6516ae2e --- /dev/null +++ b/.github/workflows/pullfrog.yml @@ -0,0 +1,57 @@ +# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED +name: Pullfrog +run-name: ${{ inputs.name || github.workflow }} +on: + workflow_dispatch: + inputs: + prompt: + type: string + description: Agent prompt + name: + type: string + description: Run name + +permissions: + contents: read + +jobs: + pullfrog: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 1 + - name: Run agent + uses: pullfrog/pullfrog@v0 + with: + prompt: ${{ inputs.prompt }} + env: + # add at least one provider API key + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GOOGLE_GENERATIVE_AI_API_KEY: + ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + XAI_API_KEY: ${{ secrets.XAI_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + + # for Amazon Bedrock (https://docs.pullfrog.com/bedrock) + # AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} + # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # AWS_REGION: us-east-1 + # BEDROCK_MODEL_ID: + + # for Google Vertex AI (https://docs.pullfrog.com/vertex) + # VERTEX_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_SERVICE_ACCOUNT_JSON }} + # GOOGLE_CLOUD_PROJECT: my-project + # VERTEX_LOCATION: global + # VERTEX_MODEL_ID: From ab5cf12ea8b8c8af82feb616bf7fbac096edc0e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Jun 2026 15:53:36 +0000 Subject: [PATCH 726/728] 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 755adf18..e3c65b58 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 June 02 + For NVIM v0.8.0 Last change: 2026 June 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -723,7 +723,7 @@ See CONTRIBUTING.md for detailed guidelines. 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💻Léopold Mebazaa💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻Vladimir Kolchurin💻RoseSecurity📖 💻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💻JunKi Jin💻abdennourzahaf📖Josiah💻Tony Fischer💻 📖Kohei Wada💻Sebastian Yaghoubi📖johncming💻Rokas Brazdžionis💻Sola📖 💻Mani Chandra💻Nischal Basuti📖Teo Ljungberg💻Joe Price💻Yufan You📖 💻Manish Kumar💻Anton Ždanov📖 💻Fredrik Averpil💻Aaron D Borden💻Md. Iftakhar Awal Chowdhury💻 📖Danilo Horta💻Mihamina Rakotomandimby📖 💻Ajmal S💻Samiul Islam💻Rui Costa💻CTCHEN💻Tobias Wölfel💻Alexander Garcia💻Max Kharandziuk💻Xinyu Xiang💻junqizhang💻Calum Lynch💻sirjls💻Vladimir Kolchurin💻RoseSecurity📖 💻Abhraneel Mukherjee💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 317955e3388a69cdb723cf6cd6ec80bff2614b93 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:12:06 +0800 Subject: [PATCH 727/728] [pre-commit.ci] pre-commit autoupdate (#1565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/JohnnyMorganz/StyLua: v2.4.1 → v2.5.2](https://github.com/JohnnyMorganz/StyLua/compare/v2.4.1...v2.5.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 ae6b39ed..283f1965 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: v2.4.1 + rev: v2.5.2 hooks: - id: stylua-github From f0c2a23ab79dc295f5e0eef0d8b587a39020c852 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jun 2026 23:12:23 +0000 Subject: [PATCH 728/728] 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 e3c65b58..fda50bbd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,5 +1,5 @@ *CopilotChat.txt* - For NVIM v0.8.0 Last change: 2026 June 16 + For NVIM v0.8.0 Last change: 2026 June 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents*