feat(git): syntax-highlight deleted lines in the diff overlay
This commit is contained in:
+102
-3
@@ -22,6 +22,7 @@ local NS_OVERLAY = vim.api.nvim_create_namespace("ow.git.hunks.overlay")
|
|||||||
---@field rel string
|
---@field rel string
|
||||||
---@field index string[]?
|
---@field index string[]?
|
||||||
---@field index_sha string?
|
---@field index_sha string?
|
||||||
|
---@field index_hl { src: string[], lines: table[][]? }?
|
||||||
---@field hunks ow.Git.Hunks.Hunk[]
|
---@field hunks ow.Git.Hunks.Hunk[]
|
||||||
---@field overlay boolean
|
---@field overlay boolean
|
||||||
---@field autocmds integer[]
|
---@field autocmds integer[]
|
||||||
@@ -153,20 +154,117 @@ local function render_signs(buf)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local SKIP_CAPTURES = { spell = true, nospell = true, conceal = true }
|
||||||
|
|
||||||
|
---@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 h ow.Git.Hunks.Hunk
|
||||||
|
---@param hl_lines table[][]? per-index-line syntax chunks, or nil
|
||||||
---@return table[]
|
---@return table[]
|
||||||
local function delete_virt_lines(h)
|
local function delete_virt_lines(h, hl_lines)
|
||||||
local width = vim.o.columns
|
local width = vim.o.columns
|
||||||
local virt = {}
|
local virt = {}
|
||||||
for _, line in ipairs(h.old_lines) do
|
for i, line in ipairs(h.old_lines) do
|
||||||
local pad = math.max(width - vim.api.nvim_strwidth(line), 0)
|
local pad = math.max(width - vim.api.nvim_strwidth(line), 0)
|
||||||
|
local cached = hl_lines and hl_lines[h.old_start + i - 1]
|
||||||
|
if cached then
|
||||||
|
local chunks = vim.list_extend({}, cached)
|
||||||
|
table.insert(chunks, {
|
||||||
|
string.rep(" ", pad),
|
||||||
|
"GitHunkDeleteLine",
|
||||||
|
})
|
||||||
|
table.insert(virt, chunks)
|
||||||
|
else
|
||||||
table.insert(virt, {
|
table.insert(virt, {
|
||||||
{ line .. string.rep(" ", pad), "GitHunkDeleteLine" },
|
{ line .. string.rep(" ", pad), "GitHunkDeleteLine" },
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
end
|
||||||
return virt
|
return virt
|
||||||
end
|
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
|
---@param buf integer
|
||||||
local function render_overlay(buf)
|
local function render_overlay(buf)
|
||||||
if not vim.api.nvim_buf_is_valid(buf) then
|
if not vim.api.nvim_buf_is_valid(buf) then
|
||||||
@@ -178,6 +276,7 @@ local function render_overlay(buf)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local line_count = vim.api.nvim_buf_line_count(buf)
|
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||||
|
local hl_lines = index_spans(state, buf)
|
||||||
for _, h in ipairs(state.hunks) do
|
for _, h in ipairs(state.hunks) do
|
||||||
if h.type ~= "delete" then
|
if h.type ~= "delete" then
|
||||||
for r = h.new_start, h.new_start + h.new_count - 1 do
|
for r = h.new_start, h.new_start + h.new_count - 1 do
|
||||||
@@ -211,7 +310,7 @@ local function render_overlay(buf)
|
|||||||
row, above = math.max(h.new_start - 1, 0), true
|
row, above = math.max(h.new_start - 1, 0), true
|
||||||
end
|
end
|
||||||
pcall(vim.api.nvim_buf_set_extmark, buf, NS_OVERLAY, row, 0, {
|
pcall(vim.api.nvim_buf_set_extmark, buf, NS_OVERLAY, row, 0, {
|
||||||
virt_lines = delete_virt_lines(h),
|
virt_lines = delete_virt_lines(h, hl_lines),
|
||||||
virt_lines_above = above,
|
virt_lines_above = above,
|
||||||
right_gravity = false,
|
right_gravity = false,
|
||||||
invalidate = true,
|
invalidate = true,
|
||||||
|
|||||||
+44
-6
@@ -82,7 +82,7 @@ t.test("pure delete (middle): hunk shape and delete sign", function()
|
|||||||
t.eq(hk.new_count, 0)
|
t.eq(hk.new_count, 0)
|
||||||
t.eq(hk.old_lines, { "b" })
|
t.eq(hk.old_lines, { "b" })
|
||||||
t.eq(sign_marks(buf), {
|
t.eq(sign_marks(buf), {
|
||||||
{ row = 0, sign = "┃", hl = "GitHunkDelete" },
|
{ row = 0, sign = "▁", hl = "GitHunkDelete" },
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ t.test("top-of-file delete: sign anchors on line 1", function()
|
|||||||
t.eq(hk.new_start, 0)
|
t.eq(hk.new_start, 0)
|
||||||
t.eq(hk.old_lines, { "a" })
|
t.eq(hk.old_lines, { "a" })
|
||||||
t.eq(sign_marks(buf), {
|
t.eq(sign_marks(buf), {
|
||||||
{ row = 0, sign = "┃", hl = "GitHunkDelete" },
|
{ row = 0, sign = "▁", hl = "GitHunkDelete" },
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ t.test("editing the buffer refreshes signs", function()
|
|||||||
t.eq(hk.type, "change")
|
t.eq(hk.type, "change")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("overlay:change hunk shows deletion and addition", function()
|
t.test("overlay: change hunk shows deletion and addition", function()
|
||||||
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
hunks.toggle_overlay(buf)
|
hunks.toggle_overlay(buf)
|
||||||
---@type integer?
|
---@type integer?
|
||||||
@@ -169,7 +169,7 @@ t.test("overlay:change hunk shows deletion and addition", function()
|
|||||||
t.eq(piece[2], "GitHunkDeleteLine")
|
t.eq(piece[2], "GitHunkDeleteLine")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("overlay:delete hunk shows only deletion lines", function()
|
t.test("overlay: delete hunk shows only deletion lines", function()
|
||||||
local _, buf = setup("a\nb\nc\n", "a\nc\n")
|
local _, buf = setup("a\nb\nc\n", "a\nc\n")
|
||||||
hunks.toggle_overlay(buf)
|
hunks.toggle_overlay(buf)
|
||||||
local marks = detailed_marks(buf, "ow.git.hunks.overlay")
|
local marks = detailed_marks(buf, "ow.git.hunks.overlay")
|
||||||
@@ -180,7 +180,7 @@ t.test("overlay:delete hunk shows only deletion lines", function()
|
|||||||
t.eq(piece[2], "GitHunkDeleteLine")
|
t.eq(piece[2], "GitHunkDeleteLine")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("overlay:add hunk highlights the added lines", function()
|
t.test("overlay: add hunk highlights the added lines", function()
|
||||||
local _, buf = setup("a\nd\n", "a\nb\nc\nd\n")
|
local _, buf = setup("a\nd\n", "a\nb\nc\nd\n")
|
||||||
hunks.toggle_overlay(buf)
|
hunks.toggle_overlay(buf)
|
||||||
local rows = {}
|
local rows = {}
|
||||||
@@ -194,7 +194,45 @@ t.test("overlay:add hunk highlights the added lines", function()
|
|||||||
t.eq(rows, { 1, 2 }, "both added lines highlighted")
|
t.eq(rows, { 1, 2 }, "both added lines highlighted")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("overlay:toggling swaps gutter signs for the overlay", function()
|
t.test("overlay: deleted lines are treesitter-highlighted", function()
|
||||||
|
local _, buf = setup(
|
||||||
|
"-- a note\nlocal x = 1\nlocal y = 2\n",
|
||||||
|
"local y = 2\n",
|
||||||
|
"a.lua"
|
||||||
|
)
|
||||||
|
t.truthy(
|
||||||
|
pcall(vim.treesitter.start, buf, "lua"),
|
||||||
|
"the lua parser should be available"
|
||||||
|
)
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
---@type table[]?
|
||||||
|
local virt
|
||||||
|
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
|
||||||
|
local d = assert(m[4])
|
||||||
|
if d.virt_lines then
|
||||||
|
virt = d.virt_lines
|
||||||
|
end
|
||||||
|
end
|
||||||
|
virt = assert(virt, "a deletion virtual line should render")
|
||||||
|
---@type table<string, boolean>
|
||||||
|
local seen = {}
|
||||||
|
for _, line in ipairs(virt) do
|
||||||
|
for _, c in ipairs(line) do
|
||||||
|
local hl = c[2]
|
||||||
|
if
|
||||||
|
type(hl) == "table"
|
||||||
|
and hl[1] == "GitHunkDeleteLine"
|
||||||
|
and hl[2]
|
||||||
|
then
|
||||||
|
seen[hl[2]] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
t.truthy(seen["@comment"], "the deleted comment keeps its @comment group")
|
||||||
|
t.truthy(seen["@keyword"], "deleted code keeps its syntax groups")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("overlay: toggling swaps gutter signs for the overlay", function()
|
||||||
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
t.truthy(
|
t.truthy(
|
||||||
#detailed_marks(buf, "ow.git.hunks") > 0,
|
#detailed_marks(buf, "ow.git.hunks") > 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user