feat(git): add status view as window with URI buffers
This commit is contained in:
@@ -82,6 +82,13 @@ function M.init()
|
|||||||
require("git.log_view").read_uri(args.buf)
|
require("git.log_view").read_uri(args.buf)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||||
|
pattern = "gitstatus://*",
|
||||||
|
group = group,
|
||||||
|
callback = function(args)
|
||||||
|
require("git.status_view").read_uri(args.buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
vim.api.nvim_create_user_command("Glog", function(opts)
|
vim.api.nvim_create_user_command("Glog", function(opts)
|
||||||
require("git.log_view").run_glog(opts)
|
require("git.log_view").run_glog(opts)
|
||||||
end, {
|
end, {
|
||||||
@@ -141,6 +148,18 @@ function M.init()
|
|||||||
vim.api.nvim_create_user_command("Grefresh", function()
|
vim.api.nvim_create_user_command("Grefresh", function()
|
||||||
require("git.repo").refresh_all()
|
require("git.repo").refresh_all()
|
||||||
end, { desc = "Refresh git status for all repos" })
|
end, { desc = "Refresh git status for all repos" })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("Gstatus", function(opts)
|
||||||
|
require("git.status_view").open({
|
||||||
|
placement = opts.fargs[1] --[[@as ow.Git.StatusView.Placement]] or "split",
|
||||||
|
})
|
||||||
|
end, {
|
||||||
|
nargs = "?",
|
||||||
|
complete = function()
|
||||||
|
return require("git.status_view").PLACEMENTS
|
||||||
|
end,
|
||||||
|
desc = "Open git status view",
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+1
-4
@@ -56,10 +56,7 @@ function Repo:_fetch_status()
|
|||||||
{ cwd = self.worktree, text = true },
|
{ cwd = self.worktree, text = true },
|
||||||
vim.schedule_wrap(function(obj)
|
vim.schedule_wrap(function(obj)
|
||||||
if obj.code ~= 0 then
|
if obj.code ~= 0 then
|
||||||
util.error(
|
util.error("git status failed: %s", vim.trim(obj.stderr or ""))
|
||||||
"git status failed: %s",
|
|
||||||
vim.trim(obj.stderr or "")
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.status = status.parse(obj.stdout or "")
|
self.status = status.parse(obj.stdout or "")
|
||||||
|
|||||||
+198
-33
@@ -6,12 +6,30 @@ local util = require("git.util")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
M.URI_PREFIX = "gitstatus://"
|
||||||
|
|
||||||
|
---@type ow.Git.StatusView.Placement[]
|
||||||
|
M.PLACEMENTS = { "sidebar", "split", "current" }
|
||||||
|
|
||||||
---@type ow.Git.Status.EntryKind[]
|
---@type ow.Git.Status.EntryKind[]
|
||||||
local KINDS = { "untracked", "unstaged", "staged", "unmerged" }
|
local KINDS = { "untracked", "unstaged", "staged", "unmerged" }
|
||||||
local WINDOW_WIDTH = 50
|
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.State
|
---@class ow.Git.StatusView.State
|
||||||
---@field repo ow.Git.Repo
|
---@field repo ow.Git.Repo
|
||||||
|
---@field placement ow.Git.StatusView.Placement
|
||||||
---@field lines table<integer, ow.Git.Status.Entry>
|
---@field lines table<integer, ow.Git.Status.Entry>
|
||||||
---@field win integer?
|
---@field win integer?
|
||||||
---@field invocation_win integer?
|
---@field invocation_win integer?
|
||||||
@@ -169,12 +187,12 @@ local function refresh(bufnr)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local status = s.repo.status
|
local status = s.repo.status
|
||||||
s.last_shown_key = nil
|
|
||||||
local fp = fingerprint(status)
|
local fp = fingerprint(status)
|
||||||
if fp == s.last_render_key then
|
if fp == s.last_render_key then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
s.last_render_key = fp
|
s.last_render_key = fp
|
||||||
|
s.last_shown_key = nil
|
||||||
render(bufnr, status)
|
render(bufnr, status)
|
||||||
if not saved_path then
|
if not saved_path then
|
||||||
return
|
return
|
||||||
@@ -382,6 +400,18 @@ local function view_entry(s, entry, focus_left)
|
|||||||
return
|
return
|
||||||
end
|
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 = entry_key(entry)
|
local key = entry_key(entry)
|
||||||
local left_win, right_win = adopt_diff_wins(s, status_win)
|
local left_win, right_win = adopt_diff_wins(s, status_win)
|
||||||
local want_pair = left and right
|
local want_pair = left and right
|
||||||
@@ -553,43 +583,64 @@ local function action_discard()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function action_help()
|
---@param placement ow.Git.StatusView.Placement
|
||||||
print(table.concat({
|
local function action_help(placement)
|
||||||
"git status window",
|
local lines = { "git status view" }
|
||||||
" <Tab> preview diff (keep focus)",
|
if placement == "sidebar" then
|
||||||
" <CR> open diff (focus left pane)",
|
table.insert(lines, " <Tab> preview diff (keep focus)")
|
||||||
" s stage file",
|
table.insert(lines, " <CR> open diff (focus left pane)")
|
||||||
" u unstage file",
|
else
|
||||||
" X discard worktree changes (untracked: delete file)",
|
table.insert(lines, " <CR> open file")
|
||||||
" g? show this help",
|
end
|
||||||
}, "\n"))
|
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, " g? show this help")
|
||||||
|
print(table.concat(lines, "\n"))
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param r ow.Git.Repo
|
---Place an existing or new gitstatus buffer in a window per `placement`.
|
||||||
local function open(r)
|
---Sidebar uses a left split with fixed width and winfixwidth; split uses
|
||||||
local existing = find_view()
|
---the default split direction (above/below per `splitbelow`); current
|
||||||
if existing then
|
---swaps into the current window.
|
||||||
vim.api.nvim_set_current_win(existing)
|
---@param bufnr integer
|
||||||
return
|
---@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
|
end
|
||||||
|
local win = util.place_buf(bufnr, split)
|
||||||
local previous_win = vim.api.nvim_get_current_win()
|
|
||||||
local bufnr, win = util.new_scratch({ split = "left" })
|
|
||||||
vim.bo[bufnr].filetype = "gitstatus"
|
|
||||||
|
|
||||||
vim.wo[win].number = false
|
vim.wo[win].number = false
|
||||||
vim.wo[win].relativenumber = false
|
vim.wo[win].relativenumber = false
|
||||||
vim.wo[win].wrap = false
|
vim.wo[win].wrap = false
|
||||||
vim.wo[win].signcolumn = "no"
|
vim.wo[win].signcolumn = "no"
|
||||||
vim.wo[win].cursorline = true
|
vim.wo[win].cursorline = true
|
||||||
|
if placement == "sidebar" then
|
||||||
vim.wo[win].winfixwidth = true
|
vim.wo[win].winfixwidth = true
|
||||||
vim.api.nvim_win_set_width(win, WINDOW_WIDTH)
|
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] = {
|
state[bufnr] = {
|
||||||
repo = r,
|
repo = r,
|
||||||
|
placement = placement,
|
||||||
lines = {},
|
lines = {},
|
||||||
win = win,
|
win = win,
|
||||||
invocation_win = previous_win,
|
invocation_win = invocation_win,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function k(lhs, rhs, desc)
|
local function k(lhs, rhs, desc)
|
||||||
@@ -601,15 +652,19 @@ local function open(r)
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
k("<Tab>", function()
|
k("<Tab>", function()
|
||||||
|
if state[bufnr].placement == "sidebar" then
|
||||||
preview_or_open(false)
|
preview_or_open(false)
|
||||||
end, "Preview diff")
|
end
|
||||||
|
end, "Preview diff (sidebar)")
|
||||||
k("<CR>", function()
|
k("<CR>", function()
|
||||||
preview_or_open(true)
|
preview_or_open(true)
|
||||||
end, "Open diff")
|
end, "Open")
|
||||||
k("s", action_stage, "Stage file")
|
k("s", action_stage, "Stage file")
|
||||||
k("u", action_unstage, "Unstage file")
|
k("u", action_unstage, "Unstage file")
|
||||||
k("X", action_discard, "Discard worktree changes")
|
k("X", action_discard, "Discard worktree changes")
|
||||||
k("g?", action_help, "Help")
|
k("g?", function()
|
||||||
|
action_help(state[bufnr].placement)
|
||||||
|
end, "Help")
|
||||||
|
|
||||||
state[bufnr].unsubscribe = r:on("refresh", function()
|
state[bufnr].unsubscribe = r:on("refresh", function()
|
||||||
refresh(bufnr)
|
refresh(bufnr)
|
||||||
@@ -628,23 +683,133 @@ local function open(r)
|
|||||||
state[bufnr] = nil
|
state[bufnr] = nil
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
end
|
||||||
|
|
||||||
vim.api.nvim_set_current_win(previous_win)
|
---Update an existing status view's state and re-place its buffer.
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param placement ow.Git.StatusView.Placement
|
||||||
|
---@param invocation_win integer
|
||||||
|
local function reuse(bufnr, placement, invocation_win)
|
||||||
|
local win = place(bufnr, placement)
|
||||||
|
local s = state[bufnr]
|
||||||
|
if s then
|
||||||
|
s.win = win
|
||||||
|
s.invocation_win = invocation_win
|
||||||
|
s.placement = placement
|
||||||
|
end
|
||||||
|
if placement == "sidebar" then
|
||||||
|
vim.api.nvim_set_current_win(invocation_win)
|
||||||
|
end
|
||||||
refresh(bufnr)
|
refresh(bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.toggle()
|
---@param r ow.Git.Repo
|
||||||
local status_win = find_view()
|
---@param placement ow.Git.StatusView.Placement
|
||||||
if status_win then
|
local function open(r, placement)
|
||||||
vim.api.nvim_win_close(status_win, false)
|
local previous_win = vim.api.nvim_get_current_win()
|
||||||
|
local name = M.URI_PREFIX .. r.worktree
|
||||||
|
local existing_buf = find_buf(name)
|
||||||
|
|
||||||
|
if existing_buf then
|
||||||
|
local visible = vim.fn.bufwinid(existing_buf)
|
||||||
|
if visible ~= -1 then
|
||||||
|
vim.api.nvim_set_current_win(visible)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
reuse(existing_buf, placement, previous_win)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.bo[bufnr].buftype = "nofile"
|
||||||
|
vim.bo[bufnr].swapfile = false
|
||||||
|
vim.bo[bufnr].modifiable = false
|
||||||
|
vim.bo[bufnr].bufhidden = placement == "sidebar" and "wipe" or "hide"
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, name)
|
||||||
|
vim.bo[bufnr].filetype = "gitstatus"
|
||||||
|
|
||||||
|
local win = place(bufnr, placement)
|
||||||
|
setup_buffer(bufnr, r, placement, win, previous_win)
|
||||||
|
|
||||||
|
if placement == "sidebar" then
|
||||||
|
vim.api.nvim_set_current_win(previous_win)
|
||||||
|
end
|
||||||
|
refresh(bufnr)
|
||||||
|
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()
|
local r = repo.resolve()
|
||||||
if not r then
|
if not r then
|
||||||
util.warning("not in a git repository")
|
util.error("not in a git repository")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
open(r)
|
open(r, placement)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set up an existing buffer (created by `:e gitstatus://<worktree>`) as a
|
||||||
|
---status view. Treated as `current` placement. Relative URIs are made
|
||||||
|
---absolute against the cwd.
|
||||||
|
---@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 })
|
||||||
|
reuse(existing, "current", win)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_buf_set_name, buf, canonical)
|
||||||
|
end
|
||||||
|
repo.bind(buf, r)
|
||||||
|
|
||||||
|
vim.bo[buf].buftype = "nofile"
|
||||||
|
vim.bo[buf].swapfile = false
|
||||||
|
vim.bo[buf].modifiable = false
|
||||||
|
vim.bo[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)
|
||||||
|
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
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
Reference in New Issue
Block a user