Skip to content

Commit

Permalink
feat: add mini.diff as option for inline diffs (#210)
Browse files Browse the repository at this point in the history
* feat: adding mini.diff module

* fix: fixing the diff against original buffer

* fix: safely call mini.diff plugin

* fix: safely check mini.diff, check if tests pass

* feat: add diff_method to the config

* docs: adding mini.diff to the README

* fix: rename miniDiff to mini_diff for consistency
  • Loading branch information
bassamsdata committed Sep 13, 2024
1 parent eab01fa commit a33d4ae
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 41 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,8 @@ Use Markdown formatting and include the programming language name at the start o
layout = "vertical", -- vertical|horizontal|buffer
diff = {
enabled = true,
-- mini_diff is using inline diff in the same buffer but requires the plugin to be installed: https://github.com/echasnovski/mini.diff
diff_method = "default", -- default|mini_diff
close_chat_at = 240, -- Close an open chat buffer if the total columns of your display are less than...
layout = "vertical", -- vertical|horizontal
opts = { "internal", "filler", "closeoff", "algorithm:patience", "followwrap", "linematch:120" },
Expand Down Expand Up @@ -1330,6 +1332,27 @@ require('legendary').setup({
})
```

**Mini.Diff**

if you're using [mini.diff](https://github.com/echasnovski/mini.diff) you can put an icon in the statusline to indicate which diff is used currently, git or llm changes:

```lua
local function getDiffSource()
local buf_id, diff_source, diffIcon
buf_id = vim.api.nvim_get_current_buf()
diff_source = vim.b[buf_id].diffCompGit
if not diff_source then
return ""
end
if diff_source == "git" then
diffIcon = "󰊤 "
elseif diff_source == "llm" then
diffIcon = ""
end
return string.format("%%#StatusLineLSP#%s", diffIcon)
end
```

## :toolbox: Troubleshooting

Before raising an [issue](https://github.com/olimorris/codecompanion.nvim/issues), there are a number of steps you can take to troubleshoot a problem:
Expand Down
1 change: 1 addition & 0 deletions lua/codecompanion/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ Use Markdown formatting and include the programming language name at the start o
layout = "vertical", -- vertical|horizontal|buffer
diff = {
enabled = true,
diff_method = "default", -- default|mini_diff
close_chat_at = 240, -- Close an open chat buffer if the total columns of your display are less than...
layout = "vertical", -- vertical|horizontal
opts = { "internal", "filler", "closeoff", "algorithm:patience", "followwrap", "linematch:120" },
Expand Down
101 changes: 60 additions & 41 deletions lua/codecompanion/strategies/inline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local config = require("codecompanion").config

local keymaps = require("codecompanion.utils.keymaps")
local log = require("codecompanion.utils.log")
local mini_diff = require("codecompanion.strategies.inline.mini_diff")
local msg_utils = require("codecompanion.utils.messages")
local ui = require("codecompanion.utils.ui")
local util = require("codecompanion.utils.util")
Expand Down Expand Up @@ -173,6 +174,13 @@ end
---Start the classification of the user's prompt
---@param opts? table
function Inline:start(opts)
-- NOTE: we need to add this here to intiate the mini.diff early to be
-- working properly
if config.display.inline.diff.diff_method == "mini_diff" then
log:trace("CodeCompanion: Using mini diff for inline display")
require("codecompanion.strategies.inline.mini_diff").setup()
end

log:trace("Starting Inline with opts: %s", opts)

if opts and opts[1] then
Expand Down Expand Up @@ -562,61 +570,72 @@ function Inline:start_diff()
if config.display.inline.diff.enabled == false then
return
end
if config.display.inline.diff.diff_method == "mini_diff" then
return -- no need to do anything here, since it's handled in mini_diff.lua
else
-- Taken from the awesome:
-- https://github.com/S1M0N38/dante.nvim

-- Taken from the awesome:
-- https://github.com/S1M0N38/dante.nvim

-- Get current window properties
local wrap = vim.wo.wrap
local linebreak = vim.wo.linebreak
local breakindent = vim.wo.breakindent
vim.cmd("set diffopt=" .. table.concat(config.display.inline.diff.opts, ","))
-- Get current window properties
local wrap = vim.wo.wrap
local linebreak = vim.wo.linebreak
local breakindent = vim.wo.breakindent
vim.cmd("set diffopt=" .. table.concat(config.display.inline.diff.opts, ","))

-- Close the chat buffer
local last_chat = require("codecompanion").last_chat()
if last_chat and last_chat:is_visible() and config.display.inline.diff.close_chat_at > vim.o.columns then
last_chat:hide()
end
-- Close the chat buffer
local last_chat = require("codecompanion").last_chat()
if last_chat and last_chat:is_visible() and config.display.inline.diff.close_chat_at > vim.o.columns then
last_chat:hide()
end

-- Create the diff buffer
if config.display.inline.diff.layout == "vertical" then
vim.cmd("vsplit")
else
vim.cmd("split")
-- Create the diff buffer
if config.display.inline.diff.layout == "vertical" then
vim.cmd("vsplit")
else
vim.cmd("split")
end
self.diff.winnr = api.nvim_get_current_win()
self.diff.bufnr = api.nvim_create_buf(false, true)
api.nvim_win_set_buf(self.diff.winnr, self.diff.bufnr)
api.nvim_set_option_value("filetype", self.context.filetype, { buf = self.diff.bufnr })
api.nvim_set_option_value("wrap", wrap, { win = self.diff.winnr })
api.nvim_set_option_value("linebreak", linebreak, { win = self.diff.winnr })
api.nvim_set_option_value("breakindent", breakindent, { win = self.diff.winnr })

-- Set the diff buffer to the contents, prior to any modifications
api.nvim_buf_set_lines(self.diff.bufnr, 0, 0, true, self.diff.lines)
api.nvim_win_set_cursor(self.diff.winnr, { self.context.cursor_pos[1], self.context.cursor_pos[2] })

-- Begin diffing
api.nvim_set_current_win(self.diff.winnr)
vim.cmd("diffthis")
api.nvim_set_current_win(self.context.winnr)
vim.cmd("diffthis")
end
self.diff.winnr = api.nvim_get_current_win()
self.diff.bufnr = api.nvim_create_buf(false, true)
api.nvim_win_set_buf(self.diff.winnr, self.diff.bufnr)
api.nvim_set_option_value("filetype", self.context.filetype, { buf = self.diff.bufnr })
api.nvim_set_option_value("wrap", wrap, { win = self.diff.winnr })
api.nvim_set_option_value("linebreak", linebreak, { win = self.diff.winnr })
api.nvim_set_option_value("breakindent", breakindent, { win = self.diff.winnr })

-- Set the diff buffer to the contents, prior to any modifications
api.nvim_buf_set_lines(self.diff.bufnr, 0, 0, true, self.diff.lines)
api.nvim_win_set_cursor(self.diff.winnr, { self.context.cursor_pos[1], self.context.cursor_pos[2] })

-- Begin diffing
api.nvim_set_current_win(self.diff.winnr)
vim.cmd("diffthis")
api.nvim_set_current_win(self.context.winnr)
vim.cmd("diffthis")
end

---Accept the changes in the diff
---@return nil
function Inline:accept()
api.nvim_win_close(self.diff.winnr, false)
self.diff = {}
if config.display.inline.diff.diff_method == "mini_diff" then
mini_diff.accept(self.context.bufnr)
else
api.nvim_win_close(self.diff.winnr, false)
self.diff = {}
end
end

---Reject the changes in the diff
---@return nil
function Inline:reject()
vim.cmd("diffoff")
api.nvim_win_close(self.diff.winnr, false)
api.nvim_buf_set_lines(self.context.bufnr, 0, -1, true, self.diff.lines)
self.diff = {}
if config.display.inline.diff.diff_method == "mini_diff" then
mini_diff.reject(self.context.bufnr)
else
vim.cmd("diffoff")
api.nvim_win_close(self.diff.winnr, false)
api.nvim_buf_set_lines(self.context.bufnr, 0, -1, true, self.diff.lines)
self.diff = {}
end
end

return Inline
214 changes: 214 additions & 0 deletions lua/codecompanion/strategies/inline/mini_diff.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
-- implementing mini.diff for llm changes
-- to see the main logic, please head to the setup function below
local M = {}

local original_buffer_content = {} -- Store the original buffer content
local codecompanion_buffers = {} -- Store which buffers are using CodeCompanion source
local revert_timers = {} -- Store timers for reverting to Git source
local log = require("codecompanion.utils.log")
local ok, MiniDiff = pcall(require, "mini.diff")
if not ok then
return log:error("Failed to load mini.diff: ", vim.log.levels.WARN)
end
local git_source = MiniDiff.gen_source.git()
local REVERT_DELAY = 5 * 60 * 1000 -- 5 minutes

---@param buf_id number
---@return boolean Whether
local function is_valid_buffer(buf_id)
return buf_id and vim.api.nvim_buf_is_valid(buf_id)
end

-- store a buffer variable to know which buffer is using codecompanion
---@param buf_id number
---@param source string
local function set_diff_source(buf_id, source)
if is_valid_buffer(buf_id) then
log:debug("Setting diff source for buffer %d to '%s'", buf_id, source)
vim.b[buf_id].diffCompGit = source
else
log:debug("Attempted to set diff source for invalid buffer %d", buf_id)
end
end

-- Define the codecompanion source for mini.diff to use
---@see https://github.com/echasnovski/mini.nvim/blob/main/doc/mini_diff.txt
---@class CodeCompanionSource
---@field name string
---@field attach fun(buf_id: number): boolean
---@field detach fun(buf_id: number)
local codecompanion_source = {
name = "codecompanion",
---@param buf_id number
---@return boolean whether the attachment was successful
attach = function(buf_id)
if not is_valid_buffer(buf_id) then
return false
end
original_buffer_content[buf_id] = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
log:trace("original_buffer_content assinged" .. "step 0")
set_diff_source(buf_id, "llm")
return true
end,
---@param buf_id number
detach = function(buf_id)
original_buffer_content[buf_id] = nil
log:trace("original_buffer_content detached" .. "step 1")
set_diff_source(buf_id, "git")
end,
}

-- used to switch back to diff agaist llm changes
---@param buf_id number
function M.switch_to_codecompanion(buf_id)
if not codecompanion_buffers[buf_id] then
log:debug("Switching buffer %d to CodeCompanion source", buf_id)
codecompanion_buffers[buf_id] = true
MiniDiff.disable(buf_id)
MiniDiff.enable(buf_id, { source = codecompanion_source })
M.update_diff(buf_id)
set_diff_source(buf_id, "llm")
else
log:debug("Buffer %d is already using CodeCompanion source", buf_id)
end
end

-- used to switch back to diff agaist git, used in 'gr' and 'ga' or by timer.
---@param buf_id number
function M.switch_to_git(buf_id)
if codecompanion_buffers[buf_id] then
log:debug("Switching buffer %d to Git source", buf_id)
codecompanion_buffers[buf_id] = nil
MiniDiff.disable(buf_id)
MiniDiff.enable(buf_id, { source = git_source })
set_diff_source(buf_id, "git")
else
log:debug("Buffer %d is already using Git source", buf_id)
end
end

function M.update_diff(buf_id)
if not is_valid_buffer(buf_id) then
return
end

local current_content = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
pcall(MiniDiff.set_ref_text, buf_id, original_buffer_content[buf_id] or {})
original_buffer_content[buf_id] = current_content
log:trace("original_buffer_content assinged " .. "step 1")
end

-- this function is called every time the diff is updated to schedule return
-- to default behaviour of mini.diff against git, if the user didn't already
-- press 'gr' to reject or 'ga' to accept.
---@param buf_id number
---@param delay number
function M.schedule_revert_to_git(buf_id, delay)
if revert_timers[buf_id] then
log:debug("Stopping existing revert timer for buffer %d", buf_id)
revert_timers[buf_id]:stop()
end
log:debug("Scheduling revert to Git source for buffer %d in %d milliseconds", buf_id, delay)
revert_timers[buf_id] = vim.defer_fn(function()
M.switch_to_git(buf_id)
revert_timers[buf_id] = nil
end, delay)
end

-- setup is the main mechanism/logic for this module.
---@param config? table
function M.setup(config)
config = config or {}
-- MiniDiff.setup({ source = git_source })

vim.api.nvim_create_autocmd("User", {
pattern = "CodeCompanionInline*",
callback = function(args)
local buf_id = args.buf
if not is_valid_buffer(buf_id) then
return
end

if args.match == "CodeCompanionInlineStarted" then
M.switch_to_codecompanion(buf_id)
elseif args.match == "CodeCompanionInlineFinished" then
-- local current_content = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
pcall(MiniDiff.set_ref_text, buf_id, original_buffer_content[buf_id] or {})
-- original_buffer_content[buf_id] = current_content
log:trace("original_buffer_content assinged " .. "step 2")
M.schedule_revert_to_git(buf_id, REVERT_DELAY)
MiniDiff.toggle_overlay()
end
end,
})

vim.api.nvim_create_autocmd("BufReadPost", {
callback = function(args)
local buf_id = args.buf
if is_valid_buffer(buf_id) and vim.b[buf_id].diffCompGit == nil then
set_diff_source(buf_id, "git")
end
end,
})
end

---@param buf_id number
function M.accept(buf_id)
if not is_valid_buffer(buf_id) then
return
end

original_buffer_content[buf_id] = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
M.update_diff(buf_id)
M.switch_to_git(buf_id)
end

---@param buf_id number
function M.reject(buf_id)
if not is_valid_buffer(buf_id) then
return
end

vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, original_buffer_content[buf_id] or {})
M.update_diff(buf_id) -- NOTE: why do we do this here again
M.switch_to_git(buf_id)
end

-- APIS ---------------------------------------------------------------

-- this API function could benefit for the user to know which diff he's using
-- at the current monoment - could be used in the statusline
---@param buf_id? number
---@return string
function M.get_current_source(buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
return vim.b[buf_id].diffCompGit or "git"
end

-- API to force switch back to git, could used by keymap
---@param buf_id? number
function M.force_git(buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
M.switch_to_git(buf_id)
end

-- API to force switch to codecompanion, could used by keymap
---@param buf_id? number
function M.force_codecompanion(buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not is_valid_buffer(buf_id) then
print("Invalid buffer ID")
return
end

-- Ensure we have original content to diff against
if not original_buffer_content[buf_id] then
original_buffer_content[buf_id] = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
end

M.switch_to_codecompanion(buf_id)
-- Force an update of the diff
M.update_diff(buf_id)
end

return M

0 comments on commit a33d4ae

Please sign in to comment.