···1111 h = bit.bxor(h, string.byte(str, i))
1212 h = (h * 16777619) % 2 ^ 32
1313 end
1414- -- mix some bits so similar strings produce very different hashes
1515- h = bit.bxor(h, bit.rshift(h, 16))
1616- h = (h * 0x45d9f3b) % 2 ^ 32
1717- h = bit.bxor(h, bit.rshift(h, 16))
1814 return h
1915end
2016···48444945--- Returns a stable "golden ratio" color from a given string, in hex format.
5046---@param str string
4747+---@param sat number
4848+---@param light number
5149---@return string
5252-function M.stable_from_string(str)
5050+function M.stable_from_string(str, sat, light)
5351 local hue = ((hash(str) * GOLDEN_RATIO_CONJUGATE) % 1) * 360
5454- return hsl_to_hex(hue, 0.5, 0.7)
5252+ return hsl_to_hex(hue, sat, light)
5553end
56545755return M
+13-1
lua/jjannotate/jj.lua
···1616 local lines = {} ---@type jjannotate.Line[]
1717 local longest_timestamp = 0
1818 local last_change_id ---@type string | nil
1919+ local row_index = {} ---@type jjannotate.Line[]
2020+ local change_id_index = {} ---@type table<string, jjannotate.Line[]>
19212022 for i, line in ipairs(vim.split(raw, "\n")) do
2123 if #line > 0 then
···30323133 if parsed.change_id == last_change_id then
3234 lines[#lines].range._end = row
3535+ row_index[row] = lines[#lines]
3336 else
3437 table.insert(lines, parsed)
3538 longest_timestamp = math.max(longest_timestamp, #parsed.timestamp)
3939+ row_index[row] = parsed
4040+ local cid = parsed.change_id
4141+ if not change_id_index[cid] then change_id_index[cid] = {} end
4242+ table.insert(change_id_index[cid], parsed)
3643 end
37443845 last_change_id = parsed.change_id
3946 end
4047 end
41484242- return { lines = lines, longest_timestamp = longest_timestamp }
4949+ return {
5050+ lines = lines,
5151+ longest_timestamp = longest_timestamp,
5252+ row_index = row_index,
5353+ change_id_index = change_id_index,
5454+ }
4355end
44564557---@param path string
+81-24
lua/jjannotate/view.lua
···11local color = require("jjannotate.color")
2233---@param path string
44----@return jjannotate.BufWin | nil
44+---@return { win: integer, buf: integer} | nil
55local function find_visible_path(path)
66 local target = vim.fn.fnamemodify(path, ":p")
77 for _, win in ipairs(vim.api.nvim_list_wins()) do
···1313---@class jjannotate.View
1414---@field win integer?
1515---@field buf integer?
1616----@field original_win integer?
1616+---@field original { win: integer, buf: integer }?
1717---@field original_opts table<string, any>
1818+---@field parsed jjannotate.ParseResult?
1819---@field ns integer
19202020----@class jjannotate.BufWin
2121----@field win integer
2222----@field buf integer
2323-2421local View = {}
2522View.__index = View
2623···3128 wrap = false,
3229}
33303434-local hl_cache = {}
3131+local hl_cache = {} ---@type table<string, { full: string, dim: string }>
35323633function View:new()
3734 return setmetatable({
3835 win = nil,
3936 buf = nil,
4040- original_win = nil,
3737+ original = nil,
4138 original_opts = {},
3939+ parsed = nil,
4240 ns = vim.api.nvim_create_namespace("jjannotate"),
4141+ ns_hl = vim.api.nvim_create_namespace("jjannotate.hl"),
4242+ augroup = vim.api.nvim_create_augroup("jjannotate", { clear = true }),
4343 }, self)
4444end
45454646+---@return boolean
4647function View:is_open() return self.win and vim.api.nvim_win_is_valid(self.win) end
47484849---@return boolean whether the view closed
···5455 return false
5556end
56575858+---@param path string
5959+---@param parsed jjannotate.ParseResult
6060+---@param opts jjannotate.Opts
5761function View:open(path, parsed, opts)
6262+ self.parsed = parsed
6363+5864 -- create buffer
5965 self.buf = self.buf or vim.api.nvim_create_buf(false, true)
6066 vim.bo[self.buf].modifiable = true
···8187 local change_id = line.change_id
82888389 if not hl_cache[change_id] then
8484- local hl_name = "JJAnnotate" .. change_id
8585- vim.api.nvim_set_hl(0, hl_name, { fg = color.stable_from_string(change_id) })
8686- hl_cache[change_id] = hl_name
9090+ local full = "JJAnnotate" .. change_id
9191+ local dim = "JJAnnotateDim" .. change_id
9292+ vim.api.nvim_set_hl(0, full, { fg = color.stable_from_string(change_id, 0.6, 0.7) })
9393+ vim.api.nvim_set_hl(0, dim, { bg = color.stable_from_string(change_id, 0.3, 0.125) })
9494+ hl_cache[change_id] = { full = full, dim = dim }
8795 end
88968989- local hl = hl_cache[change_id]
9797+ local hl = hl_cache[change_id].full
9098 for row = line.range.start, line.range._end do
9199 vim.api.nvim_buf_set_extmark(
92100 self.buf,
···113121 vim.api.nvim_win_set_width(self.win, longest_line + textoff + 3)
114122 vim.wo[self.win].winfixwidth = true
115123116116- local original = find_visible_path(path)
117117- if original then
118118- self.original_win = original.win
124124+ self.original = find_visible_path(path)
125125+ if self.original then
119126 for opt, value in pairs(enabled_opts) do
120120- self.original_opts[opt] = vim.api.nvim_get_option_value(opt, { win = original.win })
121121- vim.api.nvim_set_option_value(opt, value, { win = original.win })
127127+ self.original_opts[opt] = vim.api.nvim_get_option_value(opt, { win = self.original.win })
128128+ vim.api.nvim_set_option_value(opt, value, { win = self.original.win })
122129 vim.api.nvim_set_option_value(opt, value, { win = self.win })
123130 end
124131 end
···134141135142 self:_autocmds()
136143137137- if original then vim.api.nvim_win_set_cursor(self.win, { vim.api.nvim_win_get_cursor(original.win)[1], 0 }) end
144144+ if self.original then
145145+ vim.api.nvim_win_set_cursor(self.win, { vim.api.nvim_win_get_cursor(self.original.win)[1], 0 })
146146+ end
138147 vim.api.nvim_set_current_win(self.win)
139148 vim.cmd.syncbind()
140149end
141150142151function View:_autocmds()
152152+ local function cursor_moved()
153153+ local row = vim.api.nvim_win_get_cursor(self.win)[1] - 1
154154+ local change_id = self.parsed.row_index[row].change_id
155155+ local lines = self.parsed.change_id_index[change_id]
156156+ local dim_hl = hl_cache[change_id].dim
157157+158158+ vim.api.nvim_buf_clear_namespace(self.buf, self.ns_hl, 0, -1)
159159+ vim.api.nvim_buf_clear_namespace(self.original.buf, self.ns_hl, 0, -1)
160160+161161+ for _, line in ipairs(lines) do
162162+ vim.api.nvim_buf_set_extmark(self.buf, self.ns_hl, line.range.start, 0, {
163163+ end_row = line.range._end + 1,
164164+ hl_group = dim_hl,
165165+ hl_eol = true,
166166+ strict = false,
167167+ })
168168+ vim.api.nvim_buf_set_extmark(self.original.buf, self.ns_hl, line.range.start, 0, {
169169+ end_row = line.range._end + 1,
170170+ hl_group = dim_hl,
171171+ hl_eol = true,
172172+ strict = false,
173173+ })
174174+ end
175175+ end
176176+177177+ vim.api.nvim_create_autocmd("CursorMoved", {
178178+ group = self.augroup,
179179+ buffer = self.buf,
180180+ callback = vim.schedule_wrap(cursor_moved),
181181+ })
182182+183183+ if self.original then
184184+ vim.api.nvim_create_autocmd("CursorMoved", {
185185+ group = self.augroup,
186186+ buffer = self.original.buf,
187187+ callback = vim.schedule_wrap(cursor_moved),
188188+ })
189189+ end
190190+143191 vim.api.nvim_create_autocmd({ "BufHidden", "BufUnload" }, {
192192+ group = self.augroup,
144193 once = true,
145194 buffer = self.buf,
146146- callback = vim.schedule_wrap(function()
147147- if self.original_win and vim.api.nvim_win_is_valid(self.original_win) then
148148- for opt, value in pairs(self.original_opts) do
149149- vim.api.nvim_set_option_value(opt, value, { win = self.original_win })
195195+ callback = function()
196196+ vim.api.nvim_clear_autocmds({ group = self.augroup })
197197+198198+ vim.api.nvim_buf_clear_namespace(self.buf, self.ns_hl, 0, -1)
199199+200200+ if self.original then
201201+ vim.api.nvim_buf_clear_namespace(self.original.buf, self.ns_hl, 0, -1)
202202+ if vim.api.nvim_win_is_valid(self.original.win) then
203203+ for opt, value in pairs(self.original_opts) do
204204+ vim.api.nvim_set_option_value(opt, value, { win = self.original.win })
205205+ end
150206 end
151207 end
208208+152209 self.win = nil
153210 self.buf = nil
154154- self.original_win = nil
211211+ self.original = nil
155212 self.original_opts = {}
156156- end),
213213+ end,
157214 })
158215end
159216