Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chat): Adding buffer watcher #610

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions lua/codecompanion/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -261,67 +261,75 @@ Points to note:
callback = "keymaps.pin_reference",
description = "Pin Reference",
},
watch = {
modes = {
n = "gw",
},
index = 10,
callback = "keymaps.toggle_watch",
description = "Watch Buffer",
},
next_chat = {
modes = {
n = "}",
},
index = 10,
index = 11,
callback = "keymaps.next_chat",
description = "Next Chat",
},
previous_chat = {
modes = {
n = "{",
},
index = 11,
index = 12,
callback = "keymaps.previous_chat",
description = "Previous Chat",
},
next_header = {
modes = {
n = "]]",
},
index = 12,
index = 13,
callback = "keymaps.next_header",
description = "Next Header",
},
previous_header = {
modes = {
n = "[[",
},
index = 13,
index = 14,
callback = "keymaps.previous_header",
description = "Previous Header",
},
change_adapter = {
modes = {
n = "ga",
},
index = 14,
index = 15,
callback = "keymaps.change_adapter",
description = "Change adapter",
},
fold_code = {
modes = {
n = "gf",
},
index = 14,
index = 15,
callback = "keymaps.fold_code",
description = "Fold code",
},
debug = {
modes = {
n = "gd",
},
index = 15,
index = 16,
callback = "keymaps.debug",
description = "View debug info",
},
system_prompt = {
modes = {
n = "gs",
},
index = 16,
index = 17,
callback = "keymaps.toggle_system_prompt",
description = "Toggle the system prompt",
},
Expand Down Expand Up @@ -817,6 +825,7 @@ This is the code, for context:
chat = {
icons = {
pinned_buffer = " ",
watched_buffer = "👀 ",
},
window = {
layout = "vertical", -- float|vertical|horizontal|buffer
Expand Down
55 changes: 55 additions & 0 deletions lua/codecompanion/strategies/chat/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ function Chat.new(args)
self.parser = parser

self.References = require("codecompanion.strategies.chat.references").new({ chat = self })
self.watcher = require("codecompanion.strategies.chat.watcher").new()
self.tools = require("codecompanion.strategies.chat.tools").new({ bufnr = self.bufnr, messages = self.messages })
self.variables = require("codecompanion.strategies.chat.variables").new()

Expand Down Expand Up @@ -577,6 +578,60 @@ function Chat:submit(opts)
local bufnr = self.bufnr

local message = ts_parse_messages(self, user_role, self.header_line)

for _, ref in ipairs(self.refs) do
if ref.bufnr and ref.opts and ref.opts.watched then
local changes = self.watcher:get_changes(ref.bufnr)
log:debug("Checking watched buffer %d, found %d changes", ref.bufnr, changes and #changes or 0)

if changes and #changes > 0 then
local changes_text = string.format(
"Changes detected in `%s` (buffer %d):\n",
vim.fn.fnamemodify(api.nvim_buf_get_name(ref.bufnr), ":t"),
ref.bufnr
)

for _, change in ipairs(changes) do
if change.type == "delete" then
changes_text = changes_text
.. string.format(
"Lines %d-%d were deleted:\n```%s\n%s\n```\n",
change.start,
change.end_line,
vim.bo[ref.bufnr].filetype,
table.concat(change.lines, "\n")
)
elseif change.type == "modify" then
changes_text = changes_text
.. string.format(
"Lines %d-%d were modified from:\n```%s\n%s\n```\nto:\n```%s\n%s\n```\n",
change.start,
change.end_line,
vim.bo[ref.bufnr].filetype,
table.concat(change.old_lines, "\n"),
vim.bo[ref.bufnr].filetype,
table.concat(change.new_lines, "\n")
)
else -- type == "add"
changes_text = changes_text
.. string.format(
"Lines %d-%d were added:\n```%s\n%s\n```\n",
change.start,
change.end_line,
vim.bo[ref.bufnr].filetype,
table.concat(change.lines, "\n")
)
end
end

self:add_message({
role = config.constants.USER_ROLE,
content = changes_text,
}, { visible = true })
end
end
end

if not self:has_user_messages(message) or message.content == "" then
return log:warn("No messages to submit")
end
Expand Down
53 changes: 53 additions & 0 deletions lua/codecompanion/strategies/chat/keymaps.lua
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,59 @@ M.pin_reference = {
end,
}

M.toggle_watch = {
desc = "Toggle Watch Buffer",
callback = function(chat)
local current_line = vim.api.nvim_win_get_cursor(0)[1]
local line = vim.api.nvim_buf_get_lines(chat.bufnr, current_line - 1, current_line, true)[1]

if not vim.startswith(line, "> - ") then
return
end

local icons = config.display.chat.icons
local id = line:gsub("^> %- ", "")
if not chat.References:can_be_watched(id) then
return util.notify("This reference type cannot be watched", vim.log.levels.WARN)
end

-- Find the reference and toggle watch state
for _, ref in ipairs(chat.refs) do
local clean_id = id:gsub(icons.pinned_buffer, ""):gsub(icons.watched_buffer, "")
if ref.id == clean_id then
if not ref.opts then
ref.opts = {}
end
ref.opts.watched = not ref.opts.watched

-- Update the UI for just this line
local new_line
if ref.opts.watched then
-- Check if buffer is still valid before watching
if vim.api.nvim_buf_is_valid(ref.bufnr) and vim.api.nvim_buf_is_loaded(ref.bufnr) then
chat.watcher:watch(ref.bufnr)
new_line = string.format("> - %s%s", icons.watched_buffer, clean_id)
util.notify("Now watching buffer " .. ref.id)
else
-- Buffer is invalid, can't watch it
ref.opts.watched = false
new_line = string.format("> - %s", clean_id)
util.notify("Cannot watch invalid or unloaded buffer " .. ref.id, vim.log.levels.WARN)
end
else
chat.watcher:unwatch(ref.bufnr)
new_line = string.format("> - %s", clean_id)
util.notify("Stopped watching buffer " .. ref.id)
end

-- Update only the current line
vim.api.nvim_buf_set_lines(chat.bufnr, current_line - 1, current_line, true, { new_line })
break
end
end
end,
}

---@param chat CodeCompanion.Chat
---@param direction number
local function move_buffer(chat, direction)
Expand Down
33 changes: 29 additions & 4 deletions lua/codecompanion/strategies/chat/references.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ local config = require("codecompanion.config")
local api = vim.api
local user_role = config.strategies.chat.roles.user
local pinned_icon = config.display.chat.icons.pinned_buffer
local watched_icon = config.display.chat.icons.watched_buffer
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add these icons into a table then we can iterate over them to remove them on line 253

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


local allowed_pins = {
"<buf>",
"<file>",
}

local allowed_watches = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we should rename this to allowed_watchers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"<buf>",
}

---Parse the chat buffer to find where to add the references
---@param chat CodeCompanion.Chat
---@return table|nil
Expand Down Expand Up @@ -106,11 +111,16 @@ function References:add(ref)

if ref then
if not ref.opts then
ref.opts = {
pinned = false,
}
ref.opts = {}
end
-- Ensure both properties exist with defaults
ref.opts.pinned = ref.opts.pinned or false
ref.opts.watched = ref.opts.watched or false
table.insert(self.Chat.refs, ref)
-- If it's a buffer reference and it's being watched, start watching
if ref.bufnr and ref.opts.watched then
self.Chat.watcher:watch(ref.bufnr)
end
end

local parsed_buffer = ts_parse_buffer(self.Chat)
Expand Down Expand Up @@ -176,6 +186,8 @@ function References:render()
end
if ref.opts and ref.opts.pinned then
table.insert(lines, string.format("> - %s%s", pinned_icon, ref.id))
elseif ref.opts and ref.opts.watched then
table.insert(lines, string.format("> - %s%s", watched_icon, ref.id))
else
table.insert(lines, string.format("> - %s", ref.id))
end
Expand Down Expand Up @@ -206,6 +218,18 @@ function References:can_be_pinned(ref)
return false
end

---Determine if a reference can be watched
---@param ref string
---@return boolean
function References:can_be_watched(ref)
for _, watch in ipairs(allowed_watches) do
if ref:find(watch) then
return true
end
end
return false
end

---Get the references from the chat buffer
---@return table
function References:get_from_chat()
Expand All @@ -225,7 +249,8 @@ function References:get_from_chat()
role = role:gsub("## ", "")
elseif role == user_role and query.captures[id] == "ref" then
local ref = vim.treesitter.get_node_text(node, chat.bufnr)
ref = ref:gsub("^> %- ", ""):gsub(pinned_icon, "")
-- Clean both pinned and watched icons
ref = ref:gsub("^> %- ", ""):gsub(pinned_icon, ""):gsub(watched_icon, "")
table.insert(refs, vim.trim(ref))
end
end
Expand Down
Loading
Loading