From f378a6eb7d6649454e750b90d76f52a435a5a0e1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 11:45:04 +0100 Subject: [PATCH] Add support for #files (workspace file map) context Support workspace file map context. This context will simply include all related filenames to the prompt in chat context. Useful for searching where something might be (but limited to only searching on filenames for now). Signed-off-by: Tomas Slusny --- README.md | 3 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/context.lua | 82 ++++++++++++++++++----- lua/CopilotChat/copilot.lua | 4 +- lua/CopilotChat/debuginfo.lua | 122 ++++++++++++++++++---------------- lua/CopilotChat/health.lua | 4 ++ lua/CopilotChat/init.lua | 47 ++++++------- lua/CopilotChat/utils.lua | 6 -- 8 files changed, 160 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 65844fb4..6236d3c4 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Supported contexts are: - `buffers` - Includes all open buffers in chat context - `buffer` - Includes only the current buffer in chat context +- `files` - Includes all non-hidden filenames in the current workspace in chat context ### API @@ -237,7 +238,7 @@ Also see [here](/lua/CopilotChat/config.lua): system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b51a9ef8..35715c90 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -97,7 +97,7 @@ return { system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index ac5d5d94..05f530fe 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -1,3 +1,4 @@ +local async = require('plenary.async') local log = require('plenary.log') local M = {} @@ -72,6 +73,71 @@ local function data_ranked_by_relatedness(query, data, top_n) return result end +local get_context_data = async.wrap(function(context, bufnr, callback) + vim.schedule(function() + local outline = {} + if context == 'buffers' then + outline = vim.tbl_map( + M.build_outline, + vim.tbl_filter(function(b) + return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 + end, vim.api.nvim_list_bufs()) + ) + elseif context == 'buffer' then + table.insert(outline, M.build_outline(bufnr)) + elseif context == 'files' then + outline = M.build_file_map() + end + + callback(outline) + end) +end, 3) + +--- Get supported contexts +---@return table +function M.supported_contexts() + return { + buffers = 'Includes all open buffers in chat context', + buffer = 'Includes only the current buffer in chat context', + files = 'Includes all non-hidden filenames in the current workspace in chat context', + } +end + +--- Get list of all files in workspace +---@return table +function M.build_file_map() + -- Use vim.fn.glob() to get all files + local files = vim.fn.glob('**/*', false, true) + + -- Filter out directories + local files = vim.tbl_filter(function(file) + return vim.fn.isdirectory(file) == 0 + end, files) + + if #files == 0 then + return {} + end + + local out = {} + + -- Create embeddings in chunks + local chunk_size = 100 + for i = 1, #files, chunk_size do + local chunk = {} + for j = i, math.min(i + chunk_size - 1, #files) do + table.insert(chunk, files[j]) + end + + table.insert(out, { + content = table.concat(chunk, '\n'), + filename = 'file_map', + filetype = 'text', + }) + end + + return out +end + --- Build an outline for a buffer --- FIXME: Handle multiline function argument definitions when building the outline ---@param bufnr number @@ -198,21 +264,7 @@ function M.find_for_query(copilot, opts) local filetype = opts.filetype local bufnr = opts.bufnr - local outline = {} - if context == 'buffers' then - -- For multiple buffers, only make outlines - outline = vim.tbl_map( - function(b) - return M.build_outline(b) - end, - vim.tbl_filter(function(b) - return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 - end, vim.api.nvim_list_bufs()) - ) - elseif context == 'buffer' then - table.insert(outline, M.build_outline(bufnr)) - end - + local outline = get_context_data(context, bufnr) outline = vim.tbl_filter(function(item) return item ~= nil end, outline) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index dca4f39d..be1c5df2 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -323,8 +323,8 @@ local function generate_embedding_request(inputs, model) if input.content then out = out .. string.format( - 'File: `%s`\n```%s\n%s\n```', - input.filename, + '# FILE:%s CONTEXT\n```%s\n%s\n```', + input.filename:upper(), input.filetype, input.content ) diff --git a/lua/CopilotChat/debuginfo.lua b/lua/CopilotChat/debuginfo.lua index d17bd756..08d41d88 100644 --- a/lua/CopilotChat/debuginfo.lua +++ b/lua/CopilotChat/debuginfo.lua @@ -1,75 +1,81 @@ +local log = require('plenary.log') local utils = require('CopilotChat.utils') local context = require('CopilotChat.context') local M = {} -function M.setup() - -- Show debug info - vim.api.nvim_create_user_command('CopilotChatDebugInfo', function() - -- Get the log file path - local log_file_path = utils.get_log_file_path() +function M.open() + local lines = { + 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', + '', + 'Log file path:', + '`' .. log.logfile .. '`', + '', + 'Temp directory:', + '`' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`', + '', + 'Data directory:', + '`' .. vim.fn.stdpath('data') .. '`', + '', + } - -- Create a popup with the log file path - local lines = { - 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', - '', - 'Log file path:', - '`' .. log_file_path .. '`', - '', - } + local outline = context.build_outline(vim.api.nvim_get_current_buf()) + if outline then + table.insert(lines, 'Current buffer outline:') + table.insert(lines, '`' .. outline.filename .. '`') + table.insert(lines, '```' .. outline.filetype) + local outline_lines = vim.split(outline.content, '\n') + for _, line in ipairs(outline_lines) do + table.insert(lines, line) + end + table.insert(lines, '```') + end - local outline = context.build_outline(vim.api.nvim_get_current_buf()) - if outline then - table.insert(lines, 'Current buffer outline:') - table.insert(lines, '`' .. outline.filename .. '`') - table.insert(lines, '```' .. outline.filetype) - local outline_lines = vim.split(outline.content, '\n') - for _, line in ipairs(outline_lines) do + local files = context.build_file_map() + if files then + table.insert(lines, 'Current workspace file map:') + table.insert(lines, '```text') + for _, file in ipairs(files) do + for _, line in ipairs(vim.split(file.content, '\n')) do table.insert(lines, line) end - table.insert(lines, '```') end + table.insert(lines, '```') + end - local width = 0 - for _, line in ipairs(lines) do - width = math.max(width, #line) - end - local height = math.min(vim.o.lines - 3, #lines) - local opts = { - title = 'CopilotChat.nvim Debug Info', - relative = 'editor', - width = width, - height = height, - row = (vim.o.lines - height) / 2 - 1, - col = (vim.o.columns - width) / 2, - style = 'minimal', - border = 'rounded', - } + local width = 0 + for _, line in ipairs(lines) do + width = math.max(width, #line) + end + local height = math.min(vim.o.lines - 3, #lines) + local opts = { + title = 'CopilotChat.nvim Debug Info', + relative = 'editor', + width = width, + height = height, + row = (vim.o.lines - height) / 2 - 1, + col = (vim.o.columns - width) / 2, + style = 'minimal', + border = 'rounded', + } - if not utils.is_stable() then - opts.footer = "Press 'q' to close this window." - end + if not utils.is_stable() then + opts.footer = "Press 'q' to close this window." + end - local bufnr = vim.api.nvim_create_buf(false, true) - vim.bo[bufnr].syntax = 'markdown' - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - vim.bo[bufnr].modifiable = false - vim.treesitter.start(bufnr, 'markdown') + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].syntax = 'markdown' + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.bo[bufnr].modifiable = false + vim.treesitter.start(bufnr, 'markdown') - local win = vim.api.nvim_open_win(bufnr, true, opts) - vim.wo[win].wrap = true - vim.wo[win].linebreak = true - vim.wo[win].cursorline = true - vim.wo[win].conceallevel = 2 + local win = vim.api.nvim_open_win(bufnr, true, opts) + vim.wo[win].wrap = true + vim.wo[win].linebreak = true + vim.wo[win].cursorline = true + vim.wo[win].conceallevel = 2 - -- Bind 'q' to close the window - vim.api.nvim_buf_set_keymap( - bufnr, - 'n', - 'q', - 'close', - { noremap = true, silent = true } - ) - end, { nargs = '*', range = true }) + -- Bind 'q' to close the window + vim.api.nvim_buf_set_keymap(bufnr, 'n', 'q', 'close', { noremap = true, silent = true }) end return M diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 84f4ae55..b6f7ebb0 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -4,6 +4,7 @@ local start = vim.health.start or vim.health.report_start local error = vim.health.error or vim.health.report_error local warn = vim.health.warn or vim.health.report_warn local ok = vim.health.ok or vim.health.report_ok +local info = vim.health.info or vim.health.report_info --- Run a command and handle potential errors ---@param executable string @@ -39,6 +40,9 @@ local function treesitter_parser_available(ft) end function M.check() + start('CopilotChat.nvim') + info('If you are facing any issues, also see :CopilotChatDebugInfo for more information.') + start('CopilotChat.nvim [core]') local vim_version = vim.trim(vim.api.nvim_command_output('version')) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 48952bb9..0bef03c7 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -247,6 +247,7 @@ end function M.complete_items(callback) async.run(function() local agents = state.copilot:list_agents() + local contexts = context.supported_contexts() local items = {} local prompts_to_use = M.prompts() @@ -273,23 +274,16 @@ function M.complete_items(callback) } end - items[#items + 1] = { - word = '#buffers', - kind = 'context', - menu = 'Include all loaded buffers in context', - icase = 1, - dup = 0, - empty = 0, - } - - items[#items + 1] = { - word = '#buffer', - kind = 'context', - menu = 'Include the specified buffer in context', - icase = 1, - dup = 0, - empty = 0, - } + for prompt_context, description in pairs(contexts) do + items[#items + 1] = { + word = '#' .. prompt_context, + kind = 'context', + menu = description, + icase = 1, + dup = 0, + empty = 0, + } + end vim.schedule(function() callback(items) @@ -499,12 +493,13 @@ function M.ask(prompt, config, source) append('\n\n' .. config.answer_header .. config.separator .. '\n\n', config) local selected_context = config.context - if string.find(prompt, '#buffers') then - selected_context = 'buffers' - elseif string.find(prompt, '#buffer') then - selected_context = 'buffer' + local contexts = vim.tbl_keys(context.supported_contexts()) + for prompt_context in updated_prompt:gmatch('#([%w_-]+)') do + if vim.tbl_contains(contexts, prompt_context) then + selected_context = prompt_context + updated_prompt = string.gsub(updated_prompt, '#' .. prompt_context .. '%s*', '') + end end - updated_prompt = string.gsub(updated_prompt, '#buffers?%s*', '') async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) @@ -712,14 +707,9 @@ function M.setup(config) end, { force = true }) M.config = vim.tbl_deep_extend('force', default_config, config or {}) - if M.config.model == 'gpt-4o' then - M.config.model = 'gpt-4o-2024-05-13' - end if state.copilot then state.copilot:stop() - else - debuginfo.setup() end state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) @@ -1063,6 +1053,9 @@ function M.setup(config) vim.api.nvim_create_user_command('CopilotChatReset', function() M.reset() end, { force = true }) + vim.api.nvim_create_user_command('CopilotChatDebugInfo', function() + debuginfo.open() + end, { force = true }) local function complete_load() local options = vim.tbl_map(function(file) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 2a1e6ef3..d979d85a 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -30,12 +30,6 @@ function M.class(fn, parent) return out end ---- Get the log file path ----@return string -function M.get_log_file_path() - return log.logfile -end - --- Check if the current version of neovim is stable ---@return boolean function M.is_stable()