From ed37e84620f3c9dd66850a4bcc5d0638391375f4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 5 Feb 2025 00:11:42 +0100 Subject: [PATCH] feat(diff): add option to show full diff with diff mode Add new `full_diff` option to show_diff mapping which allows showing a full diff view instead of unified diff view. When enabled, it will show the modified file content side by side with original file using Vim's built-in diff mode. The full diff mode provides better visualization of changes and allows using Vim's diff navigation commands to review changes. Closes #705 Signed-off-by: Tomas Slusny --- README.md | 3 +- lua/CopilotChat/config.lua | 12 ++++-- lua/CopilotChat/init.lua | 31 +++++++------- lua/CopilotChat/ui/diff.lua | 85 +++++++++++++++++++++++++++++-------- 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index da8f7417..c0497881 100644 --- a/README.md +++ b/README.md @@ -556,10 +556,11 @@ Also see [here](/lua/CopilotChat/config.lua): }, yank_diff = { normal = 'gy', - register = '"', + register = '"', -- Default register to use for yanking }, show_diff = { normal = 'gd', + full_diff = false, -- Show full diff instead of unified diff when showing diff window }, show_info = { normal = 'gi', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 648459fd..44182371 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -30,9 +30,12 @@ local utils = require('CopilotChat.utils') ---@field insert string? ---@field detail string? ----@class CopilotChat.config.mapping.register : CopilotChat.config.mapping +---@class CopilotChat.config.mapping.yank_diff : CopilotChat.config.mapping ---@field register string? +---@class CopilotChat.config.mapping.show_diff : CopilotChat.config.mapping +---@field full_diff boolean? + ---@class CopilotChat.config.mappings ---@field complete CopilotChat.config.mapping? ---@field close CopilotChat.config.mapping? @@ -42,8 +45,8 @@ local utils = require('CopilotChat.utils') ---@field accept_diff CopilotChat.config.mapping? ---@field jump_to_diff CopilotChat.config.mapping? ---@field quickfix_diffs CopilotChat.config.mapping? ----@field yank_diff CopilotChat.config.mapping.register? ----@field show_diff CopilotChat.config.mapping? +---@field yank_diff CopilotChat.config.mapping.yank_diff? +---@field show_diff CopilotChat.config.mapping.show_diff? ---@field show_info CopilotChat.config.mapping? ---@field show_context CopilotChat.config.mapping? ---@field show_help CopilotChat.config.mapping? @@ -391,10 +394,11 @@ return { }, yank_diff = { normal = 'gy', - register = '"', + register = '"', -- Default register to use for yanking }, show_diff = { normal = 'gd', + full_diff = false, -- Show full diff instead of unified diff when showing diff window }, show_info = { normal = 'gi', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 2dc6b0b1..ca608fa3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -6,11 +6,6 @@ local context = require('CopilotChat.context') local prompts = require('CopilotChat.prompts') local utils = require('CopilotChat.utils') -local Chat = require('CopilotChat.ui.chat') -local Diff = require('CopilotChat.ui.diff') -local Overlay = require('CopilotChat.ui.overlay') -local Debug = require('CopilotChat.ui.debug') - local M = {} local PLUGIN_NAME = 'CopilotChat' local WORD = '([^%s]+)' @@ -959,34 +954,38 @@ function M.setup(config) if state.overlay then state.overlay:delete() end - state.overlay = Overlay('copilot-overlay', overlay_help, function(bufnr) + state.overlay = require('CopilotChat.ui.overlay')('copilot-overlay', overlay_help, function(bufnr) map_key('close', bufnr, function() state.overlay:restore(state.chat.winnr, state.chat.bufnr) end) end) if not state.debug then - state.debug = Debug() + state.debug = require('CopilotChat.ui.debug')() end if state.diff then state.diff:delete() end - state.diff = Diff(diff_help, function(bufnr) - map_key('close', bufnr, function() - state.diff:restore(state.chat.winnr, state.chat.bufnr) - end) + state.diff = require('CopilotChat.ui.diff')( + M.config.mappings.show_diff.full_diff, + diff_help, + function(bufnr) + map_key('close', bufnr, function() + state.diff:restore(state.chat.winnr, state.chat.bufnr) + end) - map_key('accept_diff', bufnr, function() - apply_diff(state.diff:get_diff(), state.chat.config) - end) - end) + map_key('accept_diff', bufnr, function() + apply_diff(state.diff:get_diff(), state.chat.config) + end) + end + ) if state.chat then state.chat:close(state.source and state.source.bufnr or nil) state.chat:delete() end - state.chat = Chat( + state.chat = require('CopilotChat.ui.chat')( M.config.question_header, M.config.answer_header, M.config.separator, diff --git a/lua/CopilotChat/ui/diff.lua b/lua/CopilotChat/ui/diff.lua index 4cf92805..0e2428da 100644 --- a/lua/CopilotChat/ui/diff.lua +++ b/lua/CopilotChat/ui/diff.lua @@ -14,13 +14,17 @@ local class = utils.class ---@class CopilotChat.ui.Diff : CopilotChat.ui.Overlay ---@field hl_ns number ---@field diff CopilotChat.ui.Diff.Diff? -local Diff = class(function(self, help, on_buf_create) +---@field augroup number +---@field full_diff boolean +local Diff = class(function(self, full_diff, help, on_buf_create) Overlay.init(self, 'copilot-diff', help, on_buf_create) self.hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') vim.api.nvim_set_hl(self.hl_ns, '@diff.plus', { bg = utils.blend_color('DiffAdd', 20) }) vim.api.nvim_set_hl(self.hl_ns, '@diff.minus', { bg = utils.blend_color('DiffDelete', 20) }) vim.api.nvim_set_hl(self.hl_ns, '@diff.delta', { bg = utils.blend_color('DiffChange', 20) }) + self.augroup = vim.api.nvim_create_augroup('CopilotChatDiff', { clear = true }) + self.full_diff = full_diff self.diff = nil end, Overlay) @@ -31,27 +35,74 @@ function Diff:show(diff, winnr) self:validate() vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) - Overlay.show( - self, - tostring(vim.diff(diff.reference, diff.change, { - result_type = 'unified', - ignore_blank_lines = true, - ignore_whitespace = true, - ignore_whitespace_change = true, - ignore_whitespace_change_at_eol = true, - ignore_cr_at_eol = true, - algorithm = 'myers', - ctxlen = #diff.reference, - })), - winnr, - diff.filetype, - 'diff' - ) + if not self.full_diff then + -- Create unified diff view + Overlay.show( + self, + tostring(vim.diff(diff.reference, diff.change, { + result_type = 'unified', + ignore_blank_lines = true, + ignore_whitespace = true, + ignore_whitespace_change = true, + ignore_whitespace_change_at_eol = true, + ignore_cr_at_eol = true, + algorithm = 'myers', + ctxlen = #diff.reference, + })), + winnr, + diff.filetype, + 'diff' + ) + + return + end + + -- Create modified version by applying the change + local modified = {} + if diff.bufnr and utils.buf_valid(diff.bufnr) then + modified = vim.api.nvim_buf_get_lines(diff.bufnr, 0, -1, false) + end + local change_lines = vim.split(diff.change, '\n') + + -- Replace the lines in the modified content + if #modified > 0 then + local start_idx = diff.start_line - 1 + local end_idx = diff.end_line - 1 + for _ = start_idx, end_idx do + table.remove(modified, start_idx) + end + for i, line in ipairs(change_lines) do + table.insert(modified, start_idx + i - 1, line) + end + else + modified = change_lines + end + + Overlay.show(self, table.concat(modified, '\n'), winnr, diff.filetype) + + if diff.bufnr and vim.api.nvim_buf_is_valid(diff.bufnr) then + vim.cmd('diffthis') + vim.api.nvim_set_current_win(vim.fn.bufwinid(diff.bufnr)) + vim.api.nvim_win_set_cursor(0, { diff.start_line, 0 }) + vim.cmd('diffthis') + vim.api.nvim_set_current_win(winnr) + vim.api.nvim_win_set_cursor(winnr, { diff.start_line, 0 }) + + -- Link diff buffers lifecycle + vim.api.nvim_create_autocmd('BufWipeout', { + group = self.augroup, + buffer = self.bufnr, + callback = function() + vim.cmd('diffoff') + end, + }) + end end ---@param winnr number ---@param bufnr number function Diff:restore(winnr, bufnr) + vim.cmd('diffoff') Overlay.restore(self, winnr, bufnr) vim.api.nvim_win_set_hl_ns(winnr, 0) end