818 lines
22 KiB
Lua
818 lines
22 KiB
Lua
local Revision = require("git.revision")
|
|
local diff = require("git.diff")
|
|
local object = require("git.object")
|
|
local repo = require("git.repo")
|
|
local status = require("git.status")
|
|
local util = require("git.util")
|
|
|
|
local M = {}
|
|
|
|
M.URI_PREFIX = "gitstatus://"
|
|
|
|
---@type ow.Git.StatusView.Placement[]
|
|
M.PLACEMENTS = { "sidebar", "split", "current" }
|
|
|
|
---@type ow.Git.Status.Section[]
|
|
local SECTIONS = { "untracked", "unstaged", "staged", "unmerged" }
|
|
local WINDOW_WIDTH = 50
|
|
|
|
---@param name string
|
|
---@return integer? bufnr
|
|
local function find_buf(name)
|
|
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
|
if vim.api.nvim_buf_get_name(b) == name then
|
|
return b
|
|
end
|
|
end
|
|
end
|
|
|
|
---@alias ow.Git.StatusView.Placement "sidebar"|"split"|"current"
|
|
|
|
---@class ow.Git.StatusView.Header
|
|
---@field is_header true
|
|
---@field section ow.Git.Status.Section
|
|
|
|
---@alias ow.Git.StatusView.Item ow.Git.Status.Row | ow.Git.StatusView.Header
|
|
|
|
---@class ow.Git.StatusView.State
|
|
---@field repo ow.Git.Repo
|
|
---@field placement ow.Git.StatusView.Placement
|
|
---@field lines table<integer, ow.Git.StatusView.Item>
|
|
---@field win integer?
|
|
---@field invocation_win integer?
|
|
---@field diff_left_win integer?
|
|
---@field diff_right_win integer?
|
|
---@field unsubscribe fun()?
|
|
---@field last_shown_key string?
|
|
|
|
---@type table<integer, ow.Git.StatusView.State>
|
|
local state = {}
|
|
|
|
local group =
|
|
vim.api.nvim_create_augroup("ow.git.status_win", { clear = false })
|
|
local ns = vim.api.nvim_create_namespace("ow.git.status_win")
|
|
|
|
---@return integer? win
|
|
---@return integer? bufnr
|
|
local function find_view()
|
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
|
local buf = vim.api.nvim_win_get_buf(win)
|
|
if vim.bo[buf].filetype == "gitstatus" then
|
|
return win, buf
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param win integer?
|
|
---@return boolean
|
|
local function valid_in_current_tab(win)
|
|
if not win or not vim.api.nvim_win_is_valid(win) then
|
|
return false
|
|
end
|
|
return vim.api.nvim_win_get_tabpage(win)
|
|
== vim.api.nvim_get_current_tabpage()
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@return integer?
|
|
local function win_for(s)
|
|
if valid_in_current_tab(s.win) then
|
|
return s.win
|
|
end
|
|
local win = find_view()
|
|
s.win = win
|
|
return win
|
|
end
|
|
|
|
---@param row ow.Git.Status.Row
|
|
---@return string line
|
|
---@return string hl_group
|
|
---@return integer hl_len
|
|
local function format_row(row)
|
|
local entry = row.entry
|
|
local orig
|
|
if entry.kind == "changed" then
|
|
---@cast entry ow.Git.Status.ChangedEntry
|
|
orig = entry.orig
|
|
end
|
|
local label = orig and (orig .. " -> " .. entry.path) or entry.path
|
|
local mark = status.mark_for(entry, row.side)
|
|
return string.format(" %s %s", mark.char, label), mark.hl, #mark.char
|
|
end
|
|
|
|
---@param section ow.Git.Status.Section
|
|
---@return string
|
|
local function display_name(section)
|
|
return (section:gsub("^%l", string.upper))
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param status ow.Git.Status
|
|
local function render(bufnr, status)
|
|
local branch = status.branch
|
|
local lines = { "Head: " .. (branch.head or "?") }
|
|
if branch.upstream then
|
|
local push = "Push: " .. branch.upstream
|
|
if branch.ahead > 0 then
|
|
push = push .. " +" .. branch.ahead
|
|
end
|
|
if branch.behind > 0 then
|
|
push = push .. " -" .. branch.behind
|
|
end
|
|
table.insert(lines, push)
|
|
end
|
|
table.insert(lines, "")
|
|
|
|
local meta = {}
|
|
local marks = {}
|
|
for _, section in ipairs(SECTIONS) do
|
|
local rows = status:rows(section)
|
|
if #rows > 0 then
|
|
table.insert(
|
|
lines,
|
|
string.format("%s (%d)", display_name(section), #rows)
|
|
)
|
|
meta[#lines] = { is_header = true, section = section }
|
|
for _, row in ipairs(rows) do
|
|
local line, hl, hl_len = format_row(row)
|
|
table.insert(lines, line)
|
|
meta[#lines] = row
|
|
table.insert(marks, {
|
|
row = #lines - 1,
|
|
col = 2,
|
|
end_col = 2 + hl_len,
|
|
hl = hl,
|
|
})
|
|
end
|
|
table.insert(lines, "")
|
|
end
|
|
end
|
|
|
|
util.set_buf_lines(bufnr, 0, -1, lines)
|
|
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
|
for _, m in ipairs(marks) do
|
|
vim.api.nvim_buf_set_extmark(bufnr, ns, m.row, m.col, {
|
|
end_col = m.end_col,
|
|
hl_group = m.hl,
|
|
})
|
|
end
|
|
state[bufnr].lines = meta
|
|
end
|
|
|
|
---@param bufnr integer
|
|
local function refresh(bufnr)
|
|
local s = state[bufnr]
|
|
if not s or not vim.api.nvim_buf_is_valid(bufnr) then
|
|
return
|
|
end
|
|
s.last_shown_key = nil
|
|
render(bufnr, s.repo.status)
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return ow.Git.StatusView.State?
|
|
---@return ow.Git.StatusView.Item?
|
|
local function current_entry(bufnr)
|
|
local s = state[bufnr]
|
|
if not s then
|
|
return nil, nil
|
|
end
|
|
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
|
return s, s.lines[lnum]
|
|
end
|
|
|
|
---@param r ow.Git.Repo
|
|
---@param path string
|
|
---@return ow.Git.Diff.Side
|
|
local function head_pane(r, path)
|
|
local rev = Revision.new({ base = "HEAD", path = path })
|
|
return {
|
|
buf = object.buf_for(r, rev),
|
|
name = object.format_uri(rev),
|
|
}
|
|
end
|
|
|
|
---@param r ow.Git.Repo
|
|
---@param path string
|
|
---@return ow.Git.Diff.Side
|
|
local function worktree_pane(r, path)
|
|
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
|
|
vim.fn.bufload(buf)
|
|
return { buf = buf, name = nil }
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@param path string
|
|
---@return ow.Git.Diff.Side
|
|
local function index_pane(s, path)
|
|
local rev = Revision.new({ stage = 0, path = path })
|
|
return {
|
|
buf = object.buf_for(s.repo, rev),
|
|
name = object.format_uri(rev),
|
|
}
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@param row ow.Git.Status.Row
|
|
---@return ow.Git.Diff.Side?
|
|
local function older_pane(s, row)
|
|
local entry = row.entry
|
|
if row.section == "staged" then
|
|
---@cast entry ow.Git.Status.ChangedEntry
|
|
if entry.staged == "added" then
|
|
return nil
|
|
end
|
|
return head_pane(s.repo, entry.orig or entry.path)
|
|
end
|
|
if row.section == "unstaged" then
|
|
return index_pane(s, entry.path)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@param row ow.Git.Status.Row
|
|
---@return ow.Git.Diff.Side?
|
|
local function newer_pane(s, row)
|
|
local entry = row.entry
|
|
if row.section == "staged" then
|
|
---@cast entry ow.Git.Status.ChangedEntry
|
|
if entry.staged == "deleted" then
|
|
return nil
|
|
end
|
|
return index_pane(s, entry.path)
|
|
end
|
|
if row.section == "unstaged" then
|
|
---@cast entry ow.Git.Status.ChangedEntry
|
|
if entry.unstaged == "deleted" then
|
|
return nil
|
|
end
|
|
return worktree_pane(s.repo, entry.path)
|
|
end
|
|
if row.section == "untracked" then
|
|
return worktree_pane(s.repo, entry.path)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---@param win integer
|
|
local function reset_diff_win(win)
|
|
vim.api.nvim_win_call(win, function()
|
|
vim.cmd(
|
|
"setlocal winfixwidth< number< relativenumber< signcolumn< wrap< cursorline<"
|
|
)
|
|
end)
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@return integer?
|
|
local function invocation_win_for(s)
|
|
local win = s.invocation_win
|
|
if not win or not vim.api.nvim_win_is_valid(win) then
|
|
return nil
|
|
end
|
|
if win == s.win then
|
|
return nil
|
|
end
|
|
if
|
|
vim.api.nvim_win_get_tabpage(win)
|
|
~= vim.api.nvim_get_current_tabpage()
|
|
then
|
|
return nil
|
|
end
|
|
return win
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@param status_win integer
|
|
---@return integer? left
|
|
---@return integer? right
|
|
local function adopt_diff_wins(s, status_win)
|
|
local left = valid_in_current_tab(s.diff_left_win) and s.diff_left_win
|
|
or nil
|
|
local right = valid_in_current_tab(s.diff_right_win) and s.diff_right_win
|
|
or nil
|
|
if left and right then
|
|
return left, right
|
|
end
|
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
|
if win ~= status_win then
|
|
local role = vim.w[win].git_diff_role
|
|
if role == "left" and not left then
|
|
left = win
|
|
elseif role == "right" and not right then
|
|
right = win
|
|
end
|
|
end
|
|
end
|
|
return left, right
|
|
end
|
|
|
|
---@param row ow.Git.Status.Row
|
|
---@return string
|
|
local function row_key(row)
|
|
local entry = row.entry
|
|
local orig
|
|
if entry.kind == "changed" then
|
|
---@cast entry ow.Git.Status.ChangedEntry
|
|
orig = entry.orig
|
|
end
|
|
return row.section .. "|" .. entry.path .. "|" .. (orig or "")
|
|
end
|
|
|
|
---@param target_win integer
|
|
---@param dir "left"|"right"
|
|
---@return integer
|
|
local function vsplit_at(target_win, dir)
|
|
local win = vim.api.nvim_open_win(
|
|
vim.api.nvim_win_get_buf(target_win),
|
|
true,
|
|
{ split = dir, win = target_win }
|
|
)
|
|
vim.cmd.clearjumps()
|
|
return win
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@param status_win integer
|
|
---@param right_win integer?
|
|
---@return integer
|
|
local function ensure_right_win(s, status_win, right_win)
|
|
if right_win then
|
|
return right_win
|
|
end
|
|
local target = invocation_win_for(s)
|
|
if target then
|
|
right_win = target
|
|
else
|
|
right_win = vsplit_at(status_win, "right")
|
|
vim.api.nvim_win_set_width(status_win, WINDOW_WIDTH)
|
|
end
|
|
reset_diff_win(right_win)
|
|
return right_win
|
|
end
|
|
|
|
---@param s ow.Git.StatusView.State
|
|
---@param row ow.Git.Status.Row
|
|
---@param focus_left boolean
|
|
local function view_row(s, row, focus_left)
|
|
local status_win = win_for(s)
|
|
if not status_win then
|
|
return
|
|
end
|
|
|
|
local left = older_pane(s, row)
|
|
local right = newer_pane(s, row)
|
|
if not left and not right then
|
|
util.warning(
|
|
"no content for %s row: %s",
|
|
row.section,
|
|
row.entry.path
|
|
)
|
|
return
|
|
end
|
|
|
|
if s.placement ~= "sidebar" then
|
|
local pane = right or left
|
|
---@cast pane ow.Git.Diff.Side
|
|
diff.set_diff(status_win, false)
|
|
vim.cmd.normal({ "m'", bang = true })
|
|
vim.api.nvim_win_set_buf(status_win, pane.buf)
|
|
if pane.name then
|
|
util.set_buf_name(pane.buf, pane.name)
|
|
end
|
|
return
|
|
end
|
|
|
|
local key = row_key(row)
|
|
local left_win, right_win = adopt_diff_wins(s, status_win)
|
|
local want_pair = left and right
|
|
|
|
if s.last_shown_key == key then
|
|
local intact = (want_pair and left_win and right_win)
|
|
or (not want_pair and right_win and not left_win)
|
|
if intact then
|
|
local target = focus_left and (left_win or right_win) or status_win
|
|
vim.api.nvim_set_current_win(target)
|
|
return
|
|
end
|
|
end
|
|
|
|
if not want_pair then
|
|
if left_win and vim.api.nvim_win_is_valid(left_win) then
|
|
pcall(vim.api.nvim_win_close, left_win, false)
|
|
left_win = nil
|
|
s.diff_left_win = nil
|
|
end
|
|
right_win = ensure_right_win(s, status_win, right_win)
|
|
s.diff_right_win = right_win
|
|
vim.w[right_win].git_diff_role = "right"
|
|
local side = left or right
|
|
---@cast side ow.Git.Diff.Side
|
|
diff.set_diff(right_win, false)
|
|
vim.api.nvim_win_set_buf(right_win, side.buf)
|
|
if side.name then
|
|
util.set_buf_name(side.buf, side.name)
|
|
end
|
|
s.last_shown_key = key
|
|
vim.api.nvim_set_current_win(focus_left and right_win or status_win)
|
|
return
|
|
end
|
|
---@cast left ow.Git.Diff.Side
|
|
---@cast right ow.Git.Diff.Side
|
|
|
|
if left_win and not right_win then
|
|
right_win = vsplit_at(left_win, "right")
|
|
reset_diff_win(right_win)
|
|
elseif right_win and not left_win then
|
|
left_win = vsplit_at(right_win, "left")
|
|
reset_diff_win(left_win)
|
|
elseif not (left_win or right_win) then
|
|
right_win = ensure_right_win(s, status_win, nil)
|
|
left_win = vsplit_at(right_win, "left")
|
|
reset_diff_win(left_win)
|
|
local combined = vim.api.nvim_win_get_width(left_win)
|
|
+ vim.api.nvim_win_get_width(right_win)
|
|
vim.api.nvim_win_set_width(left_win, math.floor(combined / 2))
|
|
end
|
|
---@cast left_win -nil
|
|
---@cast right_win -nil
|
|
|
|
vim.w[left_win].git_diff_role = "left"
|
|
vim.w[right_win].git_diff_role = "right"
|
|
s.diff_left_win = left_win
|
|
s.diff_right_win = right_win
|
|
|
|
diff.update_pair(left_win, right_win, { left = left, right = right })
|
|
s.last_shown_key = key
|
|
|
|
vim.api.nvim_set_current_win(focus_left and left_win or status_win)
|
|
end
|
|
|
|
---@param focus_left boolean
|
|
local function preview_or_open(focus_left)
|
|
local s, item = current_entry(vim.api.nvim_get_current_buf())
|
|
if not s or not item or item.is_header then
|
|
return
|
|
end
|
|
---@cast item ow.Git.Status.Row
|
|
view_row(s, item, focus_left)
|
|
end
|
|
|
|
local function action_stage()
|
|
local s, item = current_entry(vim.api.nvim_get_current_buf())
|
|
if not s or not item then
|
|
return
|
|
end
|
|
local paths = {}
|
|
if item.is_header then
|
|
if item.section == "staged" or item.section == "ignored" then
|
|
return
|
|
end
|
|
for _, row in ipairs(s.repo.status:rows(item.section)) do
|
|
table.insert(paths, row.entry.path)
|
|
end
|
|
else
|
|
---@cast item ow.Git.Status.Row
|
|
if item.section == "staged" then
|
|
return
|
|
end
|
|
table.insert(paths, item.entry.path)
|
|
end
|
|
if #paths == 0 then
|
|
return
|
|
end
|
|
local args = { "add", "--" }
|
|
vim.list_extend(args, paths)
|
|
util.git(args, {
|
|
cwd = s.repo.worktree,
|
|
on_exit = function(result)
|
|
if result.code ~= 0 then
|
|
util.error("git add failed: %s", vim.trim(result.stderr or ""))
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function action_unstage()
|
|
local s, item = current_entry(vim.api.nvim_get_current_buf())
|
|
if not s or not item then
|
|
return
|
|
end
|
|
local rows
|
|
if item.is_header then
|
|
if item.section ~= "staged" then
|
|
return
|
|
end
|
|
rows = s.repo.status:rows("staged")
|
|
else
|
|
---@cast item ow.Git.Status.Row
|
|
if item.section ~= "staged" then
|
|
return
|
|
end
|
|
rows = { item }
|
|
end
|
|
---@cast rows ow.Git.Status.Row[]
|
|
if #rows == 0 then
|
|
return
|
|
end
|
|
local args = { "restore", "--staged", "--" }
|
|
for _, row in ipairs(rows) do
|
|
local entry = row.entry
|
|
if entry.kind == "changed" then
|
|
---@cast entry ow.Git.Status.ChangedEntry
|
|
if entry.orig then
|
|
table.insert(args, entry.orig)
|
|
end
|
|
end
|
|
table.insert(args, entry.path)
|
|
end
|
|
util.git(args, {
|
|
cwd = s.repo.worktree,
|
|
on_exit = function(result)
|
|
if result.code ~= 0 then
|
|
util.error(
|
|
"git restore --staged failed: %s",
|
|
vim.trim(result.stderr or "")
|
|
)
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function action_discard()
|
|
local s, item = current_entry(vim.api.nvim_get_current_buf())
|
|
if not s or not item or item.is_header then
|
|
return
|
|
end
|
|
---@cast item ow.Git.Status.Row
|
|
if item.section == "staged" then
|
|
util.warning("file has staged changes, unstage first with 'u'")
|
|
return
|
|
end
|
|
local entry = item.entry
|
|
local path = entry.path
|
|
|
|
local prompt, action
|
|
if item.section == "untracked" then
|
|
local is_dir = path:sub(-1) == "/"
|
|
prompt = string.format(
|
|
"Delete untracked %s %s?",
|
|
is_dir and "directory" or "file",
|
|
path
|
|
)
|
|
action = function()
|
|
local target = vim.fs.joinpath(s.repo.worktree, path)
|
|
local rc = vim.fn.delete(target, is_dir and "rf" or "")
|
|
if rc ~= 0 then
|
|
util.error("failed to delete %s", path)
|
|
end
|
|
refresh(vim.api.nvim_get_current_buf())
|
|
end
|
|
elseif item.section == "unstaged" then
|
|
prompt = string.format("Discard changes to %s?", path)
|
|
action = function()
|
|
util.git({ "checkout", "--", path }, {
|
|
cwd = s.repo.worktree,
|
|
on_exit = function(result)
|
|
if result.code ~= 0 then
|
|
util.error(
|
|
"git checkout failed: %s",
|
|
vim.trim(result.stderr or "")
|
|
)
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
else
|
|
return
|
|
end
|
|
|
|
if vim.fn.confirm(prompt, "&Yes\n&No", 2) == 1 then
|
|
action()
|
|
end
|
|
end
|
|
|
|
---@param placement ow.Git.StatusView.Placement
|
|
local function action_help(placement)
|
|
local lines = { "git status view" }
|
|
if placement == "sidebar" then
|
|
table.insert(lines, " <Tab> preview diff (keep focus)")
|
|
table.insert(lines, " <CR> open diff (focus left pane)")
|
|
else
|
|
table.insert(lines, " <CR> open file")
|
|
end
|
|
table.insert(lines, " s stage file")
|
|
table.insert(lines, " u unstage file")
|
|
table.insert(
|
|
lines,
|
|
" X discard worktree changes (untracked: delete file)"
|
|
)
|
|
table.insert(lines, " R refresh")
|
|
table.insert(lines, " g? show this help")
|
|
print(table.concat(lines, "\n"))
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param placement ow.Git.StatusView.Placement
|
|
---@return integer win
|
|
local function place(bufnr, placement)
|
|
local split
|
|
if placement == "sidebar" then
|
|
split = "left"
|
|
elseif placement == "current" then
|
|
split = false
|
|
end
|
|
local win = util.place_buf(bufnr, split)
|
|
vim.wo[win].number = false
|
|
vim.wo[win].relativenumber = false
|
|
vim.wo[win].wrap = false
|
|
vim.wo[win].signcolumn = "no"
|
|
vim.wo[win].cursorline = true
|
|
if placement == "sidebar" then
|
|
vim.wo[win].winfixwidth = true
|
|
vim.api.nvim_win_set_width(win, WINDOW_WIDTH)
|
|
end
|
|
return win
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param r ow.Git.Repo
|
|
---@param placement ow.Git.StatusView.Placement
|
|
---@param win integer?
|
|
---@param invocation_win integer?
|
|
local function setup_buffer(bufnr, r, placement, win, invocation_win)
|
|
state[bufnr] = {
|
|
repo = r,
|
|
placement = placement,
|
|
lines = {},
|
|
win = win,
|
|
invocation_win = invocation_win,
|
|
}
|
|
|
|
local function k(lhs, rhs, desc)
|
|
vim.keymap.set(
|
|
"n",
|
|
lhs,
|
|
rhs,
|
|
{ buffer = bufnr, silent = true, desc = desc }
|
|
)
|
|
end
|
|
k("<CR>", function()
|
|
preview_or_open(true)
|
|
end, "Open")
|
|
k("s", action_stage, "Stage file")
|
|
k("u", action_unstage, "Unstage file")
|
|
k("X", action_discard, "Discard worktree changes")
|
|
k("R", function()
|
|
r:refresh()
|
|
end, "Refresh")
|
|
k("g?", function()
|
|
action_help(state[bufnr].placement)
|
|
end, "Help")
|
|
|
|
state[bufnr].unsubscribe = r:on("refresh", function()
|
|
refresh(bufnr)
|
|
end)
|
|
vim.api.nvim_create_autocmd("BufEnter", {
|
|
buffer = bufnr,
|
|
group = group,
|
|
callback = function()
|
|
r:refresh()
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
|
buffer = bufnr,
|
|
group = group,
|
|
callback = function()
|
|
local s = state[bufnr]
|
|
if not s then
|
|
return
|
|
end
|
|
if s.unsubscribe then
|
|
s.unsubscribe()
|
|
end
|
|
state[bufnr] = nil
|
|
end,
|
|
})
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param placement ow.Git.StatusView.Placement
|
|
local function set_keymaps(bufnr, placement)
|
|
if placement == "sidebar" then
|
|
vim.keymap.set("n", "<Tab>", function()
|
|
preview_or_open(false)
|
|
end, { buffer = bufnr, silent = true, desc = "Preview diff" })
|
|
else
|
|
pcall(vim.keymap.del, "n", "<Tab>", { buffer = bufnr })
|
|
end
|
|
end
|
|
|
|
---@param opts? { placement: ow.Git.StatusView.Placement? }
|
|
function M.open(opts)
|
|
opts = opts or {}
|
|
local placement = opts.placement or "sidebar"
|
|
local r = repo.resolve()
|
|
if not r then
|
|
util.error("not in a git repository")
|
|
return
|
|
end
|
|
local previous_win = vim.api.nvim_get_current_win()
|
|
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
|
|
|
|
local visible = vim.fn.bufwinid(buf)
|
|
if visible ~= -1 then
|
|
vim.api.nvim_set_current_win(visible)
|
|
r:refresh()
|
|
return
|
|
end
|
|
|
|
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
|
local win = place(buf, placement)
|
|
|
|
vim.bo[buf].bufhidden = placement == "sidebar" and "wipe" or "hide"
|
|
local s = state[buf]
|
|
if s then
|
|
s.win = win
|
|
s.invocation_win = previous_win
|
|
s.placement = placement
|
|
end
|
|
set_keymaps(buf, placement)
|
|
|
|
if placement == "sidebar" then
|
|
vim.api.nvim_set_current_win(previous_win)
|
|
end
|
|
|
|
if was_loaded then
|
|
refresh(buf)
|
|
end
|
|
r:refresh()
|
|
end
|
|
|
|
---@param buf integer
|
|
function M.read_uri(buf)
|
|
local name = vim.api.nvim_buf_get_name(buf)
|
|
local raw = name:sub(#M.URI_PREFIX + 1)
|
|
if raw == "" then
|
|
return
|
|
end
|
|
local worktree = vim.fs.abspath(raw)
|
|
local r = repo.resolve(worktree)
|
|
if not r then
|
|
util.error("not a git worktree: %s", raw)
|
|
return
|
|
end
|
|
if r.worktree ~= worktree then
|
|
util.warning("%s is not a worktree root, using %s", raw, r.worktree)
|
|
end
|
|
local canonical = M.URI_PREFIX .. r.worktree
|
|
if name ~= canonical then
|
|
local existing = find_buf(canonical)
|
|
if existing and existing ~= buf then
|
|
local win = vim.api.nvim_get_current_win()
|
|
if vim.api.nvim_win_get_buf(win) == buf then
|
|
vim.api.nvim_win_set_buf(win, existing)
|
|
end
|
|
vim.api.nvim_buf_delete(buf, { force = true })
|
|
local s = state[existing]
|
|
if s then
|
|
s.win = win
|
|
s.placement = "current"
|
|
end
|
|
refresh(existing)
|
|
r:refresh()
|
|
return
|
|
end
|
|
pcall(vim.api.nvim_buf_set_name, buf, canonical)
|
|
end
|
|
repo.bind(buf, r)
|
|
|
|
util.setup_scratch(buf, { bufhidden = "hide" })
|
|
vim.bo[buf].filetype = "gitstatus"
|
|
|
|
---@type integer?
|
|
local win = vim.fn.bufwinid(buf)
|
|
if win == -1 then
|
|
win = nil
|
|
end
|
|
if not state[buf] then
|
|
setup_buffer(buf, r, "current", win, nil)
|
|
else
|
|
state[buf].win = win
|
|
end
|
|
refresh(buf)
|
|
r:refresh()
|
|
end
|
|
|
|
function M.toggle()
|
|
local existing = find_view()
|
|
if existing then
|
|
vim.api.nvim_win_close(existing, false)
|
|
return
|
|
end
|
|
M.open({ placement = "sidebar" })
|
|
end
|
|
|
|
return M
|