Files
git.nvim/lua/git/diffsplit.lua
T

221 lines
6.4 KiB
Lua

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.Diffsplit.OpenOpts
---@field other string?
---@field layout ("vertical"|"horizontal")?
---@field mods vim.api.keyset.cmd.mods?
---@field focus ("current"|"other")?
---@param cur_buf integer
---@return string? other
---@return string? err
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
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_name == "" then
return nil, "no file in current buffer"
end
if vim.bo[cur_buf].buftype ~= "" then
return nil, "cannot diff this buffer (not a worktree file)"
end
local resolved = vim.fn.resolve(cur_name)
local r = repo.resolve(resolved)
if not r then
return nil, "not in a git repository"
end
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
---@param other string
---@param cur_buf integer
---@return string? resolved
---@return string? err
local function resolve_other(other, cur_buf)
if vim.startswith(other, object.URI_PREFIX) then
return other, nil
end
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)
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(other, true) then
return nil, "invalid rev: " .. other
end
return object.format_uri(Revision.new({ base = other, path = rel })), nil
end
---@param cur_buf integer
---@param other string
---@return 'aboveleft'|'belowright'|nil
local function default_split(cur_buf, other)
local cur_rev = object.parse_uri(vim.api.nvim_buf_get_name(cur_buf))
local other_rev = object.parse_uri(other)
if not cur_rev and other_rev then
return "aboveleft"
end
if cur_rev and not other_rev then
return "belowright"
end
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 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 other, err
if opts.other then
other, err = resolve_other(opts.other, cur_buf)
else
other, err = infer_other(cur_buf)
end
if not other then
util.warning("%s", err or "no diff side")
return
end
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
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.warning("%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.warning("%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