Neovim sign gutter, designed to be mostly VCS agnostic
1local M = {}
2
3local diff = require "vcsigns.diff"
4local hunkops = require "vcsigns.hunkops"
5
6local HIGHLIGHT_PRIORITY = 3
7
8local function _highlights_namespace()
9 return vim.api.nvim_create_namespace "vcsigns_highlights"
10end
11
12local function put_virtual_hunk(bufnr, ns, hunk)
13 local intra_diff = diff.intra_diff(hunk)
14 local deletion_at_top = hunk.plus_start == 0 and hunk.plus_count == 0
15
16 local line = hunk.plus_start - 1
17 local virt_lines = {}
18 -- Long string with spaces to get virt_lines highlights to eol.
19 -- Surely there is a better way...
20 local spacer = string.rep(" ", 1000)
21
22 for i, l in ipairs(hunk.minus_lines) do
23 local chunks = {}
24 local intervals = intra_diff.minus_intervals[i] or {}
25 local start = 0
26 for _, interval in ipairs(intervals) do
27 if start < interval[1] then
28 -- Add a chunk for the text before the interval.
29 chunks[#chunks + 1] =
30 { l:sub(start + 1, interval[1]), { "VcsignsDiffDelete" } }
31 end
32 -- Add a chunk for the interval.
33 chunks[#chunks + 1] = {
34 l:sub(interval[1] + 1, interval[2]),
35 { "VcsignsDiffDelete", "VcsignsDiffTextDelete" },
36 }
37 start = interval[2]
38 end
39 -- Add a chunk for the text after the last interval.
40 chunks[#chunks + 1] = { l:sub(start + 1), { "VcsignsDiffDelete" } }
41
42 chunks[#chunks + 1] = { spacer, { "VcsignsDiffDelete" } }
43 virt_lines[#virt_lines + 1] = chunks
44 end
45
46 if deletion_at_top then
47 -- We can't put virtual lines below non-existent line -1.
48 vim.api.nvim_buf_set_extmark(
49 bufnr,
50 ns,
51 0,
52 0,
53 { virt_lines = virt_lines, virt_lines_above = true }
54 )
55 else
56 vim.api.nvim_buf_set_extmark(
57 bufnr,
58 ns,
59 line + hunkops.hunk_visual_size(hunk) - 1,
60 0,
61 { virt_lines = virt_lines }
62 )
63 end
64 if hunk.plus_count > 0 then
65 -- Working around issue with line_hl_group not working with hl_group.
66 -- Workaround from:
67 -- https://github.com/lewis6991/gitsigns.nvim/issues/1115#issuecomment-2319497559
68 --
69 -- vim.api.nvim_buf_set_extmark(bufnr, ns, line, 0, {
70 -- line_hl_group = "VcsignsDiffAdd",
71 -- end_row = line + hunk.plus_count - 1,
72 -- })
73 vim.api.nvim_buf_set_extmark(bufnr, ns, line, 0, {
74 end_line = line + hunk.plus_count,
75 hl_group = "VcsignsDiffAdd",
76 priority = HIGHLIGHT_PRIORITY,
77 end_col = 0,
78 hl_eol = true,
79 strict = false,
80 })
81 end
82
83 -- Fine grained diff.
84 local plus_intervals = intra_diff.plus_intervals
85 for offset, intervals in pairs(plus_intervals) do
86 for _, interval in ipairs(intervals) do
87 local interval_line = line + offset - 1
88 vim.api.nvim_buf_set_extmark(bufnr, ns, interval_line, interval[1], {
89 end_col = interval[2],
90 hl_group = "VcsignsDiffTextAdd",
91 strict = false,
92 })
93 end
94 end
95end
96
97---@param bufnr integer The buffer number.
98---@param hunks Hunk[] A list of hunks to highlight.
99function M.highlight_hunks(bufnr, hunks)
100 local ns = _highlights_namespace()
101 vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
102 -- Reverse order, this seems to create the right order for same line hunks.
103 for hunk in vim.iter(hunks):rev() do
104 put_virtual_hunk(bufnr, ns, hunk)
105 end
106end
107
108return M