From 081d4c20242140bb185ebee142a65454ad375f7d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 12 Aug 2025 16:04:22 +0200 Subject: [PATCH 01/59] 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 02/59] 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 03/59] 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 04/59] 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 05/59] 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 06/59] 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 07/59] 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 08/59] 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 09/59] 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 10/59] 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 11/59] 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 12/59] 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 13/59] 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 14/59] 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 15/59] 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 16/59] 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 17/59] 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 18/59] 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 19/59] 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 20/59] 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 21/59] 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 22/59] 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 23/59] 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 24/59] 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 25/59] 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 26/59] 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 27/59] 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 28/59] 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 29/59] 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 30/59] 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 31/59] 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 32/59] 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 33/59] 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 34/59] 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 35/59] 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 36/59] 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 37/59] 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 38/59] 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 39/59] 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 40/59] 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 41/59] 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 42/59] 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 43/59] 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 44/59] 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 45/59] 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 46/59] 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 47/59] 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 48/59] 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 49/59] 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 50/59] 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 51/59] 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 52/59] 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 53/59] 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 54/59] 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 55/59] 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 56/59] 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 57/59] 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 58/59] 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 59/59] 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