-
-
Notifications
You must be signed in to change notification settings - Fork 406
Expand file tree
/
Copy pathbuffer_diffs.lua
More file actions
180 lines (154 loc) · 5.15 KB
/
buffer_diffs.lua
File metadata and controls
180 lines (154 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
--[[
Syncs buffer changes by tracking diffs between buffer states.
Detects line additions, deletions, and modifications.
]]
local config = require("codecompanion.config")
local log = require("codecompanion.utils.log")
local api = vim.api
local fmt = string.format
local diff = vim.text.diff or vim.diff
---@class CodeCompanion.BufferDiffs
---@field buffers table<number, CodeCompanion.BufferDiffs.State> Map of buffer numbers to their states
---@field augroup number The autocmd group ID
---@field sync fun(self: CodeCompanion.BufferDiffs, bufnr: number): nil Start syncing a buffer
---@field unsync fun(self: CodeCompanion.BufferDiffs, bufnr: number): nil Stop syncing a buffer
---@field get_changes fun(self: CodeCompanion.BufferDiffs, bufnr: number): boolean, table
---@class CodeCompanion.BufferDiffs.State
---@field content string[] Complete buffer content
---@field changedtick number Last known changedtick
---@field last_sent string[] Last content sent to LLM
---@class CodeCompanion.BufferDiffs
local BufferDiffs = {}
function BufferDiffs.new()
return setmetatable({
buffers = {},
augroup = api.nvim_create_augroup("codecompanion.buffer_diffs", { clear = true }),
}, { __index = BufferDiffs })
end
---Sync with a buffer to watch for changes
---@param bufnr number
---@return nil
function BufferDiffs:sync(bufnr)
if self.buffers[bufnr] then
return
end
if not api.nvim_buf_is_valid(bufnr) then
return log:debug("Cannot sync invalid buffer: %d", bufnr)
end
log:debug("Starting to sync buffer: %d", bufnr)
local initial_content = api.nvim_buf_get_lines(bufnr, 0, -1, false)
self.buffers[bufnr] = {
content = initial_content,
last_sent = initial_content,
changedtick = api.nvim_buf_get_changedtick(bufnr),
}
api.nvim_create_autocmd("BufDelete", {
group = self.augroup,
buffer = bufnr,
callback = function()
self:unsync(bufnr)
end,
})
end
---Stop syncing a buffer
---@param bufnr number
---@return nil
function BufferDiffs:unsync(bufnr)
if self.buffers[bufnr] then
log:debug("Unsyncing buffer %d", bufnr)
self.buffers[bufnr] = nil
end
end
---Check if buffer content has changed
---@param old_content table
---@param new_content table
---@return boolean
local function has_changes(old_content, new_content)
if #old_content ~= #new_content then
return true
end
for i = 1, #old_content do
if old_content[i] ~= new_content[i] then
return true
end
end
return false
end
---Get any changes in a synced buffer
---@param bufnr number
---@return boolean, table|nil
function BufferDiffs:get_changes(bufnr)
if not self.buffers[bufnr] then
return false, nil
end
if not api.nvim_buf_is_valid(bufnr) then
-- special case for unlisted buffers
self:unsync(bufnr)
return true, nil
end
local buffer = self.buffers[bufnr]
local current_content = api.nvim_buf_get_lines(bufnr, 0, -1, false)
local current_tick = api.nvim_buf_get_changedtick(bufnr)
if current_tick == buffer.changedtick then
return false, nil
end
local old_content = buffer.last_sent -- Store before updating
local changed = has_changes(old_content, current_content)
if changed then
buffer.content = current_content
buffer.last_sent = current_content
buffer.changedtick = current_tick
return true, old_content
end
return false, nil
end
---Generate unified diff using vim.diff
---@param old_content table
---@param new_content table
---@return string
local function format_changes_as_diff(old_content, new_content)
-- Convert line arrays to strings for vim.diff
local old_str = table.concat(old_content, "\n") .. "\n"
local new_str = table.concat(new_content, "\n") .. "\n"
local diff_result = diff(old_str, new_str, {
result_type = "unified",
ctxlen = 3,
algorithm = "myers",
})
if diff_result and diff_result ~= "" then
return fmt("````diff\n%s````", diff_result)
end
return ""
end
---Check all synced buffers for changes
---@param chat CodeCompanion.Chat
function BufferDiffs:check_for_changes(chat)
for _, item in ipairs(chat.context_items) do
if item.bufnr and item.opts and item.opts.sync_diff then
local has_changed, old_content = self:get_changes(item.bufnr)
if has_changed and old_content then
local filename = api.nvim_buf_get_name(item.bufnr)
local current_content = api.nvim_buf_get_lines(item.bufnr, 0, -1, false)
local diff_content = format_changes_as_diff(old_content, current_content)
if diff_content ~= "" then
local delta = fmt("The file `%s`, has been modified. Here are the changes:\n%s", filename, diff_content)
chat:add_message({
role = config.constants.USER_ROLE,
content = fmt(
[[<attachment filepath="%s" buffer_number="%s">%s</attachment>]],
filename,
item.bufnr,
delta
),
}, { context = { id = item.id }, visible = false })
end
elseif has_changed then
chat:add_message({
role = config.constants.USER_ROLE,
content = fmt([[Buffer %d has been removed.]], item.bufnr),
}, { visible = false })
end
end
end
end
return BufferDiffs