Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The first Neovim integration for [CodeRabbit](https://coderabbit.link/sam-natale
- **Review viewer** — read the full review in a floating window or buffer, with findings grouped by file, severity levels, and syntax-highlighted code suggestions
- **Review types** — review all changes, only committed changes, or only uncommitted changes, with optional base branch/commit comparison
- **Review history** — browse and revisit past reviews, persisted to disk across sessions
- **Quickfix integration** — send findings to the quickfix list for fast `:cnext`/`:cprev` navigation
- **Statusline integration** — drop `require("coderabbit").status()` into your statusline for a live spinner while reviews run

## Getting Started
Expand Down Expand Up @@ -49,6 +50,7 @@ Run `:checkhealth coderabbit` to verify everything is wired up.
| `:CodeRabbitClear` | Clear diagnostics |
| `:CodeRabbitShow [id]` | View results (float or buffer). Defaults to the latest review |
| `:CodeRabbitRestore [id]` | Reapply diagnostics from a saved review. Defaults to the most recent |
| `:CodeRabbitQuickfix [id]` | Populate quickfix list with findings |
| `:CodeRabbitHistory` | Browse past reviews |

For your statusline:
Expand Down Expand Up @@ -92,6 +94,9 @@ require("coderabbit").setup({
border = "rounded",
},
},
quickfix = {
auto = false, -- populate on review complete
},
on_review_complete = nil,
})
```
Expand Down
14 changes: 14 additions & 0 deletions doc/coderabbit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ All options are optional. Defaults: >lua
border = "rounded",
},
},
quickfix = {
auto = false,
},
on_review_complete = nil,
})
<
Expand All @@ -87,6 +90,9 @@ show.float.width Fraction of editor width (0-1). Default: `0.6`.
show.float.height Fraction of editor height (0-1). Default: `0.7`.
show.float.border Border style for the floating window. Default: `"rounded"`.

quickfix.auto Populate the quickfix list automatically when a review
completes. Default: `false`.

on_review_complete Callback receiving the findings table when a review
finishes.

Expand Down Expand Up @@ -115,6 +121,11 @@ COMMANDS *coderabbit-commands*
:CodeRabbitHistory *:CodeRabbitHistory*
Browse saved reviews via |vim.ui.select|.

:CodeRabbitQuickfix [id] *:CodeRabbitQuickfix*
Populate the quickfix list with findings. Pass an `id` from
`:CodeRabbitHistory` to load a saved review. Without an `id`,
uses the current review findings. Navigate with |:cnext| and |:cprev|.

==============================================================================
LUA API *coderabbit-api*

Expand All @@ -139,6 +150,9 @@ require("coderabbit").restore({id}) *coderabbit.restore()*
require("coderabbit").history() *coderabbit.history()*
Open the review history picker.

require("coderabbit").quickfix({id}) *coderabbit.quickfix()*
Populate the quickfix list with findings. `nil` = current, number = saved.

require("coderabbit").status() *coderabbit.status()*
Returns `"⠋ CodeRabbit (12s)"` while reviewing, `nil` when idle.
Designed for statusline use.
Expand Down
3 changes: 3 additions & 0 deletions lua/coderabbit/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ M.defaults = {
border = "rounded",
},
},
quickfix = {
auto = false,
},
on_review_complete = nil,
}

Expand Down
4 changes: 4 additions & 0 deletions lua/coderabbit/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ function M.history()
require("coderabbit.history").open()
end

function M.quickfix(id)
require("coderabbit.quickfix").populate(id)
end

function M.status()
return require("coderabbit.review").status()
end
Expand Down
76 changes: 76 additions & 0 deletions lua/coderabbit/quickfix.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
local M = {}

local severity_types = {
[vim.diagnostic.severity.ERROR] = "E",
[vim.diagnostic.severity.WARN] = "W",
}

--- Map vim.diagnostic.severity to quickfix type character.
--- @param severity number
--- @return string "E", "W", or "I"
function M.severity_to_type(severity)
return severity_types[severity] or "I"
end

--- Convert findings to quickfix items (pure function, no side effects).
--- @param findings table[] Array of { diagnostic, filepath }
--- @return table[] Array of { filename, lnum, col, text, type } for setqflist()
function M.findings_to_qf_items(findings)
local items = {}
for _, f in ipairs(findings) do
local d = f.diagnostic
local raw = d.user_data and d.user_data.severity_raw
local prefix = raw and ("[" .. raw .. "] ") or ""
local first_line = d.message:match("^([^\n]*)") or d.message
table.insert(items, {
filename = f.filepath,
lnum = d.lnum + 1,
col = d.col + 1,
text = prefix .. first_line,
type = M.severity_to_type(d.severity),
})
end
return items
end

--- Populate the quickfix list from findings and open the window.
--- @param findings table[] Array of { diagnostic, filepath }
--- @param opts table|nil { title = string }
function M.set(findings, opts)
opts = opts or {}
local items = M.findings_to_qf_items(findings)
vim.fn.setqflist({}, "r", {
title = opts.title or "CodeRabbit Review",
items = items,
})
vim.cmd("copen")
end

--- Populate quickfix from current review or a saved review by ID.
--- @param id number|nil Review ID (nil = current in-memory findings)
function M.populate(id)
local findings, title

local review = require("coderabbit.review")

if id then
local entry = review.get_review(id)
if not entry then
vim.notify("CodeRabbit: Review #" .. id .. " not found", vim.log.levels.WARN)
return
end
findings = entry.findings or {}
title = "CodeRabbit Review #" .. id
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
else
findings = review.get_results()
if #findings == 0 and not review.get_context() then
vim.notify("CodeRabbit: No review results. Run :CodeRabbitReview first", vim.log.levels.WARN)
return
end
title = "CodeRabbit Review"
end

M.set(findings, { title = title })
end

return M
6 changes: 6 additions & 0 deletions lua/coderabbit/review.lua
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ function M.run(opts)

storage.save(state.findings, M.get_context())

if cfg.quickfix.auto and #state.findings > 0 then
require("coderabbit.quickfix").set(state.findings, {
title = "CodeRabbit Review",
})
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if cfg.on_review_complete then
cfg.on_review_complete(state.findings)
end
Expand Down
12 changes: 12 additions & 0 deletions plugin/coderabbit.lua
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,15 @@ vim.api.nvim_create_user_command("CodeRabbitHistory", function()
end, {
desc = "Browse CodeRabbit review history",
})

vim.api.nvim_create_user_command("CodeRabbitQuickfix", function(args)
ensure_setup()
local id = args.fargs[1] and tonumber(args.fargs[1]) or nil
require("coderabbit").quickfix(id)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
end, {
nargs = "?",
complete = function()
return require("coderabbit.storage").ids()
end,
desc = "Populate quickfix list with CodeRabbit findings (optional: review ID)",
})
135 changes: 135 additions & 0 deletions tests/coderabbit/quickfix_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
local quickfix = require("coderabbit.quickfix")
local h = require("tests.helpers")
local test, eq = h.test, h.eq
local E, W, I = h.E, h.W, h.I

-- ──────────────────────────────────────────────────────────
-- Tests: severity_to_type
-- ──────────────────────────────────────────────────────────

test("severity_to_type: ERROR -> E", function()
eq(quickfix.severity_to_type(E), "E")
end)

test("severity_to_type: WARN -> W", function()
eq(quickfix.severity_to_type(W), "W")
end)

test("severity_to_type: INFO -> I", function()
eq(quickfix.severity_to_type(I), "I")
end)

test("severity_to_type: HINT -> I (fallback)", function()
eq(quickfix.severity_to_type(vim.diagnostic.severity.HINT), "I")
end)

-- ──────────────────────────────────────────────────────────
-- Tests: findings_to_qf_items
-- ──────────────────────────────────────────────────────────

test("findings_to_qf_items: empty findings returns empty", function()
local items = quickfix.findings_to_qf_items({})
eq(#items, 0)
end)

test("findings_to_qf_items: single finding produces correct entry", function()
local findings = { h.finding("/tmp/repo/foo.lua", 41, E, "null check", {}) }
local items = quickfix.findings_to_qf_items(findings)
eq(#items, 1)
eq(items[1].filename, "/tmp/repo/foo.lua")
eq(items[1].lnum, 42) -- 0-indexed -> 1-indexed
eq(items[1].col, 1) -- col 0 -> 1
eq(items[1].type, "E")
end)

test("findings_to_qf_items: lnum 0 becomes 1", function()
local findings = { h.finding("/tmp/repo/a.lua", 0, I, "file-level issue") }
local items = quickfix.findings_to_qf_items(findings)
eq(items[1].lnum, 1)
end)

test("findings_to_qf_items: severity maps correctly", function()
local findings = {
h.finding("/tmp/repo/a.lua", 0, E, "error"),
h.finding("/tmp/repo/b.lua", 0, W, "warn"),
h.finding("/tmp/repo/c.lua", 0, I, "info"),
}
local items = quickfix.findings_to_qf_items(findings)
eq(items[1].type, "E")
eq(items[2].type, "W")
eq(items[3].type, "I")
end)

test("findings_to_qf_items: text includes severity_raw prefix", function()
local findings = { h.finding("/tmp/repo/a.lua", 10, W, "missing import") }
local items = quickfix.findings_to_qf_items(findings)
-- helpers.finding sets severity_raw = "minor" by default
eq(items[1].text, "[minor] missing import")
end)

test("findings_to_qf_items: multi-line message uses first line only", function()
local findings = { h.finding("/tmp/repo/a.lua", 5, E, "first line\nsecond line\nthird") }
local items = quickfix.findings_to_qf_items(findings)
eq(items[1].text, "[minor] first line")
end)

test("findings_to_qf_items: missing severity_raw omits prefix", function()
local findings = {
{
filepath = "/tmp/repo/a.lua",
diagnostic = {
lnum = 0,
col = 0,
severity = E,
message = "bare finding",
source = "coderabbit",
},
},
}
local items = quickfix.findings_to_qf_items(findings)
eq(items[1].text, "bare finding")
end)

test("findings_to_qf_items: multiple findings produce correct count", function()
local findings = {
h.finding("/tmp/repo/a.lua", 1, E, "one"),
h.finding("/tmp/repo/b.lua", 2, W, "two"),
h.finding("/tmp/repo/c.lua", 3, I, "three"),
}
local items = quickfix.findings_to_qf_items(findings)
eq(#items, 3)
end)

-- ──────────────────────────────────────────────────────────
-- Tests: set
-- ──────────────────────────────────────────────────────────

test("set: populates quickfix list with items", function()
local findings = {
h.finding("/tmp/repo/a.lua", 10, E, "error here"),
h.finding("/tmp/repo/b.lua", 20, W, "warning here"),
}
quickfix.set(findings, { title = "Test Review" })
vim.cmd("cclose")
local qf = vim.fn.getqflist({ title = 1, items = 1 })
eq(qf.title, "Test Review")
eq(#qf.items, 2)
end)

test("set: empty findings clears quickfix list", function()
quickfix.set({ h.finding("/tmp/repo/a.lua", 0, E, "x") })
quickfix.set({})
vim.cmd("cclose")
local qf = vim.fn.getqflist({ items = 1 })
eq(#qf.items, 0)
end)

test("set: replaces existing quickfix content", function()
quickfix.set({ h.finding("/tmp/repo/a.lua", 0, E, "first") })
quickfix.set({ h.finding("/tmp/repo/b.lua", 1, W, "second") })
vim.cmd("cclose")
local qf = vim.fn.getqflist({ items = 1 })
eq(#qf.items, 1)
end)

h.summary()
Loading