diff --git a/lua/coderabbit/actions.lua b/lua/coderabbit/actions.lua index 0af6ef2..3c021ea 100644 --- a/lua/coderabbit/actions.lua +++ b/lua/coderabbit/actions.lua @@ -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 @@ -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 = {} @@ -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, }, }, }, @@ -98,7 +127,7 @@ function M.attach(bufnr) capabilities = { codeActionProvider = true, executeCommandProvider = { - commands = { "coderabbit.apply" }, + commands = { "coderabbit.apply", "coderabbit.cleanup" }, }, }, }) @@ -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 diff --git a/tests/coderabbit/actions_spec.lua b/tests/coderabbit/actions_spec.lua index 825af4d..f090299 100644 --- a/tests/coderabbit/actions_spec.lua +++ b/tests/coderabbit/actions_spec.lua @@ -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 -- ──────────────────────────────────────────────────────────