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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion doc/differ.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion lua/differ/pr/checks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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
Expand Down
29 changes: 16 additions & 13 deletions lua/differ/pr/overview.lua
Original file line number Diff line number Diff line change
@@ -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), <CR> 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, <CR> 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
Expand Down Expand Up @@ -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. <CR> 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.
-- <CR> 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()
Expand Down Expand Up @@ -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
-- <CR>) 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 <CR>) 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
Expand Down Expand Up @@ -207,7 +208,7 @@ local function set_keymaps(b)
-- g?: a floating keymap cheatsheet, dismissed with <Esc> / 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)",
" <CR> thread row: jump into the review here, else open the PR url",
" ]t / [t next / previous thread",
" gx open the PR in the browser",
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions lua/differ/view.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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", {
Expand Down
19 changes: 19 additions & 0 deletions test/nvim/pr_nav_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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("<CR> on a thread row enters the review with the cursor on the anchored diff row", function()
local restore = open_overview(default_responses())

Expand Down
38 changes: 38 additions & 0 deletions test/nvim/pr_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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() },
Expand Down
51 changes: 51 additions & 0 deletions test/nvim/view_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
Loading