refactor(git): rewrite diff module around :diffsplit
This commit is contained in:
+111
-152
@@ -1,175 +1,134 @@
|
||||
local Revision = require("git.core.revision")
|
||||
local object = require("git.object")
|
||||
local repo = require("git.core.repo")
|
||||
local util = require("git.core.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.Diff.Side
|
||||
---@field buf integer
|
||||
---@field name string?
|
||||
|
||||
---@class ow.Git.Diff.Pair
|
||||
---@field left ow.Git.Diff.Side
|
||||
---@field right ow.Git.Diff.Side
|
||||
|
||||
---@param win integer
|
||||
---@param enabled boolean
|
||||
function M.set_diff(win, enabled)
|
||||
vim.api.nvim_win_call(win, function()
|
||||
vim.cmd(enabled and "diffthis" or "diffoff")
|
||||
end)
|
||||
end
|
||||
|
||||
---@param left integer
|
||||
---@param right integer
|
||||
---@param vertical boolean
|
||||
function M.open(left, right, vertical)
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(right)
|
||||
vim.cmd.diffthis()
|
||||
vim.api.nvim_open_win(left, true, {
|
||||
split = vertical and "left" or "above",
|
||||
})
|
||||
vim.cmd.diffthis()
|
||||
end
|
||||
|
||||
---@param left_win integer
|
||||
---@param right_win integer
|
||||
---@param pair ow.Git.Diff.Pair
|
||||
function M.update_pair(left_win, right_win, pair)
|
||||
M.set_diff(left_win, false)
|
||||
M.set_diff(right_win, false)
|
||||
vim.api.nvim_win_set_buf(left_win, pair.left.buf)
|
||||
vim.api.nvim_win_set_buf(right_win, pair.right.buf)
|
||||
for _, side in ipairs({ pair.left, pair.right }) do
|
||||
if side.name then
|
||||
util.set_buf_name(side.buf, side.name)
|
||||
end
|
||||
end
|
||||
M.set_diff(left_win, true)
|
||||
M.set_diff(right_win, true)
|
||||
vim.api.nvim_win_set_cursor(left_win, { 1, 0 })
|
||||
vim.api.nvim_win_set_cursor(right_win, { 1, 0 })
|
||||
vim.cmd.syncbind()
|
||||
end
|
||||
|
||||
---@param buf_a integer
|
||||
---@param buf_b integer
|
||||
---@param a_left boolean
|
||||
---@param vertical boolean
|
||||
local function place_pair(buf_a, buf_b, a_left, vertical)
|
||||
if a_left then
|
||||
M.open(buf_a, buf_b, vertical)
|
||||
else
|
||||
M.open(buf_b, buf_a, vertical)
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts ow.Git.Diff.SplitOpts
|
||||
---@param buf integer
|
||||
---@param rev ow.Git.Revision
|
||||
local function uri_split(opts, buf, rev)
|
||||
local r = repo.resolve(buf)
|
||||
if not r then
|
||||
util.error("git URI buffer has no worktree")
|
||||
return
|
||||
end
|
||||
if not rev.path then
|
||||
util.error("git URI has no path, cannot diff against worktree")
|
||||
return
|
||||
end
|
||||
local object = require("git.object")
|
||||
|
||||
if opts.rev and opts.rev:find(":", 1, true) then
|
||||
if not r:rev_parse(opts.rev, true) then
|
||||
util.error("invalid rev: %s", opts.rev)
|
||||
return
|
||||
end
|
||||
place_pair(
|
||||
buf,
|
||||
object.buf_for(r, Revision.parse(opts.rev)),
|
||||
false,
|
||||
opts.vertical
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if not opts.rev then
|
||||
local worktree_path = vim.fs.joinpath(r.worktree, rev.path)
|
||||
if not vim.uv.fs_stat(worktree_path) then
|
||||
util.error("worktree file does not exist: %s", rev.path)
|
||||
return
|
||||
end
|
||||
local worktree_buf = vim.fn.bufadd(worktree_path)
|
||||
vim.fn.bufload(worktree_buf)
|
||||
place_pair(buf, worktree_buf, true, opts.vertical)
|
||||
return
|
||||
end
|
||||
|
||||
if rev.stage == 1 then
|
||||
util.warning("gD on merge base is ambiguous, use :Gdiffsplit <rev>")
|
||||
return
|
||||
end
|
||||
|
||||
local mapping = {
|
||||
[2] = { Revision.new({ stage = 3, path = rev.path }), true },
|
||||
[3] = { Revision.new({ stage = 2, path = rev.path }), false },
|
||||
[0] = { Revision.new({ base = "HEAD", path = rev.path }), false },
|
||||
}
|
||||
local m = mapping[rev.stage]
|
||||
or { Revision.new({ stage = 0, path = rev.path }), true }
|
||||
local other_rev, left = m[1], m[2]
|
||||
if not r:rev_parse(other_rev:format(), true) then
|
||||
util.error("invalid rev: %s", other_rev:format())
|
||||
return
|
||||
end
|
||||
place_pair(buf, object.buf_for(r, other_rev), left, opts.vertical)
|
||||
end
|
||||
|
||||
---@class ow.Git.Diff.SplitOpts
|
||||
---@field rev string?
|
||||
---@field vertical boolean
|
||||
---@field target string?
|
||||
---@field mods vim.api.keyset.cmd.mods?
|
||||
|
||||
---@param opts ow.Git.Diff.SplitOpts
|
||||
function M.split(opts)
|
||||
local cur_buf = vim.api.nvim_get_current_buf()
|
||||
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
||||
|
||||
local cur_rev = require("git.object").parse_uri(cur_path)
|
||||
---@param cur_buf integer
|
||||
---@return string? target
|
||||
---@return string? err
|
||||
local function infer_target(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
|
||||
return uri_split(opts, cur_buf, cur_rev)
|
||||
local r = repo.resolve(cur_buf)
|
||||
if not r then
|
||||
return nil, "git URI buffer has no worktree"
|
||||
end
|
||||
if not cur_rev.path then
|
||||
return nil, "git URI has no path, cannot diff against worktree"
|
||||
end
|
||||
local worktree_path = vim.fs.joinpath(r.worktree, cur_rev.path)
|
||||
if not vim.uv.fs_stat(worktree_path) then
|
||||
return nil, "worktree file does not exist: " .. cur_rev.path
|
||||
end
|
||||
return worktree_path, nil
|
||||
end
|
||||
|
||||
if cur_path == "" then
|
||||
util.error("no file in current buffer")
|
||||
return
|
||||
if cur_name == "" then
|
||||
return nil, "no file in current buffer"
|
||||
end
|
||||
if vim.bo[cur_buf].buftype ~= "" then
|
||||
util.error("cannot diff this buffer (not a worktree file)")
|
||||
return
|
||||
return nil, "cannot diff this buffer (not a worktree file)"
|
||||
end
|
||||
cur_path = vim.fn.resolve(cur_path)
|
||||
local r = repo.resolve(cur_path)
|
||||
local resolved = vim.fn.resolve(cur_name)
|
||||
local r = repo.resolve(resolved)
|
||||
if not r then
|
||||
util.error("not in a git repository")
|
||||
return
|
||||
return nil, "not in a git repository"
|
||||
end
|
||||
local rel = vim.fs.relpath(r.worktree, cur_path)
|
||||
local rel = vim.fs.relpath(r.worktree, resolved)
|
||||
if not rel then
|
||||
return nil, "current buffer is outside the worktree"
|
||||
end
|
||||
return object.format_uri(Revision.new({ stage = 0, path = rel })), nil
|
||||
end
|
||||
|
||||
local rev
|
||||
if not opts.rev then
|
||||
rev = Revision.new({ stage = 0, path = rel })
|
||||
elseif opts.rev:find(":", 1, true) then
|
||||
rev = Revision.parse(opts.rev)
|
||||
else
|
||||
rev = Revision.new({ base = opts.rev, path = rel })
|
||||
---@param target 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
|
||||
end
|
||||
if not r:rev_parse(rev:format(), true) then
|
||||
util.error("invalid rev: %s", rev:format())
|
||||
if vim.fn.filereadable(target) == 1 then
|
||||
return target, nil
|
||||
end
|
||||
local cur_name = vim.api.nvim_buf_get_name(cur_buf)
|
||||
local cur_rev = object.parse_uri(cur_name)
|
||||
local r, rel
|
||||
if cur_rev and cur_rev.path then
|
||||
r = repo.resolve(cur_buf)
|
||||
rel = cur_rev.path
|
||||
elseif cur_name ~= "" then
|
||||
local resolved = vim.fn.resolve(cur_name)
|
||||
r = repo.resolve(resolved)
|
||||
if r then
|
||||
rel = vim.fs.relpath(r.worktree, resolved)
|
||||
end
|
||||
end
|
||||
if not r then
|
||||
return nil, "not in a git repository"
|
||||
end
|
||||
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
|
||||
end
|
||||
return object.format_uri(Revision.new({ base = target, path = rel })), nil
|
||||
end
|
||||
|
||||
---@param cur_buf integer
|
||||
---@param target string
|
||||
---@return 'aboveleft'|'belowright'|nil
|
||||
local function default_split(cur_buf, target)
|
||||
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
|
||||
return "aboveleft"
|
||||
end
|
||||
if cur_rev and not target_rev then
|
||||
return "belowright"
|
||||
end
|
||||
if cur_rev and target_rev then
|
||||
if cur_rev.stage == 0 and target_rev.base then
|
||||
return "aboveleft"
|
||||
end
|
||||
if cur_rev.base and target_rev.stage == 0 then
|
||||
return "belowright"
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param opts? ow.Git.Diff.SplitOpts
|
||||
function M.split(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)
|
||||
else
|
||||
target, err = infer_target(cur_buf)
|
||||
end
|
||||
if not target then
|
||||
util.error("%s", err or "no diff target")
|
||||
return
|
||||
end
|
||||
local buf = require("git.object").buf_for(r, rev)
|
||||
place_pair(buf, cur_buf, true, opts.vertical)
|
||||
local mods = opts.mods
|
||||
if not mods or mods.split == nil then
|
||||
local placement = default_split(cur_buf, target)
|
||||
if placement then
|
||||
mods = vim.tbl_extend("force", mods or {}, { split = placement })
|
||||
end
|
||||
end
|
||||
vim.cmd.diffsplit({ args = { target }, mods = mods })
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user