feat(git): stable cursor + section-header s/u in status view

This commit is contained in:
2026-05-06 03:04:16 +02:00
parent 431ca7bde7
commit 74bfd552f2
+64 -49
View File
@@ -27,10 +27,16 @@ end
---@alias ow.Git.StatusView.Placement "sidebar"|"split"|"current" ---@alias ow.Git.StatusView.Placement "sidebar"|"split"|"current"
---@class ow.Git.StatusView.Header
---@field is_header true
---@field kind ow.Git.Status.EntryKind
---@alias ow.Git.StatusView.Item ow.Git.Status.Entry | ow.Git.StatusView.Header
---@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 placement ow.Git.StatusView.Placement
---@field lines table<integer, ow.Git.Status.Entry> ---@field lines table<integer, ow.Git.StatusView.Item>
---@field win integer? ---@field win integer?
---@field invocation_win integer? ---@field invocation_win integer?
---@field diff_left_win integer? ---@field diff_left_win integer?
@@ -110,6 +116,7 @@ local function render(bufnr, status)
lines, lines,
string.format("%s (%d)", display_name(kind), #entries) string.format("%s (%d)", display_name(kind), #entries)
) )
meta[#lines] = { is_header = true, kind = kind }
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do
local line, hl, hl_len = format_entry(entry) local line, hl, hl_len = format_entry(entry)
table.insert(lines, line) table.insert(lines, line)
@@ -144,36 +151,13 @@ local function refresh(bufnr)
if not s or not vim.api.nvim_buf_is_valid(bufnr) then if not s or not vim.api.nvim_buf_is_valid(bufnr) then
return return
end end
local saved_path
local status_win = win_for(s)
if status_win then
local lnum = vim.api.nvim_win_get_cursor(status_win)[1]
local entry = s.lines[lnum]
if entry then
saved_path = entry.path
end
end
s.last_shown_key = nil s.last_shown_key = nil
render(bufnr, s.repo.status) render(bufnr, s.repo.status)
if not saved_path then
return
end
for lnum, entry in pairs(s.lines) do
if entry.path == saved_path then
local win = win_for(s)
if win then
pcall(vim.api.nvim_win_set_cursor, win, { lnum, 0 })
end
break
end
end
end end
---@param bufnr integer ---@param bufnr integer
---@return ow.Git.StatusView.State? ---@return ow.Git.StatusView.State?
---@return ow.Git.Status.Entry? ---@return ow.Git.StatusView.Item?
local function current_entry(bufnr) local function current_entry(bufnr)
local s = state[bufnr] local s = state[bufnr]
if not s then if not s then
@@ -442,23 +426,41 @@ end
---@param focus_left boolean ---@param focus_left boolean
local function preview_or_open(focus_left) local function preview_or_open(focus_left)
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, item = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry then if not s or not item or item.is_header then
return return
end end
view_entry(s, entry, focus_left) ---@cast item ow.Git.Status.Entry
view_entry(s, item, focus_left)
end end
local function action_stage() local function action_stage()
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, item = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry then if not s or not item then
return return
end end
if entry.kind == "staged" then local paths = {}
if item.is_header then
if item.kind == "staged" or item.kind == "ignored" then
return
end
for _, e in ipairs(s.repo.status:by_kind(item.kind)) do
table.insert(paths, e.path)
end
else
---@cast item ow.Git.Status.Entry
if item.kind == "staged" then
return
end
table.insert(paths, item.path)
end
if #paths == 0 then
return return
end end
local cmd = { "git", "add", "--" }
vim.list_extend(cmd, paths)
vim.system( vim.system(
{ "git", "add", "--", entry.path }, cmd,
{ cwd = s.repo.worktree }, { cwd = s.repo.worktree },
vim.schedule_wrap(function(obj) vim.schedule_wrap(function(obj)
if obj.code ~= 0 then if obj.code ~= 0 then
@@ -469,18 +471,30 @@ local function action_stage()
end end
local function action_unstage() local function action_unstage()
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, item = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry then if not s or not item then
return return
end end
if entry.kind ~= "staged" then if item.kind ~= "staged" then
return return
end end
local cmd = { "git", "restore", "--staged", "--" } local cmd = { "git", "restore", "--staged", "--" }
if entry.orig then local entries
table.insert(cmd, entry.orig) if item.is_header then
entries = s.repo.status:by_kind("staged")
else
---@cast item ow.Git.Status.Entry
entries = { item }
end
if #entries == 0 then
return
end
for _, e in ipairs(entries) do
if e.orig then
table.insert(cmd, e.orig)
end
table.insert(cmd, e.path)
end end
table.insert(cmd, entry.path)
vim.system( vim.system(
cmd, cmd,
{ cwd = s.repo.worktree }, { cwd = s.repo.worktree },
@@ -496,36 +510,37 @@ local function action_unstage()
end end
local function action_discard() local function action_discard()
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, item = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry then if not s or not item or item.is_header then
return return
end end
if entry.kind == "staged" then ---@cast item ow.Git.Status.Entry
if item.kind == "staged" then
util.warning("file has staged changes, unstage first with 'u'") util.warning("file has staged changes, unstage first with 'u'")
return return
end end
local prompt, action local prompt, action
if entry.kind == "untracked" then if item.kind == "untracked" then
local is_dir = entry.path:sub(-1) == "/" local is_dir = item.path:sub(-1) == "/"
prompt = string.format( prompt = string.format(
"Delete untracked %s %s?", "Delete untracked %s %s?",
is_dir and "directory" or "file", is_dir and "directory" or "file",
entry.path item.path
) )
action = function() action = function()
local target = vim.fs.joinpath(s.repo.worktree, entry.path) local target = vim.fs.joinpath(s.repo.worktree, item.path)
local rc = vim.fn.delete(target, is_dir and "rf" or "") local rc = vim.fn.delete(target, is_dir and "rf" or "")
if rc ~= 0 then if rc ~= 0 then
util.error("failed to delete %s", entry.path) util.error("failed to delete %s", item.path)
end end
refresh(vim.api.nvim_get_current_buf()) refresh(vim.api.nvim_get_current_buf())
end end
elseif entry.kind == "unstaged" then elseif item.kind == "unstaged" then
prompt = string.format("Discard changes to %s?", entry.path) prompt = string.format("Discard changes to %s?", item.path)
action = function() action = function()
vim.system( vim.system(
{ "git", "checkout", "--", entry.path }, { "git", "checkout", "--", item.path },
{ cwd = s.repo.worktree }, { cwd = s.repo.worktree },
vim.schedule_wrap(function(obj) vim.schedule_wrap(function(obj)
if obj.code ~= 0 then if obj.code ~= 0 then