From 8bc31066f72a8c55630980f7d008b88425d6b33b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 10:40:58 +0100 Subject: [PATCH] Revamp how diagnostics are handled - Automatically include diagnostics in selections. This means that the chat will always know about them as well - With diagnostics automatically included, CopilotChatFixDiagnostics is no longer necessary, CopilotChatFix is enough - With diagnostics automatically included, actions.help_actions also no longer serves any purpose, CopilotChatExplain + CopilotChatFix is enough - To better incorporate new workflow, also default to visual || buffer for selection (which was popular config adjustment in first place anyway) Signed-off-by: Tomas Slusny --- README.md | 34 ++---------- lua/CopilotChat/actions.lua | 31 ++--------- lua/CopilotChat/config.lua | 20 ++++--- lua/CopilotChat/copilot.lua | 57 +++++++++++++++----- lua/CopilotChat/init.lua | 62 ++++++++------------- lua/CopilotChat/select.lua | 105 ++++++++++++++++++------------------ lua/CopilotChat/utils.lua | 4 ++ 7 files changed, 143 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 627f63a5..6157151a 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,12 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl #### Commands coming from default prompts -- `:CopilotChatExplain` - Write an explanation for the active selection as paragraphs of text +- `:CopilotChatExplain` - Write an explanation for the active selection and diagnostics as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code -- `:CopilotChatFixDiagnostic` - Please assist with the following diagnostic issue in file - `:CopilotChatCommit` - Write commit message for the change with commitizen convention - `:CopilotChatCommitStaged` - Write commit message for the change with commitizen convention @@ -181,9 +180,6 @@ local response = chat.response() -- Pick a prompt using vim.ui.select local actions = require("CopilotChat.actions") --- Pick help actions -actions.pick(actions.help_actions()) - -- Pick prompt actions actions.pick(actions.prompt_actions({ selection = require("CopilotChat.select").visual, @@ -227,15 +223,15 @@ Also see [here](/lua/CopilotChat/config.lua): history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received - -- default selection (visual or line) + -- default selection selection = function(source) - return select.visual(source) or select.line(source) + return select.visual(source) or select.buffer(source) end, -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -255,10 +251,6 @@ Also see [here](/lua/CopilotChat/config.lua): Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', }, - FixDiagnostic = { - prompt = 'Please assist with the following diagnostic issue in file:', - selection = select.diagnostics, - }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, @@ -462,15 +454,6 @@ Requires [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) plug ```lua -- lazy.nvim keys - -- Show help actions with telescope - { - "cch", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.telescope").pick(actions.help_actions()) - end, - desc = "CopilotChat - Help actions", - }, -- Show prompts actions with telescope { "ccp", @@ -494,15 +477,6 @@ Requires [fzf-lua](https://github.com/ibhagwan/fzf-lua) plugin to be installed. ```lua -- lazy.nvim keys - -- Show help actions with fzf-lua - { - "cch", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.fzflua").pick(actions.help_actions()) - end, - desc = "CopilotChat - Help actions", - }, -- Show prompts actions with fzf-lua { "ccp", diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua index 1d838a7b..4ae5dfeb 100644 --- a/lua/CopilotChat/actions.lua +++ b/lua/CopilotChat/actions.lua @@ -2,37 +2,14 @@ ---@field prompt string: The prompt to display ---@field actions table: A table with the actions to pick from -local select = require('CopilotChat.select') local chat = require('CopilotChat') +local utils = require('CopilotChat.utils') local M = {} ---- Diagnostic help actions ----@param config CopilotChat.config?: The chat configuration ----@return CopilotChat.integrations.actions?: The help actions -function M.help_actions(config) - local bufnr = vim.api.nvim_get_current_buf() - local winnr = vim.api.nvim_get_current_win() - local cursor = vim.api.nvim_win_get_cursor(winnr) - local line_diagnostics = vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 }) - - if #line_diagnostics == 0 then - return nil - end - - return { - prompt = 'Copilot Chat Help Actions', - actions = { - ['Fix diagnostic'] = vim.tbl_extend('keep', { - prompt = 'Please assist with fixing the following diagnostic issue in file:', - selection = select.diagnostics, - }, config or {}), - ['Explain diagnostic'] = vim.tbl_extend('keep', { - prompt = 'Please explain the following diagnostic issue in file:', - selection = select.diagnostics, - }, config or {}), - }, - } +function M.help_actions() + utils.deprecate('help_actions()', 'prompt_actions()') + return M.prompt_actions() end --- User prompt actions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index e8993d9e..bf7fabb0 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -5,15 +5,23 @@ local select = require('CopilotChat.select') --- @field bufnr number --- @field winnr number +---@class CopilotChat.config.selection.diagnostic +---@field message string +---@field severity string +---@field start_row number +---@field start_col number +---@field end_row number +---@field end_col number + ---@class CopilotChat.config.selection ---@field lines string +---@field diagnostics table? ---@field filename string? ---@field filetype string? ---@field start_row number? ---@field start_col number? ---@field end_row number? ---@field end_col number? ----@field prompt_extra string? ---@class CopilotChat.config.prompt ---@field prompt string? @@ -106,15 +114,15 @@ return { history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received - -- default selection (visual or line) + -- default selection selection = function(source) - return select.visual(source) or select.line(source) + return select.visual(source) or select.buffer(source) end, -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -170,10 +178,6 @@ return { Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', }, - FixDiagnostic = { - prompt = 'Please assist with the following diagnostic issue in file:', - selection = select.diagnostics, - }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 7db68afa..d999a7cc 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -5,7 +5,7 @@ ---@field content string? ---@class CopilotChat.copilot.ask.opts ----@field selection string? +---@field selection CopilotChat.config.selection? ---@field embeddings table? ---@field filename string? ---@field filetype string? @@ -152,24 +152,56 @@ local function get_cached_token() return nil end -local function generate_selection_message(filename, filetype, start_row, end_row, selection) - if not selection or selection == '' then +local function generate_selection_message(filename, filetype, selection) + local content = selection.lines + + if not content or content == '' then return '' end - local content = selection - if start_row > 0 then - local lines = vim.split(selection, '\n') + if selection.start_row and selection.start_row > 0 then + local lines = vim.split(content, '\n') local total_lines = #lines local max_length = #tostring(total_lines) for i, line in ipairs(lines) do - local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + start_row) + local formatted_line_number = + string.format('%' .. max_length .. 'd', i - 1 + selection.start_row) lines[i] = formatted_line_number .. ': ' .. line end content = table.concat(lines, '\n') end - return string.format('Active selection: `%s`\n```%s\n%s\n```', filename, filetype, content) + local out = string.format('Active selection: `%s`\n```%s\n%s\n```', filename, filetype, content) + + if selection.diagnostics then + local diagnostics = {} + for _, diagnostic in ipairs(selection.diagnostics) do + local start_row = diagnostic.start_row + local end_row = diagnostic.end_row + if start_row == end_row then + table.insert( + diagnostics, + string.format('%s line=%d: %s', diagnostic.severity, start_row, diagnostic.message) + ) + else + table.insert( + diagnostics, + string.format( + '%s line=%d-%d: %s', + diagnostic.severity, + start_row, + end_row, + diagnostic.message + ) + ) + end + end + + out = + string.format('%s\nDiagnostics: `%s`\n%s\n', out, filename, table.concat(diagnostics, '\n')) + end + + return out end local function generate_embeddings_message(embeddings) @@ -473,9 +505,7 @@ function Copilot:ask(prompt, opts) local embeddings = opts.embeddings or {} local filename = opts.filename or '' local filetype = opts.filetype or '' - local selection = opts.selection or '' - local start_row = opts.start_row or 0 - local end_row = opts.end_row or 0 + local selection = opts.selection or {} local system_prompt = opts.system_prompt or prompts.COPILOT_INSTRUCTIONS local model = opts.model or 'gpt-4o-2024-05-13' local temperature = opts.temperature or 0.1 @@ -484,7 +514,7 @@ function Copilot:ask(prompt, opts) self.current_job = job_id log.trace('System prompt: ' .. system_prompt) - log.trace('Selection: ' .. selection) + log.trace('Selection: ' .. (selection.lines or '')) log.debug('Prompt: ' .. prompt) log.debug('Embeddings: ' .. #embeddings) log.debug('Filename: ' .. filename) @@ -501,8 +531,7 @@ function Copilot:ask(prompt, opts) log.debug('Tokenizer: ' .. tokenizer) tiktoken_load(tokenizer) - local selection_message = - generate_selection_message(filename, filetype, start_row, end_row, selection) + local selection_message = generate_selection_message(filename, filetype, selection) local embeddings_message = generate_embeddings_message(embeddings) -- Count required tokens that we cannot reduce diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5adf9efc..ad969b43 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -401,11 +401,11 @@ function M.ask(prompt, config, source) message = message:match('^%s*(.-)%s*$') return message end - return tostring(err) + return vim.inspect(err) end local function on_error(err) - log.error(err) + log.error(vim.inspect(err)) vim.schedule(function() append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) append('```\n' .. get_error_message(err) .. '\n```', config) @@ -417,28 +417,15 @@ function M.ask(prompt, config, source) local selection = get_selection() local filetype = selection.filetype or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.bo[state.source.bufnr].filetype) + or 'text' local filename = selection.filename - or ( - vim.api.nvim_buf_is_valid(state.source.bufnr) - and vim.api.nvim_buf_get_name(state.source.bufnr) - ) - - if not filetype then - on_error('No filetype found for the current buffer') - return - end - - if not filename then - on_error('No filename found for the current buffer') - return - end + or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.api.nvim_buf_get_name( + state.source.bufnr + )) + or 'untitled' state.last_system_prompt = system_prompt - if selection.prompt_extra then - updated_prompt = updated_prompt .. ' ' .. selection.prompt_extra - end - if state.copilot:stop() then append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) end @@ -471,12 +458,10 @@ function M.ask(prompt, config, source) local ask_ok, response, token_count, token_max_count = pcall(state.copilot.ask, state.copilot, updated_prompt, { - selection = selection.lines, + selection = selection, embeddings = embeddings, filename = filename, filetype = filetype, - start_row = selection.start_row, - end_row = selection.end_row, system_prompt = system_prompt, model = config.model, temperature = config.temperature, @@ -617,34 +602,34 @@ end --- Set up the plugin ---@param config CopilotChat.config|nil function M.setup(config) - -- Handle old mapping format and show error - local found_old_format = false + -- Handle changed configuration if config then if config.mappings then for name, key in pairs(config.mappings) do if type(key) == 'string' then - vim.notify( - 'config.mappings.' - .. name - .. ": 'mappings' format have changed, please update your configuration, for now revering to default settings. See ':help CopilotChat-configuration' for current format", - vim.log.levels.ERROR + utils.deprecate( + 'config.mappings.' .. name, + 'config.mappings.' .. name .. '.normal and config.mappings.' .. name .. '.insert' ) - found_old_format = true + + config.mappings[name] = { + normal = key, + } end end end if config.yank_diff_register then - vim.notify( - 'config.yank_diff_register: This option has been removed, please use mappings.yank_diff.register instead', - vim.log.levels.ERROR - ) + utils.deprecate('config.yank_diff_register', 'config.mappings.yank_diff.register') + config.mappings.yank_diff.register = config.yank_diff_register end end - if found_old_format then - config.mappings = nil - end + -- Handle removed commands + vim.api.nvim_create_user_command('CopilotChatFixDiagnostic', function() + utils.deprecate('CopilotChatFixDiagnostic', 'CopilotChatFix') + M.ask('/Fix') + end, { force = true }) M.config = vim.tbl_deep_extend('force', default_config, config or {}) if M.config.model == 'gpt-4o' then @@ -951,7 +936,6 @@ function M.setup(config) }) vim.api.nvim_create_user_command('CopilotChatModel', function() - -- Show which model is being used in an alert vim.notify('Using model: ' .. M.config.model, vim.log.levels.INFO) end, { force = true }) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index cb958721..82e25b7b 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,5 +1,32 @@ local M = {} +local function get_diagnostics_in_range(bufnr, start_line, end_line) + local diagnostics = vim.diagnostic.get(bufnr) + local range_diagnostics = {} + local severity = { + [1] = 'ERROR', + [2] = 'WARNING', + [3] = 'INFORMATION', + [4] = 'HINT', + } + + for _, diagnostic in ipairs(diagnostics) do + local lnum = diagnostic.lnum + 1 + if lnum >= start_line and lnum <= end_line then + table.insert(range_diagnostics, { + message = diagnostic.message, + severity = severity[diagnostic.severity], + start_row = lnum, + start_col = diagnostic.col + 1, + end_row = lnum, + end_col = diagnostic.end_col and (diagnostic.end_col + 1) or diagnostic.col + 1, + }) + end + end + + return #range_diagnostics > 0 and range_diagnostics or nil +end + local function get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, full_line) -- Exit if no actual selection if start_line == finish_line and start_col == finish_col then @@ -68,34 +95,6 @@ function M.visual(source) return get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, false) end ---- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. ---- @return CopilotChat.config.selection|nil -function M.unnamed() - local lines = vim.fn.getreg('"') - - if not lines or lines == '' then - return nil - end - - return { - lines = lines, - } -end - ---- Select and process contents of plus register (+). This register is synchronized with system clipboard. ---- @return CopilotChat.config.selection|nil -function M.clipboard() - local lines = vim.fn.getreg('+') - - if not lines or lines == '' then - return nil - end - - return { - lines = lines, - } -end - --- Select and process whole buffer --- @param source CopilotChat.config.source --- @return CopilotChat.config.selection|nil @@ -107,13 +106,16 @@ function M.buffer(source) return nil end - return { + local out = { lines = table.concat(lines, '\n'), start_row = 1, start_col = 1, end_row = #lines, end_col = #lines[#lines], } + + out.diagnostics = get_diagnostics_in_range(bufnr, out.start_row, out.end_row) + return out end --- Select and process current line @@ -129,45 +131,44 @@ function M.line(source) return nil end - return { + local out = { lines = line, start_row = cursor[1], start_col = 1, end_row = cursor[1], end_col = #line, } + + out.diagnostics = get_diagnostics_in_range(bufnr, out.start_row, out.end_row) + return out end ---- Select whole buffer and find diagnostics ---- It uses the built-in LSP client in Neovim to get the diagnostics. ---- @param source CopilotChat.config.source +--- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. --- @return CopilotChat.config.selection|nil -function M.diagnostics(source) - local bufnr = source.bufnr - local winnr = source.winnr - local select_buffer = M.buffer(source) - if not select_buffer then +function M.unnamed() + local lines = vim.fn.getreg('"') + + if not lines or lines == '' then return nil end - local cursor = vim.api.nvim_win_get_cursor(winnr) - local line_diagnostics = vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 }) + return { + lines = lines, + } +end - if #line_diagnostics == 0 then - return nil - end +--- Select and process contents of plus register (+). This register is synchronized with system clipboard. +--- @return CopilotChat.config.selection|nil +function M.clipboard() + local lines = vim.fn.getreg('+') - local diagnostics = {} - for _, diagnostic in ipairs(line_diagnostics) do - table.insert(diagnostics, diagnostic.message) + if not lines or lines == '' then + return nil end - local result = table.concat(diagnostics, '. ') - result = result:gsub('^%s*(.-)%s*$', '%1'):gsub('\n', ' ') - - local file_name = vim.api.nvim_buf_get_name(bufnr) - select_buffer.prompt_extra = file_name .. ':' .. cursor[1] .. '. ' .. result - return select_buffer + return { + lines = lines, + } end --- Select and process current git diff diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index abc07126..2a1e6ef3 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -90,4 +90,8 @@ function M.return_to_normal_mode() end end +function M.deprecate(old, new) + vim.deprecate(old, new, '3.0.0', 'CopilotChat.nvim', false) +end + return M