Skip to content

Commit f8346f3

Browse files
committed
feat: add heat map
1 parent 4c40357 commit f8346f3

File tree

9 files changed

+224
-21
lines changed

9 files changed

+224
-21
lines changed

lua/gitsigns/actions.lua

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ M.toggle_word_diff = function(value)
115115
config.word_diff = not config.word_diff
116116
end
117117
-- Don't use refresh() to avoid flicker
118-
util.redraw({ buf = 0, range = { vim.fn.line('w0') - 1, vim.fn.line('w$') } })
118+
util.redraw({ range = { 0, -1 } })
119119
return config.word_diff
120120
end
121121

@@ -150,6 +150,18 @@ M.toggle_deleted = function(value)
150150
return config.show_deleted
151151
end
152152

153+
M.toggle_heatmap = function(value)
154+
if value ~= nil then
155+
config.heat_map = value
156+
else
157+
config.heat_map = not config.heat_map
158+
end
159+
160+
M.refresh()
161+
162+
return config.heat_map
163+
end
164+
153165
--- @param bufnr? integer
154166
--- @param hunks? Gitsigns.Hunk.Hunk[]?
155167
--- @return Gitsigns.Hunk.Hunk? hunk
@@ -1546,6 +1558,7 @@ M.refresh = async.create(0, function()
15461558
v:invalidate(true)
15471559
manager.update(k)
15481560
end
1561+
util.redraw({ range = { 0, -1 } })
15491562
end)
15501563

15511564
--- @param name string

lua/gitsigns/blame.lua

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ end
4646
---@param text string
4747
---@return string
4848
local function lalign(amount, text)
49-
local len = vim.str_utfindex(text)
49+
local len = vim.str_utfindex(text, 'utf-8')
5050
return text .. string.rep(' ', math.max(0, amount - len))
5151
end
5252

@@ -66,30 +66,32 @@ local M = {}
6666
local function render(blame, win, main_win, buf_sha)
6767
local max_author_len = 0
6868

69-
for _, blame_info in pairs(blame) do
70-
max_author_len = math.max(max_author_len, (vim.str_utfindex(blame_info.commit.author)))
69+
for _, b in pairs(blame) do
70+
max_author_len = math.max(max_author_len, (vim.str_utfindex(b.commit.author, 'utf-8')))
7171
end
7272

7373
local lines = {} --- @type string[]
7474
local last_sha --- @type string?
7575
local cnt = 0
7676
local commit_lines = {} --- @type table<integer,true>
77-
for i, hl in pairs(blame) do
78-
local sha = hl.commit.abbrev_sha
77+
78+
for i, b in pairs(blame) do
79+
local commit = b.commit
80+
local sha = commit.abbrev_sha
7981
local next_sha = blame[i + 1] and blame[i + 1].commit.abbrev_sha or nil
8082
if sha == last_sha then
8183
cnt = cnt + 1
8284
local c = sha == next_sha and chars.mid or chars.last
83-
lines[i] = cnt == 1 and string.format('%s %s', c, hl.commit.summary) or c
85+
lines[i] = cnt == 1 and string.format('%s %s', c, commit.summary) or c
8486
else
8587
cnt = 0
8688
commit_lines[i] = true
8789
lines[i] = string.format(
8890
'%s %s %s %s',
8991
chars.first,
9092
sha,
91-
lalign(max_author_len, hl.commit.author),
92-
util.expand_format('<author_time>', hl.commit)
93+
lalign(max_author_len, commit.author),
94+
util.expand_format('<author_time>', commit)
9395
)
9496
end
9597
last_sha = sha

lua/gitsigns/cache.lua

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ local M = {
55
CacheEntry = {},
66
}
77

8+
--- @class (exact) Gitsigns.CacheEntry.Blame
9+
--- @field [integer] Gitsigns.BlameInfo?
10+
--- @field max_time? integer
11+
--- @field min_time? integer
12+
813
--- @class (exact) Gitsigns.CacheEntry
914
--- @field bufnr integer
1015
--- @field file string
@@ -19,7 +24,8 @@ local M = {
1924
--- @field staged_diffs? Gitsigns.Hunk.Hunk[]
2025
--- @field gitdir_watcher? uv.uv_fs_event_t
2126
--- @field git_obj Gitsigns.GitObj
22-
--- @field blame? table<integer,Gitsigns.BlameInfo?>
27+
--- @field blame? Gitsigns.CacheEntry.Blame
28+
--- @field commits? table<string,Gitsigns.CommitInfo?>
2329
---
2430
--- @field update_lock? true Update in progress
2531
local CacheEntry = M.CacheEntry
@@ -43,6 +49,7 @@ function CacheEntry:invalidate(all)
4349
self.hunks = nil
4450
self.hunks_staged = nil
4551
self.blame = nil
52+
self.commits = nil
4653
if all then
4754
-- The below doesn't need to be invalidated
4855
-- if the buffer changes
@@ -82,6 +89,7 @@ local BLAME_THRESHOLD_LEN = 10000
8289
--- @param lnum? integer
8390
--- @param opts? Gitsigns.BlameOpts
8491
--- @return table<integer,Gitsigns.BlameInfo?>
92+
--- @return table<string,Gitsigns.CommitInfo?>
8593
--- @return boolean? full
8694
function CacheEntry:run_blame(lnum, opts)
8795
local bufnr = self.bufnr
@@ -100,13 +108,13 @@ function CacheEntry:run_blame(lnum, opts)
100108
local tick = vim.b[bufnr].changedtick
101109
local lnum0 = vim.api.nvim_buf_line_count(bufnr) > BLAME_THRESHOLD_LEN and lnum or nil
102110
-- TODO(lewis6991): Cancel blame on changedtick
103-
local blame = self.git_obj:run_blame(contents, lnum0, self.git_obj.revision, opts)
111+
local blame, commits = self.git_obj:run_blame(contents, lnum0, self.git_obj.revision, opts)
104112
async.schedule()
105113
if not vim.api.nvim_buf_is_valid(bufnr) then
106-
return {}
114+
return {}, {}
107115
end
108116
if vim.b[bufnr].changedtick == tick then
109-
return blame, lnum0 == nil
117+
return blame, commits, lnum0 == nil
110118
end
111119
end
112120
end
@@ -153,7 +161,8 @@ function CacheEntry:get_blame(lnum, opts)
153161
blame[lnum] = Blame.get_blame_nc(relpath, lnum)
154162
else
155163
-- Refresh/update cache
156-
local b, full = self:run_blame(lnum, opts)
164+
local b, commits, full = self:run_blame(lnum, opts)
165+
self.commits = vim.tbl_extend('force', self.commits or {}, commits)
157166
if lnum and not full then
158167
blame[lnum] = b[lnum]
159168
else
@@ -166,6 +175,25 @@ function CacheEntry:get_blame(lnum, opts)
166175
return blame[lnum]
167176
end
168177

178+
--- @async
179+
function CacheEntry:update_blame_times()
180+
self:get_blame()
181+
local blame = assert(self.blame)
182+
183+
if blame.max_time and blame.min_time then
184+
return
185+
end
186+
187+
local min_time = math.huge
188+
for _, c in pairs(assert(self.commits)) do
189+
min_time = math.min(min_time, c.author_time)
190+
end
191+
192+
blame.min_time = min_time
193+
--- @diagnostic disable-next-line: undefined-field
194+
blame.max_time = vim.uv.clock_gettime('realtime').sec
195+
end
196+
169197
function CacheEntry:destroy()
170198
local w = self.gitdir_watcher
171199
if w and not w:is_closing() then

lua/gitsigns/color.lua

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
local M = {}
2+
3+
--- @param value integer
4+
--- @return [integer, integer, integer]
5+
function M.int_to_rgb(value)
6+
local r = bit.band(bit.rshift(value, 16), 0xFF)
7+
local g = bit.band(bit.rshift(value, 8), 0xFF)
8+
local b = bit.band(value, 0xFF)
9+
return { r, g, b }
10+
end
11+
12+
--- @param rgb [integer, integer, integer]
13+
--- @return integer
14+
function M.rgb_to_int(rgb)
15+
return rgb[1] * 0x10000 + rgb[2] * 0x100 + rgb[3]
16+
end
17+
18+
--- @param stops [integer,integer,integer][]
19+
--- @param t number 0-1
20+
--- @return [integer, integer, integer]
21+
function M.gradient(stops, t)
22+
local num_stops = #stops
23+
if num_stops < 2 then
24+
error('At least two color stops are required')
25+
end
26+
27+
local segment_length = 1 / (num_stops - 1)
28+
local segment_index = math.floor(t / segment_length)
29+
30+
if segment_index >= num_stops - 1 then
31+
return { stops[num_stops][1], stops[num_stops][2], stops[num_stops][3] }
32+
end
33+
34+
local local_t = (t - segment_index * segment_length) / segment_length
35+
36+
local color1 = stops[segment_index + 1]
37+
local color2 = stops[segment_index + 2]
38+
39+
return M.blend(color1, color2, local_t)
40+
end
41+
42+
--- @param a integer
43+
--- @param b integer
44+
--- @param alpha number
45+
--- @return integer
46+
local function lerp(a, b, alpha)
47+
return math.floor(a + (b - a) * alpha)
48+
end
49+
50+
--- @param color1 [integer, integer, integer]
51+
--- @param color2 [integer, integer, integer]
52+
--- @param alpha number 0-1
53+
--- @return [integer, integer, integer]
54+
function M.blend(color1, color2, alpha)
55+
return {
56+
lerp(color1[1], color2[1], alpha),
57+
lerp(color1[2], color2[2], alpha),
58+
lerp(color1[3], color2[3], alpha),
59+
}
60+
end
61+
62+
local temp_color_stops = {
63+
{ 0, 0, 255 }, -- Blue
64+
{ 255, 0, 0 }, -- Red
65+
{ 255, 255, 0 }, -- Yellow
66+
}
67+
68+
--- @param value number 0-1
69+
--- @return [integer, integer, integer]
70+
function M.temp(value)
71+
return M.gradient(temp_color_stops, value)
72+
end
73+
74+
return M

lua/gitsigns/config.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
--- @field linehl boolean
6565
--- @field culhl boolean
6666
--- @field show_deleted boolean
67+
--- @field heat_map boolean
6768
--- @field sign_priority integer
6869
--- @field _on_attach_pre fun(bufnr: integer, callback: fun(_: table))
6970
--- @field on_attach fun(bufnr: integer)
@@ -502,6 +503,14 @@ M.schema = {
502503
]],
503504
},
504505

506+
heat_map = {
507+
type = 'boolean',
508+
default = false,
509+
description = [[
510+
Show a blame heatmap for the current buffer.
511+
]],
512+
},
513+
505514
diff_opts = {
506515
type = 'table',
507516
deep_extend = true,

lua/gitsigns/git.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ end
129129
--- @param revision? string
130130
--- @param opts? Gitsigns.BlameOpts
131131
--- @return table<integer,Gitsigns.BlameInfo?>
132+
--- @return table<string,Gitsigns.CommitInfo?>
132133
function Obj:run_blame(contents, lnum, revision, opts)
133134
return require('gitsigns.git.blame').run_blame(self, contents, lnum, revision, opts)
134135
end

lua/gitsigns/git/blame.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ end
200200
--- @param revision? string
201201
--- @param opts? Gitsigns.BlameOpts
202202
--- @return table<integer, Gitsigns.BlameInfo>
203+
--- @return table<string, Gitsigns.CommitInfo>
203204
function M.run_blame(obj, contents, lnum, revision, opts)
204205
local ret = {} --- @type table<integer,Gitsigns.BlameInfo>
205206

@@ -217,7 +218,7 @@ function M.run_blame(obj, contents, lnum, revision, opts)
217218
filename = obj.file,
218219
}
219220
end
220-
return ret
221+
return ret, {}
221222
end
222223

223224
opts = opts or {}
@@ -256,10 +257,9 @@ function M.run_blame(obj, contents, lnum, revision, opts)
256257

257258
if stderr then
258259
error_once('Error running git-blame: ' .. stderr)
259-
return {}
260260
end
261261

262-
return ret
262+
return ret, commits
263263
end
264264

265265
return M

0 commit comments

Comments
 (0)