jujutsu file annotations for neovim (like blame)
4

Configure Feed

Select the types of activity you want to include in your feed.

apply highlights to lines

Signed-off-by: Ron Shavit <me@ronshavit.com>

Ron Shavit (May 26, 2026, 10:03 PM +0300) 0ca80366 45ff079a

+108 -17
+52
lua/color.lua
··· 1 + local M = {} 2 + 3 + local GOLDEN_RATIO_CONJUGATE = 0.618033988749895 4 + 5 + ---@param str string 6 + ---@return number 7 + local function hash(str) 8 + local h = 5381 9 + for i = 1, #str do 10 + h = (bit.lshift(h, 5) + h) + string.byte(str, i) 11 + h = h % 2 ^ 32 12 + end 13 + return h 14 + end 15 + 16 + ---@param hue number 17 + ---@param sat number 18 + ---@param light number 19 + ---@return string 20 + local function hsl_to_hex(hue, sat, light) 21 + local h = hue / 60 22 + local c = (1 - math.abs(2 * light - 1)) * sat 23 + local x = c * (1 - math.abs(h % 2 - 1)) 24 + local m = light - c / 2 25 + 26 + local r, g, b 27 + if h < 1 then 28 + r, g, b = c, x, 0 29 + elseif h < 2 then 30 + r, g, b = x, c, 0 31 + elseif h < 3 then 32 + r, g, b = 0, c, x 33 + elseif h < 4 then 34 + r, g, b = 0, x, c 35 + elseif h < 5 then 36 + r, g, b = x, 0, c 37 + else 38 + r, g, b = c, 0, x 39 + end 40 + 41 + return string.format("#%02x%02x%02x", math.floor((r + m) * 255), math.floor((g + m) * 255), math.floor((b + m) * 255)) 42 + end 43 + 44 + --- Returns a stable "golden ratio" color from a given string, in hex format. 45 + ---@param str string 46 + ---@return string 47 + function M.stable_from_string(str) 48 + local hue = ((hash(str) * GOLDEN_RATIO_CONJUGATE) % 1) * 360 49 + return hsl_to_hex(hue, 0.5, 0.7) 50 + end 51 + 52 + return M
+56 -17
lua/jjannotate.lua
··· 1 + local ns = vim.api.nvim_create_namespace("jjannotate") 2 + local color = require("color") 1 3 local notify = vim.schedule_wrap(vim.notify) 2 4 3 5 ---@class jjannotate.ParseResult ··· 63 65 return { lines = lines, longest_timestamp = longest_timestamp } 64 66 end 65 67 66 - ---@param line jjannotate.Line 68 + ---@param line jjannotate.Line | nil 67 69 ---@param timestamp_len integer 68 70 ---@return string 69 71 local function render_line(line, timestamp_len) 72 + if line == nil then return "Untracked" end 70 73 return string.format("%s %-" .. timestamp_len .. "s %s", line.change_id, line.timestamp, line.author) 71 74 end 72 75 73 76 ---@param parsed jjannotate.ParseResult 74 - ---@param scrollbind boolean 75 - local function open_win(parsed, scrollbind) 77 + ---@return integer, string[] 78 + local function create_buf(parsed) 76 79 -- TODO: if there's an existing jjannotate buffer, reuse it 77 - -- TODO: if there's an existing jjannotate window, reuse it 78 80 79 - -- setup buffer 80 81 local buf = vim.api.nvim_create_buf(false, true) 81 82 vim.bo[buf].buftype = "nofile" 82 83 vim.bo[buf].filetype = "jjannotate" 83 84 84 - -- render lines 85 85 local lines = {} 86 86 for _, line in ipairs(parsed.lines) do 87 - if line then 88 - table.insert(lines, render_line(line, parsed.longest_timestamp)) 89 - else 90 - table.insert(lines, "Untracked") 91 - end 87 + table.insert(lines, render_line(line, parsed.longest_timestamp)) 92 88 end 93 - 94 - -- print(vim.inspect(lines)) 95 89 96 90 vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) 97 91 ··· 99 93 vim.bo[buf].modifiable = false 100 94 vim.bo[buf].readonly = true 101 95 102 - -- setup window 96 + return buf, lines 97 + end 98 + 99 + ---@param buf integer 100 + ---@param parsed jjannotate.ParseResult 101 + local function apply_highlights(buf, parsed) 102 + local hl_cache = {} 103 + local hl_idx = 0 104 + 105 + for i, line in ipairs(parsed.lines) do 106 + if line then 107 + local change_id = line.change_id 108 + if not hl_cache[change_id] then 109 + hl_idx = hl_idx + 1 110 + local hl_name = "JJAnnotate" .. hl_idx 111 + vim.api.nvim_set_hl(0, hl_name, { fg = color.stable_from_string(change_id) }) 112 + hl_cache[change_id] = hl_name 113 + end 114 + 115 + local hl = hl_cache[change_id] 116 + local row = i - 1 117 + local cid_len = #line.change_id 118 + vim.api.nvim_buf_set_extmark(buf, ns, row, 0, { end_col = cid_len, hl_group = "Comment" }) 119 + vim.api.nvim_buf_set_extmark(buf, ns, row, cid_len + 2, { end_col = -1, hl_group = hl, strict = false }) 120 + end 121 + end 122 + end 123 + 124 + ---@param buf integer 125 + ---@param lines string[] 126 + ---@param scrollbind boolean 127 + local function open_win(buf, lines, scrollbind) 128 + -- TODO: if there's an existing jjannotate window, reuse it 129 + 103 130 local longest_line = 0 104 131 for _, line in ipairs(lines) do 105 132 longest_line = math.max(longest_line, vim.fn.strdisplaywidth(line)) ··· 111 138 vim.wo[win].winfixwidth = true 112 139 113 140 ---@type integer|nil 114 - local attached_win 115 141 if scrollbind then 116 - attached_win = vim.api.nvim_get_current_win() 142 + local attached_win = vim.api.nvim_get_current_win() 117 143 vim.wo[win].scrollbind = true 118 144 vim.wo[attached_win].scrollbind = true 145 + vim.b[buf].jjannotate_attached_win = attached_win 119 146 end 120 147 121 148 local function unscrollbind() 149 + local attached_win = vim.b[buf].jjannotate_attached_win 122 150 if attached_win then 123 151 vim.wo[attached_win].scrollbind = false 124 152 attached_win = nil 125 153 end 126 154 end 155 + 127 156 -- mappings 128 157 vim.keymap.set("n", "q", function() vim.api.nvim_win_close(win, true) end, { buffer = buf }) 129 158 ··· 142 171 vim.api.nvim_win_close(win, true) 143 172 end, 144 173 }) 174 + 175 + return win 176 + end 177 + 178 + ---@param parsed jjannotate.ParseResult 179 + ---@param scrollbind boolean 180 + local function open_annotate_win(parsed, scrollbind) 181 + local buf, lines = create_buf(parsed) 182 + apply_highlights(buf, parsed) 183 + open_win(buf, lines, scrollbind) 145 184 end 146 185 147 186 --- Runs `jj file annotate {path}`, defaults to the current file if no path is given. ··· 161 200 162 201 run_annotate(path, function(raw) 163 202 local parsed = parse_annotate(raw) 164 - vim.schedule(function() open_win(parsed, path == current_file) end) 203 + vim.schedule(function() open_annotate_win(parsed, path == current_file) end) 165 204 end) 166 205 end 167 206