diff --git a/README.md b/README.md index ef9b147..c647578 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Run `:checkhealth coderabbit` to verify everything is wired up. | `:CodeRabbitReview [type]` | Run a review. Defaults to `all`, or pass `committed`/`uncommitted` | | `:CodeRabbitStop` | Cancel a running review | | `:CodeRabbitClear` | Clear diagnostics | -| `:CodeRabbitShow [id]` | View results in a split. Defaults to the latest review | +| `:CodeRabbitShow [id]` | View results (float or buffer). Defaults to the latest review | | `:CodeRabbitHistory` | Browse past reviews | For your statusline: @@ -73,6 +73,14 @@ require("coderabbit").setup({ signs = true, underline = true, }, + show = { + layout = "float", -- "float" or "buffer" + float = { + width = 0.6, + height = 0.7, + border = "rounded", + }, + }, on_review_complete = nil, }) ``` diff --git a/doc/coderabbit.txt b/doc/coderabbit.txt index 08e7dfc..ab09994 100644 --- a/doc/coderabbit.txt +++ b/doc/coderabbit.txt @@ -54,6 +54,14 @@ All options are optional. Defaults: >lua signs = true, underline = true, }, + show = { + layout = "float", + float = { + width = 0.6, + height = 0.7, + border = "rounded", + }, + }, on_review_complete = nil, }) < @@ -72,6 +80,13 @@ diagnostics.virtual_text Show inline virtual text. diagnostics.signs Show sign column indicators. diagnostics.underline Underline diagnostic ranges. +show.layout `"float"` or `"buffer"`. Default: `"float"`. + `"float"` opens a centered floating window. + `"buffer"` replaces the current buffer (oil.nvim style). +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"`. + on_review_complete Callback receiving the findings table when a review finishes. @@ -88,8 +103,9 @@ COMMANDS *coderabbit-commands* Clear all CodeRabbit diagnostics. :CodeRabbitShow [id] *:CodeRabbitShow* - Open review results in a vertical split. Pass an `id` from - `:CodeRabbitHistory` to view a saved review. Press `q` to close. + Open review results. Display mode is controlled by `show.layout`. + Pass an `id` from `:CodeRabbitHistory` to view a saved review. + Press `q` to close. :CodeRabbitHistory *:CodeRabbitHistory* Browse saved reviews via |vim.ui.select|. diff --git a/lua/coderabbit/config.lua b/lua/coderabbit/config.lua index 097d1ad..00d17f3 100644 --- a/lua/coderabbit/config.lua +++ b/lua/coderabbit/config.lua @@ -22,6 +22,14 @@ M.defaults = { signs = true, underline = true, }, + show = { + layout = "float", + float = { + width = 0.6, + height = 0.7, + border = "rounded", + }, + }, on_review_complete = nil, } diff --git a/lua/coderabbit/show.lua b/lua/coderabbit/show.lua index 782b707..8800b55 100644 --- a/lua/coderabbit/show.lua +++ b/lua/coderabbit/show.lua @@ -32,6 +32,45 @@ local function line_label(diag) return nil end +local function set_win_opts(winid) + vim.api.nvim_set_option_value("number", false, { win = winid }) + vim.api.nvim_set_option_value("relativenumber", false, { win = winid }) + vim.api.nvim_set_option_value("signcolumn", "no", { win = winid }) + vim.api.nvim_set_option_value("wrap", true, { win = winid }) + vim.api.nvim_set_option_value("linebreak", true, { win = winid }) + vim.api.nvim_set_option_value("spell", false, { win = winid }) + vim.api.nvim_set_option_value("conceallevel", 2, { win = winid }) +end + +local function open_float(buf, float_cfg) + local width = math.floor(vim.o.columns * float_cfg.width) + local height = math.floor(vim.o.lines * float_cfg.height) + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + style = "minimal", + border = float_cfg.border, + title = " CodeRabbit Review ", + title_pos = "center", + }) +end + +local function open_window(buf) + local cfg = require("coderabbit.config").get().show + local layout = cfg.layout + + if layout == "buffer" then + vim.api.nvim_win_set_buf(0, buf) + else + open_float(buf, cfg.float) + end +end + --- Render findings and context into an array of markdown lines. --- @param findings table[] Array of { diagnostic, filepath } --- @param context table|nil Review context metadata @@ -172,8 +211,8 @@ function M.open(id) if winid ~= -1 then vim.api.nvim_set_current_win(winid) else - vim.cmd("vsplit") - vim.api.nvim_win_set_buf(0, buf_id) + open_window(buf_id) + set_win_opts(0) end vim.api.nvim_set_option_value("modifiable", true, { buf = buf_id }) vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, content) @@ -181,26 +220,20 @@ function M.open(id) return end + local layout = require("coderabbit.config").get().show.layout + buf_id = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_name(buf_id, "coderabbit://review") vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf_id }) - vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf_id }) + vim.api.nvim_set_option_value("bufhidden", layout == "buffer" and "hide" or "wipe", { buf = buf_id }) vim.api.nvim_set_option_value("swapfile", false, { buf = buf_id }) vim.api.nvim_set_option_value("filetype", "markdown", { buf = buf_id }) vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, content) vim.api.nvim_set_option_value("modifiable", false, { buf = buf_id }) - vim.cmd("vsplit") - vim.api.nvim_win_set_buf(0, buf_id) - - vim.api.nvim_set_option_value("number", false, { win = 0 }) - vim.api.nvim_set_option_value("relativenumber", false, { win = 0 }) - vim.api.nvim_set_option_value("signcolumn", "no", { win = 0 }) - vim.api.nvim_set_option_value("wrap", true, { win = 0 }) - vim.api.nvim_set_option_value("linebreak", true, { win = 0 }) - vim.api.nvim_set_option_value("spell", false, { win = 0 }) - vim.api.nvim_set_option_value("conceallevel", 2, { win = 0 }) + open_window(buf_id) + set_win_opts(0) vim.keymap.set("n", "q", function() M.close() diff --git a/tests/coderabbit/show_spec.lua b/tests/coderabbit/show_spec.lua index 3a59461..cb72451 100644 --- a/tests/coderabbit/show_spec.lua +++ b/tests/coderabbit/show_spec.lua @@ -259,4 +259,104 @@ test("is_open: false when no buffer, true when visible", function() end) end) +-- ────────────────────────────────────────────────────────── +-- Tests: layout modes +-- ────────────────────────────────────────────────────────── + +local config = require("coderabbit.config") + +local function with_layout(layout, fn) + local prev = config.get().show.layout + config.get().show.layout = layout + local ok, err = pcall(fn) + config.get().show.layout = prev + if not ok then + error(err, 2) + end +end + +test("layout float: opens floating window", function() + with_layout("float", function() + with_review(one_finding, one_ctx, false, function() + show.open() + local bufnr = show._get_buf_id() + assert(bufnr and vim.api.nvim_buf_is_valid(bufnr), "expected valid buffer") + local winid = vim.fn.bufwinid(bufnr) + assert(winid ~= -1, "expected buffer in a window") + local win_cfg = vim.api.nvim_win_get_config(winid) + eq(win_cfg.relative, "editor") + assert(win_cfg.width > 0, "expected positive width") + assert(win_cfg.height > 0, "expected positive height") + end) + end) +end) + +test("layout float: close removes floating window", function() + with_layout("float", function() + with_review(one_finding, one_ctx, false, function() + show.open() + eq(show.is_open(), true) + show.close() + eq(show.is_open(), false) + eq(show._get_buf_id(), nil) + end) + end) +end) + +test("layout buffer: opens in current window (no split or float)", function() + with_layout("buffer", function() + with_review(one_finding, one_ctx, false, function() + local win_before = vim.api.nvim_get_current_win() + local win_count_before = #vim.api.nvim_list_wins() + show.open() + local bufnr = show._get_buf_id() + assert(bufnr and vim.api.nvim_buf_is_valid(bufnr), "expected valid buffer") + eq(vim.api.nvim_get_current_win(), win_before) + eq(#vim.api.nvim_list_wins(), win_count_before) + local win_cfg = vim.api.nvim_win_get_config(vim.fn.bufwinid(bufnr)) + eq(win_cfg.relative, "") + end) + end) +end) + +test("layout buffer: bufhidden is hide", function() + with_layout("buffer", function() + with_review(one_finding, one_ctx, false, function() + show.open() + local bufnr = show._get_buf_id() + eq(vim.api.nvim_get_option_value("bufhidden", { buf = bufnr }), "hide") + end) + end) +end) + +test("layout float: bufhidden is wipe", function() + with_layout("float", function() + with_review(one_finding, one_ctx, false, function() + show.open() + local bufnr = show._get_buf_id() + eq(vim.api.nvim_get_option_value("bufhidden", { buf = bufnr }), "wipe") + end) + end) +end) + +test("layout float: q keymap closes float", function() + with_layout("float", function() + with_review(one_finding, { cwd = CWD }, false, function() + show.open() + local keymaps = vim.api.nvim_buf_get_keymap(show._get_buf_id(), "n") + local found = false + for _, km in ipairs(keymaps) do + if km.lhs == "q" then + found = true + end + end + assert(found, "expected q keymap") + end) + end) +end) + +test("default layout is float", function() + eq(config.defaults.show.layout, "float") +end) + h.summary()