fix(hunks): remove hunk overlay

This commit is contained in:
2026-05-29 15:06:17 +02:00
parent 351b5690c2
commit 73df30f1d0
4 changed files with 2 additions and 561 deletions
+1 -398
View File
@@ -5,7 +5,6 @@ local util = require("git.core.util")
local M = {}
local NS_SIGNS = vim.api.nvim_create_namespace("ow.git.hunks")
local NS_OVERLAY = vim.api.nvim_create_namespace("ow.git.hunks.overlay")
local NS_PREVIEW = vim.api.nvim_create_namespace("ow.git.hunks.preview")
local NS_PREVIEW_OVERFLOW =
vim.api.nvim_create_namespace("ow.git.hunks.preview.overflow")
@@ -28,10 +27,8 @@ local NS_PREVIEW_OVERFLOW =
---@field index_sha string?
---@field head string[]?
---@field head_sha string?
---@field index_hl { src: string[], lines: table[][]? }?
---@field hunks ow.Git.Hunks.Hunk[]
---@field staged ow.Git.Hunks.Hunk[]
---@field overlay boolean
---@field autocmds integer[]
---@type table<integer, ow.Git.Hunks.BufState>
@@ -71,16 +68,6 @@ local function diff_opts()
return opts
end
---@return boolean
local function inline_diff_enabled()
for _, item in ipairs(vim.split(vim.o.diffopt, ",", { plain = true })) do
if item == "inline:none" then
return false
end
end
return true
end
---@param old_lines string[]
---@param new_lines string[]
---@return ow.Git.Hunks.Hunk[]
@@ -241,7 +228,7 @@ local function render_signs(buf)
end
vim.api.nvim_buf_clear_namespace(buf, NS_SIGNS, 0, -1)
local state = states[buf]
if not state or state.overlay then
if not state then
return
end
local signs = resolve_signs()
@@ -268,379 +255,9 @@ local function render_signs(buf)
end
end
local SKIP_CAPTURES = { spell = true, nospell = true, conceal = true }
---@alias ow.Git.Hunks.InlineSpan { start_col: integer, end_col: integer }
---@param line string
---@return { text: string, start_col: integer, end_col: integer }[]
local function char_pieces(line)
local out = {}
local start = 1
for c in line:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
local stop = start + #c - 1
table.insert(out, {
text = c,
start_col = start - 1,
end_col = stop,
})
start = stop + 1
end
if start <= #line then
table.insert(out, {
text = line:sub(start),
start_col = start - 1,
end_col = #line,
})
end
return out
end
---@param pieces { text: string, start_col: integer, end_col: integer }[]
---@param start_idx integer
---@param count integer
---@return ow.Git.Hunks.InlineSpan?
local function char_span(pieces, start_idx, count)
if count <= 0 then
return nil
end
local first = pieces[start_idx]
local last = pieces[start_idx + count - 1]
if not first or not last then
return nil
end
return { start_col = first.start_col, end_col = last.end_col }
end
---@param old_line string
---@param new_line string
---@return ow.Git.Hunks.InlineSpan[] old_spans
---@return ow.Git.Hunks.InlineSpan[] new_spans
local function inline_spans(old_line, new_line)
if old_line == new_line or not inline_diff_enabled() then
return {}, {}
end
local old_pieces = char_pieces(old_line)
local new_pieces = char_pieces(new_line)
if #old_pieces == 0 then
return {},
#new_pieces > 0 and { { start_col = 0, end_col = #new_line } } or {}
end
if #new_pieces == 0 then
return { { start_col = 0, end_col = #old_line } }, {}
end
local old_text = {}
for _, c in ipairs(old_pieces) do
table.insert(old_text, c.text)
end
local new_text = {}
for _, c in ipairs(new_pieces) do
table.insert(new_text, c.text)
end
local raw = vim.text.diff(
table.concat(old_text, "\n"),
table.concat(new_text, "\n"),
diff_opts()
)
if type(raw) ~= "table" then
return { { start_col = 0, end_col = #old_line } }, {
{ start_col = 0, end_col = #new_line },
}
end
local old_spans = {}
local new_spans = {}
for _, h in ipairs(raw) do
local os_ = h[1] --[[@as integer]]
local oc = h[2] --[[@as integer]]
local ns_ = h[3] --[[@as integer]]
local nc = h[4] --[[@as integer]]
local old_span = char_span(old_pieces, os_, oc)
if old_span then
table.insert(old_spans, old_span)
end
local new_span = char_span(new_pieces, ns_, nc)
if new_span then
table.insert(new_spans, new_span)
end
end
return old_spans, new_spans
end
---@param hl string|string[]
---@return string[]
local function hl_list(hl)
if type(hl) == "table" then
return vim.list_extend({}, hl)
end
return { hl }
end
---@param col integer
---@param spans ow.Git.Hunks.InlineSpan[]
---@return boolean
local function in_inline_span(col, spans)
for _, span in ipairs(spans) do
if col >= span.start_col and col < span.end_col then
return true
end
end
return false
end
---@param line string
---@param chunks table[]
---@param spans ow.Git.Hunks.InlineSpan[]?
---@return table[]
local function apply_inline_to_chunks(line, chunks, spans)
if not spans or #spans == 0 then
return chunks
end
local out = {}
local col = 0
for _, chunk in ipairs(chunks) do
local text = chunk[1]
local hl = chunk[2]
local pos = 1
while pos <= #text do
local inline = in_inline_span(col, spans)
local stop = pos + 1
while
stop <= #text
and in_inline_span(col + stop - pos, spans) == inline
do
stop = stop + 1
end
local piece = text:sub(pos, stop - 1)
local piece_hl = hl
if inline then
piece_hl = hl_list(hl)
table.insert(piece_hl, "DiffText")
end
table.insert(out, { piece, piece_hl })
col = col + #piece
pos = stop
end
end
if col < #line then
return chunks
end
return out
end
---@param buf integer
---@param lines string[]
---@return table[][]?
local function highlight_index(buf, lines)
if not vim.treesitter.highlighter.active[buf] then
return nil
end
local got, parser = pcall(vim.treesitter.get_parser, buf)
if not got or not parser then
return nil
end
local lang = parser:lang()
local query = vim.treesitter.query.get(lang, "highlights")
if not query then
return nil
end
local source = table.concat(lines, "\n")
local got_root, root = pcall(function()
local trees = vim.treesitter.get_string_parser(source, lang):parse()
local tree = trees and trees[1]
return tree and tree:root()
end)
if not got_root or not root then
return nil
end
---@type table<integer, table<integer, string>>
local groups = {}
for id, node in query:iter_captures(root, source) do
local name = query.captures[id]
if name and name:sub(1, 1) ~= "_" and not SKIP_CAPTURES[name] then
local sr, sc, er, ec = node:range()
for row = sr, math.min(er, #lines - 1) do
local row_groups = groups[row] or {}
groups[row] = row_groups
local from = row == sr and sc or 0
local to = row == er and ec or #(lines[row + 1] or "")
for col = from, to - 1 do
row_groups[col] = name
end
end
end
end
local out = {}
for row = 0, #lines - 1 do
local line = lines[row + 1] or ""
local row_groups = groups[row] or {}
local chunks = {}
local col = 0
while col < #line do
local name = row_groups[col]
local stop = col + 1
while stop < #line and row_groups[stop] == name do
stop = stop + 1
end
local hl ---@type string|string[]
if name then
hl = { "GitHunkDeleteLine", "@" .. name }
else
hl = "GitHunkDeleteLine"
end
table.insert(chunks, { line:sub(col + 1, stop), hl })
col = stop
end
out[row + 1] = chunks
end
return out
end
---@param h ow.Git.Hunks.Hunk
---@param hl_lines table[][]? per-index-line syntax chunks, or nil
---@param inline_by_line table<integer, ow.Git.Hunks.InlineSpan[]>?
---@return table[]
local function delete_virt_lines(h, hl_lines, inline_by_line)
local width = vim.o.columns
local virt = {}
for i, line in ipairs(h.old_lines) do
local pad = math.max(width - vim.api.nvim_strwidth(line), 0)
local cached = hl_lines and hl_lines[h.old_start + i - 1]
local inline = inline_by_line and inline_by_line[i]
if cached then
local chunks = apply_inline_to_chunks(
line,
vim.list_extend({}, cached),
inline
)
table.insert(chunks, {
string.rep(" ", pad),
"GitHunkDeleteLine",
})
table.insert(virt, chunks)
else
local chunks = apply_inline_to_chunks(
line,
{ { line, "GitHunkDeleteLine" } },
inline
)
table.insert(chunks, {
string.rep(" ", pad),
"GitHunkDeleteLine",
})
table.insert(virt, chunks)
end
end
return virt
end
---@param h ow.Git.Hunks.Hunk
---@return table<integer, ow.Git.Hunks.InlineSpan[]> old_by_line
---@return table<integer, ow.Git.Hunks.InlineSpan[]> new_by_line
local function hunk_inline_spans(h)
if h.type ~= "change" or #h.old_lines ~= #h.new_lines then
return {}, {}
end
local old_by_line = {}
local new_by_line = {}
for i = 1, math.min(#h.old_lines, #h.new_lines) do
local old_line = h.old_lines[i] or ""
local new_line = h.new_lines[i] or ""
local old_spans, new_spans = inline_spans(old_line, new_line)
old_by_line[i] = old_spans
new_by_line[i] = new_spans
end
return old_by_line, new_by_line
end
---@param state ow.Git.Hunks.BufState
---@param buf integer
---@return table[][]?
local function index_spans(state, buf)
if not state.index then
return nil
end
local cache = state.index_hl
if cache and cache.src == state.index then
return cache.lines
end
local lines = highlight_index(buf, state.index)
state.index_hl = { src = state.index, lines = lines }
return lines
end
---@param buf integer
local function render_overlay(buf)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
vim.api.nvim_buf_clear_namespace(buf, NS_OVERLAY, 0, -1)
local state = states[buf]
if not state or not state.overlay then
return
end
local line_count = vim.api.nvim_buf_line_count(buf)
local hl_lines = index_spans(state, buf)
for _, h in ipairs(state.hunks) do
local old_inline, new_inline = hunk_inline_spans(h)
if h.type ~= "delete" then
for r = h.new_start, h.new_start + h.new_count - 1 do
local row = r - 1
if row >= 0 and row < line_count then
pcall(
vim.api.nvim_buf_set_extmark,
buf,
NS_OVERLAY,
row,
0,
{
line_hl_group = "GitHunkAddLine",
priority = 100,
}
)
for _, span in ipairs(new_inline[r - h.new_start + 1] or {}) do
pcall(
vim.api.nvim_buf_set_extmark,
buf,
NS_OVERLAY,
row,
span.start_col,
{
end_col = span.end_col,
hl_group = "DiffText",
priority = 101,
}
)
end
end
end
end
if h.type ~= "add" then
local row, above
if h.type == "delete" then
if h.new_start <= 0 then
row, above = 0, true
elseif h.new_start >= line_count then
row, above = math.max(line_count - 1, 0), false
else
row, above = h.new_start, true
end
else
row, above = math.max(h.new_start - 1, 0), true
end
pcall(vim.api.nvim_buf_set_extmark, buf, NS_OVERLAY, row, 0, {
virt_lines = delete_virt_lines(h, hl_lines, old_inline),
virt_lines_above = above,
right_gravity = false,
invalidate = true,
})
end
end
end
---@param buf integer
local function render(buf)
render_signs(buf)
render_overlay(buf)
end
---@param state ow.Git.Hunks.BufState
@@ -758,7 +375,6 @@ function M.attach(buf)
head_sha = nil,
hunks = {},
staged = {},
overlay = false,
autocmds = {},
}
states[buf] = state
@@ -797,7 +413,6 @@ function M.detach(buf)
end
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_clear_namespace(buf, NS_SIGNS, 0, -1)
vim.api.nvim_buf_clear_namespace(buf, NS_OVERLAY, 0, -1)
end
for _, id in ipairs(state.autocmds) do
pcall(vim.api.nvim_del_autocmd, id)
@@ -807,18 +422,6 @@ function M.detach(buf)
states[buf] = nil
end
---@param buf integer?
function M.toggle_overlay(buf)
buf = resolve_buf(buf)
local state = states[buf]
if not state then
util.warning("git hunks: buffer not attached")
return
end
state.overlay = not state.overlay
render(buf)
end
---@param hunks ow.Git.Hunks.Hunk[]
---@param row integer 1-indexed cursor line
---@return ow.Git.Hunks.Hunk?