···11+local M = {}
22+33+local GOLDEN_RATIO_CONJUGATE = 0.618033988749895
44+55+---@param str string
66+---@return number
77+local function hash(str)
88+ local h = 5381
99+ for i = 1, #str do
1010+ h = (bit.lshift(h, 5) + h) + string.byte(str, i)
1111+ h = h % 2 ^ 32
1212+ end
1313+ return h
1414+end
1515+1616+---@param hue number
1717+---@param sat number
1818+---@param light number
1919+---@return string
2020+local function hsl_to_hex(hue, sat, light)
2121+ local h = hue / 60
2222+ local c = (1 - math.abs(2 * light - 1)) * sat
2323+ local x = c * (1 - math.abs(h % 2 - 1))
2424+ local m = light - c / 2
2525+2626+ local r, g, b
2727+ if h < 1 then
2828+ r, g, b = c, x, 0
2929+ elseif h < 2 then
3030+ r, g, b = x, c, 0
3131+ elseif h < 3 then
3232+ r, g, b = 0, c, x
3333+ elseif h < 4 then
3434+ r, g, b = 0, x, c
3535+ elseif h < 5 then
3636+ r, g, b = x, 0, c
3737+ else
3838+ r, g, b = c, 0, x
3939+ end
4040+4141+ return string.format("#%02x%02x%02x", math.floor((r + m) * 255), math.floor((g + m) * 255), math.floor((b + m) * 255))
4242+end
4343+4444+--- Returns a stable "golden ratio" color from a given string, in hex format.
4545+---@param str string
4646+---@return string
4747+function M.stable_from_string(str)
4848+ local hue = ((hash(str) * GOLDEN_RATIO_CONJUGATE) % 1) * 360
4949+ return hsl_to_hex(hue, 0.5, 0.7)
5050+end
5151+5252+return M
+56-17
lua/jjannotate.lua
···11+local ns = vim.api.nvim_create_namespace("jjannotate")
22+local color = require("color")
13local notify = vim.schedule_wrap(vim.notify)
2435---@class jjannotate.ParseResult
···6365 return { lines = lines, longest_timestamp = longest_timestamp }
6466end
65676666----@param line jjannotate.Line
6868+---@param line jjannotate.Line | nil
6769---@param timestamp_len integer
6870---@return string
6971local function render_line(line, timestamp_len)
7272+ if line == nil then return "Untracked" end
7073 return string.format("%s %-" .. timestamp_len .. "s %s", line.change_id, line.timestamp, line.author)
7174end
72757376---@param parsed jjannotate.ParseResult
7474----@param scrollbind boolean
7575-local function open_win(parsed, scrollbind)
7777+---@return integer, string[]
7878+local function create_buf(parsed)
7679 -- TODO: if there's an existing jjannotate buffer, reuse it
7777- -- TODO: if there's an existing jjannotate window, reuse it
78807979- -- setup buffer
8081 local buf = vim.api.nvim_create_buf(false, true)
8182 vim.bo[buf].buftype = "nofile"
8283 vim.bo[buf].filetype = "jjannotate"
83848484- -- render lines
8585 local lines = {}
8686 for _, line in ipairs(parsed.lines) do
8787- if line then
8888- table.insert(lines, render_line(line, parsed.longest_timestamp))
8989- else
9090- table.insert(lines, "Untracked")
9191- end
8787+ table.insert(lines, render_line(line, parsed.longest_timestamp))
9288 end
9393-9494- -- print(vim.inspect(lines))
95899690 vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
9791···9993 vim.bo[buf].modifiable = false
10094 vim.bo[buf].readonly = true
10195102102- -- setup window
9696+ return buf, lines
9797+end
9898+9999+---@param buf integer
100100+---@param parsed jjannotate.ParseResult
101101+local function apply_highlights(buf, parsed)
102102+ local hl_cache = {}
103103+ local hl_idx = 0
104104+105105+ for i, line in ipairs(parsed.lines) do
106106+ if line then
107107+ local change_id = line.change_id
108108+ if not hl_cache[change_id] then
109109+ hl_idx = hl_idx + 1
110110+ local hl_name = "JJAnnotate" .. hl_idx
111111+ vim.api.nvim_set_hl(0, hl_name, { fg = color.stable_from_string(change_id) })
112112+ hl_cache[change_id] = hl_name
113113+ end
114114+115115+ local hl = hl_cache[change_id]
116116+ local row = i - 1
117117+ local cid_len = #line.change_id
118118+ vim.api.nvim_buf_set_extmark(buf, ns, row, 0, { end_col = cid_len, hl_group = "Comment" })
119119+ vim.api.nvim_buf_set_extmark(buf, ns, row, cid_len + 2, { end_col = -1, hl_group = hl, strict = false })
120120+ end
121121+ end
122122+end
123123+124124+---@param buf integer
125125+---@param lines string[]
126126+---@param scrollbind boolean
127127+local function open_win(buf, lines, scrollbind)
128128+ -- TODO: if there's an existing jjannotate window, reuse it
129129+103130 local longest_line = 0
104131 for _, line in ipairs(lines) do
105132 longest_line = math.max(longest_line, vim.fn.strdisplaywidth(line))
···111138 vim.wo[win].winfixwidth = true
112139113140 ---@type integer|nil
114114- local attached_win
115141 if scrollbind then
116116- attached_win = vim.api.nvim_get_current_win()
142142+ local attached_win = vim.api.nvim_get_current_win()
117143 vim.wo[win].scrollbind = true
118144 vim.wo[attached_win].scrollbind = true
145145+ vim.b[buf].jjannotate_attached_win = attached_win
119146 end
120147121148 local function unscrollbind()
149149+ local attached_win = vim.b[buf].jjannotate_attached_win
122150 if attached_win then
123151 vim.wo[attached_win].scrollbind = false
124152 attached_win = nil
125153 end
126154 end
155155+127156 -- mappings
128157 vim.keymap.set("n", "q", function() vim.api.nvim_win_close(win, true) end, { buffer = buf })
129158···142171 vim.api.nvim_win_close(win, true)
143172 end,
144173 })
174174+175175+ return win
176176+end
177177+178178+---@param parsed jjannotate.ParseResult
179179+---@param scrollbind boolean
180180+local function open_annotate_win(parsed, scrollbind)
181181+ local buf, lines = create_buf(parsed)
182182+ apply_highlights(buf, parsed)
183183+ open_win(buf, lines, scrollbind)
145184end
146185147186--- Runs `jj file annotate {path}`, defaults to the current file if no path is given.
···161200162201 run_annotate(path, function(raw)
163202 local parsed = parse_annotate(raw)
164164- vim.schedule(function() open_win(parsed, path == current_file) end)
203203+ vim.schedule(function() open_annotate_win(parsed, path == current_file) end)
165204 end)
166205end
167206