feat(hunks): stage selected hunks

This commit is contained in:
2026-05-29 10:22:52 +02:00
parent 7312a9832a
commit 7a243ef907
4 changed files with 159 additions and 36 deletions
+112 -35
View File
@@ -621,20 +621,6 @@ local function hunk_at(hunks, row)
return nil
end
---@param state ow.Git.Hunks.BufState
---@param buf integer
---@param row integer 1-indexed cursor line
---@return ow.Git.Hunks.Hunk?
local function staged_hunk_at(state, buf, row)
local line_count = vim.api.nvim_buf_line_count(buf)
for _, s in ipairs(staged_signs(state, line_count)) do
if s.row == row - 1 then
return s.hunk
end
end
return nil
end
---@param buf integer?
---@return integer buf
---@return ow.Git.Hunks.BufState? state
@@ -748,17 +734,20 @@ end
---@param h ow.Git.Hunks.Hunk
---@param old_lines string[]
---@param rel string
---@return string patch
---@param include_header boolean?
---@param context integer?
---@return string[] patch lines
---@return boolean zero_context
local function build_patch(h, old_lines, rel)
local function build_patch_lines(h, old_lines, rel, include_header, context)
context = context or PATCH_CONTEXT
local old_before, new_before = hunk_offsets(h)
local pre = {}
for i = math.max(old_before - PATCH_CONTEXT + 1, 1), old_before do
for i = math.max(old_before - context + 1, 1), old_before do
pre[#pre + 1] = old_lines[i] or ""
end
local post = {}
local after = old_before + h.old_count
for i = after + 1, math.min(after + PATCH_CONTEXT, #old_lines) do
for i = after + 1, math.min(after + context, #old_lines) do
post[#post + 1] = old_lines[i] or ""
end
local old_n = #pre + h.old_count + #post
@@ -786,9 +775,29 @@ local function build_patch(h, old_lines, rel)
for _, l in ipairs(post) do
body[#body + 1] = " " .. l
end
local lines = { "--- a/" .. rel, "+++ b/" .. rel }
local lines = {}
if include_header ~= false then
lines = { "--- a/" .. rel, "+++ b/" .. rel }
end
vim.list_extend(lines, body)
return table.concat(lines, "\n") .. "\n", #pre == 0 and #post == 0
return lines, #pre == 0 and #post == 0
end
---@param hunks ow.Git.Hunks.Hunk[]
---@param old_lines string[]
---@param rel string
---@param context integer?
---@return string patch
---@return boolean zero_context
local function build_patch(hunks, old_lines, rel, context)
local lines = { "--- a/" .. rel, "+++ b/" .. rel }
local zero_context = false
for _, h in ipairs(hunks) do
local body, zero = build_patch_lines(h, old_lines, rel, false, context)
vim.list_extend(lines, body)
zero_context = zero_context or zero
end
return table.concat(lines, "\n") .. "\n", zero_context
end
---@param state ow.Git.Hunks.BufState
@@ -818,27 +827,95 @@ local function apply_patch(state, buf, patch, zero_context)
})
end
---@param h ow.Git.Hunks.Hunk
---@return integer first 1-indexed buffer line
---@return integer last 1-indexed buffer line
local function hunk_range(h)
if h.type == "delete" then
local line = math.max(h.new_start, 1)
return line, line
end
return h.new_start, h.new_start + h.new_count - 1
end
---@param h ow.Git.Hunks.Hunk
---@param first integer
---@param last integer
---@return boolean
local function hunk_overlaps(h, first, last)
local h_first, h_last = hunk_range(h)
return h_first <= last and h_last >= first
end
---@param state ow.Git.Hunks.BufState
---@param buf integer
---@param first integer
---@param last integer
---@return ow.Git.Hunks.Hunk[]
local function staged_hunks_in_range(state, buf, first, last)
local out = {}
local seen = {}
local line_count = vim.api.nvim_buf_line_count(buf)
for _, s in ipairs(staged_signs(state, line_count)) do
local line = s.row + 1
if line >= first and line <= last and not seen[s.hunk] then
seen[s.hunk] = true
table.insert(out, s.hunk)
end
end
return out
end
---@param first integer
---@param last integer
---@param buf? integer
function M.toggle_stage(buf)
function M.toggle_stage_range(first, last, buf)
buf = resolve_buf(buf)
local state = states[buf]
if not state then
if not state or not state.index then
return
end
if first > last then
first, last = last, first
end
local unstaged = {}
for _, h in ipairs(state.hunks) do
if hunk_overlaps(h, first, last) then
table.insert(unstaged, h)
end
end
if #unstaged > 0 then
local context = #unstaged > 1 and 0 or nil
local patch, zero =
build_patch(unstaged, state.index, state.rel, context)
apply_patch(state, buf, patch, zero)
return
end
local staged = staged_hunks_in_range(state, buf, first, last)
if #staged > 0 then
local inverted = {}
for _, h in ipairs(staged) do
table.insert(inverted, invert(h))
end
local context = #inverted > 1 and 0 or nil
local patch, zero =
build_patch(inverted, state.index, state.rel, context)
apply_patch(state, buf, patch, zero)
return
end
util.warning("git hunks: no hunk in selection")
end
---@param buf? integer
function M.toggle_stage(buf)
local row = vim.api.nvim_win_get_cursor(0)[1]
local unstaged = hunk_at(state.hunks, row)
if unstaged and state.index then
local patch, zero = build_patch(unstaged, state.index, state.rel)
apply_patch(state, buf, patch, zero)
return
end
local staged = staged_hunk_at(state, buf, row)
if staged and state.index then
local patch, zero = build_patch(invert(staged), state.index, state.rel)
apply_patch(state, buf, patch, zero)
return
end
util.warning("git hunks: no hunk at cursor")
M.toggle_stage_range(row, row, buf)
end
---@param buf? integer
function M.toggle_stage_selection(buf)
local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
M.toggle_stage_range(vim.fn.line("v"), cursor_line, buf)
end
---@param buf? integer