refactor(diffsplit): clarify diff side API

This commit is contained in:
2026-05-29 14:22:24 +02:00
parent f0ae3fc656
commit 193616e87d
6 changed files with 262 additions and 55 deletions
+116 -30
View File
@@ -6,13 +6,15 @@ local util = require("git.core.util")
local M = {}
---@class ow.Git.Diffsplit.OpenOpts
---@field target string?
---@field other string?
---@field layout ("vertical"|"horizontal")?
---@field mods vim.api.keyset.cmd.mods?
---@field focus ("current"|"other")?
---@param cur_buf integer
---@return string? target
---@return string? other
---@return string? err
local function infer_target(cur_buf)
local function infer_other(cur_buf)
local cur_name = vim.api.nvim_buf_get_name(cur_buf)
local cur_rev = object.parse_uri(cur_name)
if cur_rev then
@@ -48,16 +50,16 @@ local function infer_target(cur_buf)
return object.format_uri(Revision.new({ stage = 0, path = rel })), nil
end
---@param target string
---@param other string
---@param cur_buf integer
---@return string? resolved
---@return string? err
local function resolve_target(target, cur_buf)
if vim.startswith(target, object.URI_PREFIX) then
return target, nil
local function resolve_other(other, cur_buf)
if vim.startswith(other, object.URI_PREFIX) then
return other, nil
end
if vim.fn.filereadable(target) == 1 then
return target, nil
if vim.fn.filereadable(other) == 1 then
return other, nil
end
local cur_name = vim.api.nvim_buf_get_name(cur_buf)
local cur_rev = object.parse_uri(cur_name)
@@ -78,57 +80,141 @@ local function resolve_target(target, cur_buf)
if not rel then
return nil, "current buffer has no path"
end
if not r:rev_parse(target, true) then
return nil, "invalid rev: " .. target
if not r:rev_parse(other, true) then
return nil, "invalid rev: " .. other
end
return object.format_uri(Revision.new({ base = target, path = rel })), nil
return object.format_uri(Revision.new({ base = other, path = rel })), nil
end
---@param cur_buf integer
---@param target string
---@param other string
---@return 'aboveleft'|'belowright'|nil
local function default_split(cur_buf, target)
local function default_split(cur_buf, other)
local cur_rev = object.parse_uri(vim.api.nvim_buf_get_name(cur_buf))
local target_rev = object.parse_uri(target)
if not cur_rev and target_rev then
local other_rev = object.parse_uri(other)
if not cur_rev and other_rev then
return "aboveleft"
end
if cur_rev and not target_rev then
if cur_rev and not other_rev then
return "belowright"
end
if cur_rev and target_rev then
if cur_rev.stage == 0 and target_rev.base then
if cur_rev and other_rev then
if cur_rev.stage == 0 and other_rev.base then
return "aboveleft"
end
if cur_rev.base and target_rev.stage == 0 then
if cur_rev.base and other_rev.stage == 0 then
return "belowright"
end
end
return nil
end
---@alias ow.Git.Diffsplit.Side string|integer
---@class ow.Git.Diffsplit.OpenPairOpts
---@field layout ("vertical"|"horizontal")?
---@field mods vim.api.keyset.cmd.mods?
---@field focus ("old"|"new")?
---@param mods vim.api.keyset.cmd.mods?
---@param layout ("vertical"|"horizontal")?
---@return vim.api.keyset.cmd.mods
local function layout_mods(mods, layout)
mods = vim.tbl_extend("force", {}, mods or {})
if mods.vertical == nil then
mods.vertical = layout ~= "horizontal"
end
return mods
end
---@param side ow.Git.Diffsplit.Side
---@param cur_buf integer
---@return string? name
---@return integer? buf
---@return string? err
local function resolve_side(side, cur_buf)
if type(side) == "number" then
local name = vim.api.nvim_buf_get_name(side)
if name == "" then
return nil, nil, "diff side buffer has no name"
end
return name, side, nil
end
local name, err = resolve_other(side, cur_buf)
return name, nil, err
end
---@param side ow.Git.Diffsplit.Side
---@param cur_buf integer
---@return integer? buf
---@return string? err
local function buf_for_side(side, cur_buf)
local name, buf, err = resolve_side(side, cur_buf)
if not name then
return nil, err
end
if buf then
return buf, nil
end
buf = vim.fn.bufadd(name)
vim.fn.bufload(buf)
return buf, nil
end
---@param opts? ow.Git.Diffsplit.OpenOpts
function M.open(opts)
opts = opts or {}
local cur_buf = vim.api.nvim_get_current_buf()
local target, err
if opts.target then
target, err = resolve_target(opts.target, cur_buf)
local other, err
if opts.other then
other, err = resolve_other(opts.other, cur_buf)
else
target, err = infer_target(cur_buf)
other, err = infer_other(cur_buf)
end
if not target then
util.error("%s", err or "no diff target")
if not other then
util.error("%s", err or "no diff side")
return
end
local mods = opts.mods
if not mods or mods.split == nil then
local placement = default_split(cur_buf, target)
local mods = layout_mods(opts.mods, opts.layout)
if mods.split == nil then
local placement = default_split(cur_buf, other)
if placement then
mods = vim.tbl_extend("force", mods or {}, { split = placement })
end
end
vim.cmd.diffsplit({ args = { target }, mods = mods })
local cur_win = vim.api.nvim_get_current_win()
vim.cmd.diffsplit({ args = { other }, mods = mods })
if opts.focus == "current" and vim.api.nvim_win_is_valid(cur_win) then
vim.api.nvim_set_current_win(cur_win)
end
end
---@param old ow.Git.Diffsplit.Side
---@param new ow.Git.Diffsplit.Side
---@param opts? ow.Git.Diffsplit.OpenPairOpts
function M.open_pair(old, new, opts)
opts = opts or {}
local cur_buf = vim.api.nvim_get_current_buf()
local new_buf, err = buf_for_side(new, cur_buf)
if not new_buf then
util.error("%s", err or "no new diff side")
return
end
local old_name, _, old_err = resolve_side(old, cur_buf)
if not old_name then
util.error("%s", old_err or "no old diff side")
return
end
vim.cmd.normal({ "m'", bang = true })
vim.api.nvim_set_current_buf(new_buf)
local mods = layout_mods(opts.mods, opts.layout)
mods.split = mods.split or "aboveleft"
vim.cmd.diffsplit({ args = { old_name }, mods = mods })
if opts.focus ~= "old" then
vim.cmd.wincmd("p")
end
end
return M
+3 -5
View File
@@ -395,11 +395,9 @@ local function open_section(r, commit, section)
local left = side(r, commit, true, section.blob_a, section.path_a)
local right = side(r, commit, false, section.blob_b, section.path_b)
if left and right then
vim.cmd.normal({ "m'", bang = true })
vim.api.nvim_set_current_buf(right)
require("git.diffsplit").open({
target = vim.api.nvim_buf_get_name(left),
mods = { vertical = true },
require("git.diffsplit").open_pair(left, right, {
layout = "vertical",
focus = "new",
})
return
end
+3 -9
View File
@@ -391,17 +391,11 @@ local function view_row(s, row, focus_left)
---@cast left ow.Git.StatusView.Pane
---@cast right ow.Git.StatusView.Pane
vim.api.nvim_win_set_buf(target, right.buf)
if right.name then
util.set_buf_name(right.buf, right.name)
end
local older = left.name or vim.api.nvim_buf_get_name(left.buf)
local left_win
vim.api.nvim_win_call(target, function()
diffsplit.open({
target = older,
mods = { vertical = true },
diffsplit.open_pair(left.buf, right.buf, {
layout = "vertical",
focus = "old",
})
left_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_cursor(left_win, { 1, 0 })
+10 -10
View File
@@ -183,16 +183,16 @@ local DIFF_DIRECTIONS = { "vertical", "horizontal" }
vim.api.nvim_create_user_command("Gdiffsplit", function(opts)
local fargs = opts.fargs
local mods = nil
local layout = nil
local rev_idx = 1
if fargs[1] == "vertical" then
mods = { vertical = true }
layout = "vertical"
rev_idx = 2
elseif fargs[1] == "horizontal" then
mods = { vertical = false }
layout = "horizontal"
rev_idx = 2
end
require("git.diffsplit").open({ target = fargs[rev_idx], mods = mods })
require("git.diffsplit").open({ other = fargs[rev_idx], layout = layout })
end, {
nargs = "*",
complete = function(arg_lead, cmd_line, _)
@@ -253,21 +253,21 @@ vim.keymap.set("n", "<Plug>(git-edit)", function()
end, { silent = true, desc = "Edit a git object" })
vim.keymap.set("n", "<Plug>(git-diffsplit-vertical)", function()
require("git.diffsplit").open({ mods = { vertical = true } })
require("git.diffsplit").open({ layout = "vertical" })
end, { silent = true, desc = "Open a diff split against index (vertical)" })
vim.keymap.set("n", "<Plug>(git-diffsplit-horizontal)", function()
require("git.diffsplit").open({ mods = { vertical = false } })
require("git.diffsplit").open({ layout = "horizontal" })
end, { silent = true, desc = "Open a diff split against index (horizontal)" })
vim.keymap.set("n", "<Plug>(git-diffsplit-vertical-head)", function()
require("git.diffsplit").open({
target = "HEAD",
mods = { vertical = true },
other = "HEAD",
layout = "vertical",
})
end, { silent = true, desc = "Open a diff split against HEAD (vertical)" })
vim.keymap.set("n", "<Plug>(git-diffsplit-horizontal-head)", function()
require("git.diffsplit").open({
target = "HEAD",
mods = { vertical = false },
other = "HEAD",
layout = "horizontal",
})
end, { silent = true, desc = "Open a diff split against HEAD (horizontal)" })
+129
View File
@@ -0,0 +1,129 @@
local Revision = require("git.core.revision")
local h = require("test.git.helpers")
local t = require("test")
---@return integer[]
local function diff_wins()
local wins = {}
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if vim.wo[win].diff then
table.insert(wins, win)
end
end
return wins
end
---@param role "left"|"right"
---@return integer
local function vertical_diff_win(role)
local wins = diff_wins()
table.sort(wins, function(a, b)
return vim.api.nvim_win_get_position(a)[2]
< vim.api.nvim_win_get_position(b)[2]
end)
local win = role == "left" and wins[1] or wins[#wins]
if not win then
error("diff window not found")
end
return win
end
---@param dir string
local function cleanup_dir_buffers(dir)
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(buf)
if name:find(dir, 1, true) then
pcall(vim.api.nvim_buf_delete, buf, { force = true })
end
end
end
t.test("open from worktree focuses the index pane", function()
local dir = h.make_repo({ ["a.txt"] = "old\n" })
t.write(dir, "a.txt", "new\n")
vim.cmd.edit(vim.fs.joinpath(dir, "a.txt"))
local current_buf = vim.api.nvim_get_current_buf()
require("git.diffsplit").open()
t.wait_for(function()
return #diff_wins() == 2
end, "diff windows to open")
t.eq(vim.api.nvim_get_current_win(), vertical_diff_win("left"))
t.truthy(
vim.api.nvim_get_current_buf() ~= current_buf,
"worktree buffer should lose focus"
)
end)
t.test("open from index focuses the worktree pane", function()
local dir = h.make_repo({ ["a.txt"] = "old\n" })
t.write(dir, "a.txt", "new\n")
local r = assert(require("git.core.repo").resolve(dir))
local index_buf = require("git.object").buf_for(
r,
Revision.new({ stage = 0, path = "a.txt" })
)
vim.api.nvim_set_current_buf(index_buf)
require("git.diffsplit").open({ layout = "vertical" })
t.wait_for(function()
return #diff_wins() == 2
end, "diff windows to open")
local right = vertical_diff_win("right")
t.eq(
vim.api.nvim_get_current_win(),
right,
"worktree buffer should be focused"
)
t.truthy(
vim.api.nvim_get_current_buf() ~= index_buf,
"index buffer should lose focus"
)
end)
t.test("open_pair keeps old on the left when splitright is set", function()
local dir = vim.fn.tempname()
vim.fn.mkdir(dir, "p")
t.defer(function()
cleanup_dir_buffers(dir)
vim.fn.delete(dir, "rf")
end)
local old = vim.fs.joinpath(dir, "old.txt")
local new = vim.fs.joinpath(dir, "new.txt")
vim.fn.writefile({ "old" }, old)
vim.fn.writefile({ "new" }, new)
local prev_splitright = vim.o.splitright
vim.o.splitright = true
t.defer(function()
vim.o.splitright = prev_splitright
end)
local old_buf = vim.fn.bufadd(old)
local new_buf = vim.fn.bufadd(new)
vim.fn.bufload(old_buf)
vim.fn.bufload(new_buf)
require("git.diffsplit").open_pair(old_buf, new_buf, {
layout = "vertical",
})
t.wait_for(function()
return #diff_wins() == 2
end, "diff windows to open")
local left = vertical_diff_win("left")
local right = vertical_diff_win("right")
t.eq(vim.api.nvim_win_get_buf(left), old_buf, "old pane should be left")
t.eq(vim.api.nvim_win_get_buf(right), new_buf, "new pane should be right")
t.eq(
vim.api.nvim_get_current_win(),
right,
"new pane should be focused by default"
)
end)
+1 -1
View File
@@ -341,7 +341,7 @@ t.test(
error("a non-sidebar window should remain after close")
end
vim.api.nvim_set_current_win(remaining)
require("git.diffsplit").open({ mods = { vertical = true } })
require("git.diffsplit").open({ layout = "vertical" })
t.wait_for(function()
local count = 0
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do