Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
45 changes: 40 additions & 5 deletions lua/coderabbit/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@ function M.apply(bufnr, lnum, end_lnum, suggestion, message)
local old_count = end_line - lnum
local delta = #new_lines - old_count

-- Remove the applied diagnostic (first match only)
M.cleanup(bufnr, lnum, end_lnum, message, delta)
end

--- Remove a diagnostic and shift subsequent ones after an edit has been applied.
--- @param bufnr number Buffer number
--- @param lnum number 0-indexed start line of the applied edit
--- @param end_lnum number|nil 0-indexed end line (nil = single line)
--- @param message string Diagnostic message (used to identify which diagnostic to remove)
--- @param delta number Line count change from the edit (positive = lines added, negative = removed)
function M.cleanup(bufnr, lnum, end_lnum, message, delta)
local existing = vim.diagnostic.get(bufnr, { namespace = ns })
local remaining = {}
local removed = false
local end_line = (end_lnum or lnum) + 1
for _, d in ipairs(existing) do
if not removed and d.lnum == lnum and d.end_lnum == (end_lnum or lnum) and d.message == message then
removed = true
Expand All @@ -47,6 +57,7 @@ end
function M.get_actions(bufnr, range)
local start_line = range.start.line
local end_line = range["end"].line
local uri = vim.uri_from_bufnr(bufnr)

local diags = vim.diagnostic.get(bufnr, { namespace = ns })
local actions = {}
Expand All @@ -59,19 +70,37 @@ function M.get_actions(bufnr, range)
for i, suggestion in ipairs(suggestions) do
local title = #suggestions > 1 and string.format("CodeRabbit: Apply fix (%d/%d)", i, #suggestions)
or "CodeRabbit: Apply fix"

local edit_end_line = (diag.end_lnum or diag.lnum) + 1
local new_lines = vim.split(suggestion, "\n", { plain = true })
local delta = #new_lines - (edit_end_line - diag.lnum)

table.insert(actions, {
title = title,
kind = "quickfix",
edit = {
changes = {
[uri] = {
{
range = {
start = { line = diag.lnum, character = 0 },
["end"] = { line = edit_end_line, character = 0 },
},
newText = suggestion:gsub("\n*$", "\n"),
},
},
},
},
command = {
title = title,
command = "coderabbit.apply",
command = "coderabbit.cleanup",
arguments = {
{
bufnr = bufnr,
lnum = diag.lnum,
end_lnum = diag.end_lnum,
suggestion = suggestion,
message = diag.message,
delta = delta,
},
},
},
Expand All @@ -98,7 +127,7 @@ function M.attach(bufnr)
capabilities = {
codeActionProvider = true,
executeCommandProvider = {
commands = { "coderabbit.apply" },
commands = { "coderabbit.apply", "coderabbit.cleanup" },
},
},
})
Expand All @@ -110,13 +139,19 @@ function M.attach(bufnr)
local result = M.get_actions(buf, params.range)
callback(nil, result)
elseif method == "workspace/executeCommand" then
local args = type(params.arguments) == "table" and params.arguments[1]
if params.command == "coderabbit.apply" then
local args = type(params.arguments) == "table" and params.arguments[1]
if type(args) == "table" and args.bufnr and args.lnum and args.suggestion and args.message then
vim.schedule(function()
M.apply(args.bufnr, args.lnum, args.end_lnum, args.suggestion, args.message)
end)
end
elseif params.command == "coderabbit.cleanup" then
if type(args) == "table" and args.bufnr and args.message and args.delta then
vim.schedule(function()
M.cleanup(args.bufnr, args.lnum, args.end_lnum, args.message, args.delta)
end)
end
end
callback(nil, nil)
else
Expand Down
72 changes: 72 additions & 0 deletions tests/coderabbit/actions_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,78 @@ test("get_actions: only returns actions for diagnostics in range", function()
eq(result[1].command.arguments[1].lnum, 0)
end)

-- ──────────────────────────────────────────────────────────
-- Tests: cleanup
-- ──────────────────────────────────────────────────────────

test("cleanup: removes matching diagnostic", function()
reset()
local bufnr = h.make_buf({ "line0", "line1", "line2" })
vim.diagnostic.set(diagnostics.ns, bufnr, { h.diag(1, W, "fix this", { "fixed" }) })
eq(#vim.diagnostic.get(bufnr, { namespace = diagnostics.ns }), 1)
actions.cleanup(bufnr, 1, nil, "fix this", 0)
eq(#vim.diagnostic.get(bufnr, { namespace = diagnostics.ns }), 0)
end)

test("cleanup: shifts later diagnostics by delta", function()
reset()
local bufnr = h.make_buf({ "line0", "line1", "line2", "line3", "line4" })
vim.diagnostic.set(diagnostics.ns, bufnr, {
h.diag(1, W, "replace this", { "new1\nnew2\nnew3" }),
h.diag(3, I, "later issue", { "fix_later" }, 4),
})
-- delta = 3 new lines - 1 old line = +2
actions.cleanup(bufnr, 1, nil, "replace this", 2)
local remaining = vim.diagnostic.get(bufnr, { namespace = diagnostics.ns })
eq(#remaining, 1)
eq(remaining[1].lnum, 5)
eq(remaining[1].end_lnum, 6)
end)

-- ──────────────────────────────────────────────────────────
-- Tests: get_actions – WorkspaceEdit
-- ──────────────────────────────────────────────────────────

test("get_actions: returns edit with WorkspaceEdit", function()
reset()
local bufnr = h.make_buf({ "line0", "line1", "line2" })
vim.diagnostic.set(diagnostics.ns, bufnr, { h.diag(1, W, "issue", { "fixed_line" }) })
local result = actions.get_actions(bufnr, range(1))
eq(#result, 1)
assert(result[1].edit ~= nil, "action should have edit field")
assert(result[1].edit.changes ~= nil, "edit should have changes")
-- Verify TextEdit structure
local uri = vim.uri_from_bufnr(bufnr)
local edits = result[1].edit.changes[uri]
assert(edits ~= nil, "changes should contain buffer URI")
eq(#edits, 1)
eq(edits[1].range.start.line, 1)
eq(edits[1].range["end"].line, 2)
eq(edits[1].newText, "fixed_line\n")
end)

test("get_actions: multi-line edit has correct range and newText", function()
reset()
local bufnr = h.make_buf({ "line0", "line1", "line2", "line3", "line4" })
vim.diagnostic.set(diagnostics.ns, bufnr, { h.diag(1, E, "replace", { "new1\nnew2" }, 3) })
local result = actions.get_actions(bufnr, range(2))
eq(#result, 1)
local uri = vim.uri_from_bufnr(bufnr)
local edit = result[1].edit.changes[uri][1]
eq(edit.range.start.line, 1)
eq(edit.range["end"].line, 4)
eq(edit.newText, "new1\nnew2\n")
end)

test("get_actions: cleanup command has correct delta", function()
reset()
local bufnr = h.make_buf({ "line0", "line1", "line2" })
vim.diagnostic.set(diagnostics.ns, bufnr, { h.diag(1, W, "issue", { "a\nb\nc" }) })
local result = actions.get_actions(bufnr, range(1))
eq(result[1].command.command, "coderabbit.cleanup")
eq(result[1].command.arguments[1].delta, 2) -- 3 new lines - 1 old = +2
end)

-- ──────────────────────────────────────────────────────────
-- Tests: apply – end_lnum disambiguation
-- ──────────────────────────────────────────────────────────
Expand Down
Loading