diff --git a/CHANGELOG.md b/CHANGELOG.md index baf4006..d9f774f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Changed + +- The PR overview's `g?` cheatsheet now correctly says that `r` starts a draft review, whilst `e` only enters the review + +### Fixed + +- The diff drew a cursor line even with `cursorline` off and the PR overview drew none with it on; both now respect the user's config. Opening the PR checks float used to switch `cursorline` on for the rest of the session, it now doesn't + +## [0.1.41] — 2026-09-06 + ### Fixed - A renamed file in a PR review showed every line as new, which wasn't very useful. It now diffs against the file's old path, showing only what actually changed diff --git a/README.md b/README.md index 999aa90..4b8e6ec 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ require("differ").setup({ context = math.huge, -- fold threshold; math.huge = whole file, no folds wrap = true, -- soft-wrap long lines in the diff view diff_counter = true, -- hunk counter in the diff window's winbar - cursorline_tint = true, -- allow the cursorline to inherit the add/delete highlights + cursorline_tint = true, -- add/delete tint on the cursor line; only drawn when 'cursorline' is on deep_diff = { -- word-level diffing enabled = true, granularity = "word", -- "word" | "char" diff --git a/doc/differ.txt b/doc/differ.txt index 4c87ab5..2e13bd5 100644 --- a/doc/differ.txt +++ b/doc/differ.txt @@ -156,7 +156,7 @@ you’re in doubt, `:Differ sidecar` reports which one is actually running. context = math.huge, -- fold threshold; math.huge = whole file, no folds wrap = true, -- soft-wrap long lines in the diff view diff_counter = true, -- hunk counter in the diff window's winbar - cursorline_tint = true, -- allow the cursorline to inherit the add/delete highlights + cursorline_tint = true, -- add/delete tint on the cursor line; only drawn when 'cursorline' is on deep_diff = { -- word-level diffing enabled = true, granularity = "word", -- "word" | "char" diff --git a/lua/differ/pr/checks.lua b/lua/differ/pr/checks.lua index fe981de..202008d 100644 --- a/lua/differ/pr/checks.lua +++ b/lua/differ/pr/checks.lua @@ -3,6 +3,7 @@ -- vim.ui.open. no rerun/dispatch (reserved); the sidecar fetch is the only I/O local client = require("differ.pr.client") +local set_wo = require("differ.util.win").set_local local M = {} @@ -122,7 +123,7 @@ local function render(checks) title = title, title_pos = "center", }) - vim.wo[win].cursorline = true + set_wo(win, "cursorline", true) local function close() if vim.api.nvim_win_is_valid(win) then diff --git a/lua/differ/pr/overview.lua b/lua/differ/pr/overview.lua index 9fdd407..0d98e04 100644 --- a/lua/differ/pr/overview.lua +++ b/lua/differ/pr/overview.lua @@ -1,10 +1,10 @@ -- the PR overview home: a read-only pre-review page with the PR summary + a -- minimal timeline (conversation comments, submitted review verdicts, and code -- threads). it is a step *before* the review proper — no file panel, just a dedicated --- page filling the session tab. e/r enter the review (build the panel + diff), on --- a thread row enters it at that thread's file/line, q backs into an in-progress --- review. the pure layout --- lives in ui/overview.lua; this owns the vim surface (buffer, window, extmarks, +-- page filling the session tab. e enters the review (builds the panel + diff), r enters +-- and also starts a github draft review, on a thread row enters at that thread's +-- file/line, q backs into an in-progress review. the pure layout lives in +-- ui/overview.lua; this owns the vim surface (buffer, window, extmarks, -- fetches). get_timeline is a round-trip and threads are ensured (shared, PR-wide, -- also feeding the header count) — the meta comes from session.pr_meta (enriched in -- pr/init), checks reuse slice 5 @@ -124,10 +124,10 @@ local function arm_guard(session, win) end -- buffer-local keymaps; they read the live session each time so the reused buffer never --- acts on a stale session. e enters the review (files), r enters + starts a review, q --- backs into the review when one is open, gx opens the PR url. on a thread row --- enters the review at that thread's file/line; elsewhere it opens the url. ]t/[t hop --- between thread boxes; g? floats the keymap cheatsheet +-- acts on a stale session. e enters the review (panel + diff), r enters and also starts +-- a github draft review, q backs into the review when one is open, gx opens the PR url. +-- on a thread row enters the review at that thread's file/line; elsewhere it opens +-- the url. ]t/[t hop between thread boxes; g? floats the keymap cheatsheet ---@param b integer local function set_keymaps(b) local function live() @@ -166,9 +166,10 @@ local function set_keymaps(b) end open_url() end - -- e/r: enter the review. on a thread row, jump into that thread's file/line (like - -- ) so the comment's diff opens directly; elsewhere enter at the panel's file. r - -- also starts a review either way + -- e/r: enter the review (panel + diff). on a thread row, jump into that thread's + -- file/line (like ) so the comment's diff opens directly; elsewhere enter at the + -- panel's file. r also starts a github draft review either way, so comments become + -- drafts submitted as one batch rather than posting on the spot local function enter(review) local s = live() if not s then @@ -207,7 +208,7 @@ local function set_keymaps(b) -- g?: a floating keymap cheatsheet, dismissed with / q / g? local function show_help() require("differ.ui.help").show({ - " e / r enter review / enter + start a review (thread row: at its file)", + " e / r enter review / enter + start a draft review (thread row: at its file)", " thread row: jump into the review here, else open the PR url", " ]t / [t next / previous thread", " gx open the PR in the browser", @@ -252,7 +253,9 @@ local function setup_window(win) set_wo(win, "relativenumber", false) set_wo(win, "signcolumn", "no") set_wo(win, "foldcolumn", "0") - set_wo(win, "cursorline", false) + -- the window is taken over from another surface, so mirror the global rather + -- than inherit whatever local cursorline it was left with + set_wo(win, "cursorline", vim.go.cursorline) set_wo(win, "wrap", true) set_wo(win, "conceallevel", 2) set_wo(win, "list", false) diff --git a/lua/differ/view.lua b/lua/differ/view.lua index 4e33778..8268fb2 100644 --- a/lua/differ/view.lua +++ b/lua/differ/view.lua @@ -391,6 +391,11 @@ function View:_paint_cursorline() if not (win and vim.api.nvim_win_is_valid(win)) then return end + -- read the window's effective value, so a :setlocal cursorline inside the diff + -- still paints while a global nocursorline leaves the rows clean + if not vim.api.nvim_get_option_value("cursorline", { scope = "local", win = win }) then + return + end local row = vim.api.nvim_win_get_cursor(win)[1] - 1 local line = col.map.lines[row + 1] -- tint the cursor line by the row's change kind so the add/remove colour survives @@ -622,6 +627,18 @@ function View:_setup_window(winid, bufnr) end end, }) + -- toggling cursorline mid-session repaints straight away instead of waiting for + -- the next cursor move. OptionSet takes the option name as its pattern, so it + -- can't also be buffer-scoped: skip the fire unless this view holds focus + vim.api.nvim_create_autocmd("OptionSet", { + group = group, + pattern = "cursorline", + callback = function() + if vim.api.nvim_get_current_buf() == bufnr then + self:_paint_cursorline() + end + end, + }) local km = self.keymaps bind(bufnr, km.next_hunk, function() self:goto_hunk("next", { diff --git a/test/nvim/pr_nav_spec.lua b/test/nvim/pr_nav_spec.lua index 749e26e..913468e 100644 --- a/test/nvim/pr_nav_spec.lua +++ b/test/nvim/pr_nav_spec.lua @@ -198,6 +198,25 @@ describe("pr overview <-> review navigation loop", function() restore() end) + it("the overview page follows the cursorline setting", function() + local had_cursorline = vim.go.cursorline + + vim.o.cursorline = true + local restore = open_overview(default_responses()) + local win = pr.current_session().overview_win + assert.is_true(vim.api.nvim_get_option_value("cursorline", { scope = "local", win = win })) + pr.end_session() + restore() + + vim.o.cursorline = false + restore = open_overview(default_responses()) + win = pr.current_session().overview_win + assert.is_false(vim.api.nvim_get_option_value("cursorline", { scope = "local", win = win })) + restore() + + vim.o.cursorline = had_cursorline + end) + it(" on a thread row enters the review with the cursor on the anchored diff row", function() local restore = open_overview(default_responses()) diff --git a/test/nvim/pr_spec.lua b/test/nvim/pr_spec.lua index eb39d02..579b041 100644 --- a/test/nvim/pr_spec.lua +++ b/test/nvim/pr_spec.lua @@ -201,6 +201,44 @@ describe("pr session notifies", function() restore() end) + it("leaves the global cursorline untouched when the checks float opens", function() + local restore = stub_sidecar({ + get_pr = { result = get_pr_result() }, + get_file_versions = { + result = { base = { content = "a\n" }, head = { content = "b\n" } }, + }, + get_checks = { + result = { + rollup = "SUCCESS", + checks = { { name = "build", status = "COMPLETED", conclusion = "SUCCESS" } }, + }, + }, + }) + + local had_cursorline = vim.go.cursorline + vim.o.cursorline = false -- :set, so the global default goes off too + + pr.show(PR) + assert.is_true(vim.wait(1000, function() + return pr.current_session() ~= nil + end)) + + pr.checks() + assert.is_true(vim.wait(1000, function() + return vim.api.nvim_win_get_config(0).relative ~= "" + end)) + + local float = vim.api.nvim_get_current_win() + -- the float still carries its own cursor line, set :setlocal + assert.is_true( + vim.api.nvim_get_option_value("cursorline", { scope = "local", win = float }) + ) + assert.is_false(vim.go.cursorline) -- but the global default is left alone + + vim.o.cursorline = had_cursorline + restore() + end) + it("notifies 'no thread on this line' toggling a thread off a plain diff line", function() local restore = stub_sidecar({ get_pr = { result = get_pr_result() }, diff --git a/test/nvim/view_spec.lua b/test/nvim/view_spec.lua index ce60d47..f51aad9 100644 --- a/test/nvim/view_spec.lua +++ b/test/nvim/view_spec.lua @@ -1031,6 +1031,16 @@ end) describe("view cursor-line overlay", function() local cursor_ns = vim.api.nvim_create_namespace("differ.cursorline") + -- the overlay follows the window's cursorline, and nvim ships it off + local had_cursorline + before_each(function() + had_cursorline = vim.o.cursorline + vim.o.cursorline = true + end) + after_each(function() + vim.o.cursorline = had_cursorline + end) + local CURSORLINE = { differCursorLine = true, differCursorLineAdd = true, @@ -1150,6 +1160,47 @@ describe("view cursor-line overlay", function() assert.are.equal("differCursorLine", overlay_hl(buf, chg_row)) -- no tint v:close() end) + + it("paints no overlay when cursorline is off", function() + vim.cmd("silent! only") + vim.o.cursorline = false + local v = View.new(model("a\nb\nc\n", "a\nB\nc\n"), { + layout = "stacked", + context = math.huge, + deep_diff = { enabled = true }, + }) + v:open() + local buf, win = v.columns[1].bufnr, v.columns[1].winid + assert.are.same({}, overlay_rows(buf)) + + vim.api.nvim_win_set_cursor(win, { 1, 0 }) + vim.api.nvim_exec_autocmds("CursorMoved", { buffer = buf }) + assert.are.same({}, overlay_rows(buf)) -- and stays clean as the cursor moves + v:close() + end) + + it("repaints when cursorline is toggled on the diff window mid-session", function() + vim.cmd("silent! only") + vim.o.cursorline = false + local v = View.new(model("a\nb\nc\n", "a\nB\nc\n"), { + layout = "stacked", + context = math.huge, + deep_diff = { enabled = true }, + }) + v:open() + local buf, win = v.columns[1].bufnr, v.columns[1].winid + assert.are.same({}, overlay_rows(buf)) + + -- a :setlocal cursorline inside the diff paints without a cursor move + vim.api.nvim_set_option_value("cursorline", true, { scope = "local", win = win }) + local cur = vim.api.nvim_win_get_cursor(win)[1] - 1 + assert.are.same({ cur }, overlay_rows(buf)) + + -- and turning it back off wipes the overlay already painted + vim.api.nvim_set_option_value("cursorline", false, { scope = "local", win = win }) + assert.are.same({}, overlay_rows(buf)) + v:close() + end) end) describe("view close guard", function()