From 564b141a6ff2da140ef5d98fc2f419721f4aedbc Mon Sep 17 00:00:00 2001 From: Ron Shavit Date: Wed, 27 May 2026 01:21:56 +0300 Subject: [PATCH] highlight lines related to cursor's change id Signed-off-by: Ron Shavit --- lua/jjannotate.lua | 2 + lua/jjannotate/color.lua | 10 ++-- lua/jjannotate/jj.lua | 14 +++++- lua/jjannotate/view.lua | 105 ++++++++++++++++++++++++++++++--------- 4 files changed, 100 insertions(+), 31 deletions(-) diff --git a/lua/jjannotate.lua b/lua/jjannotate.lua index e8b1ea2..0929f29 100644 --- a/lua/jjannotate.lua +++ b/lua/jjannotate.lua @@ -14,6 +14,8 @@ local defaults = { ---@class jjannotate.ParseResult ---@field lines jjannotate.Line[] ---@field longest_timestamp integer +---@field row_index jjannotate.Line[] +---@field change_id_index table ---@class jjannotate.Range ---@field start integer 0-indexed inclusive diff --git a/lua/jjannotate/color.lua b/lua/jjannotate/color.lua index 6efdb14..8331f43 100644 --- a/lua/jjannotate/color.lua +++ b/lua/jjannotate/color.lua @@ -11,10 +11,6 @@ local function hash(str) h = bit.bxor(h, string.byte(str, i)) h = (h * 16777619) % 2 ^ 32 end - -- mix some bits so similar strings produce very different hashes - h = bit.bxor(h, bit.rshift(h, 16)) - h = (h * 0x45d9f3b) % 2 ^ 32 - h = bit.bxor(h, bit.rshift(h, 16)) return h end @@ -48,10 +44,12 @@ end --- Returns a stable "golden ratio" color from a given string, in hex format. ---@param str string +---@param sat number +---@param light number ---@return string -function M.stable_from_string(str) +function M.stable_from_string(str, sat, light) local hue = ((hash(str) * GOLDEN_RATIO_CONJUGATE) % 1) * 360 - return hsl_to_hex(hue, 0.5, 0.7) + return hsl_to_hex(hue, sat, light) end return M diff --git a/lua/jjannotate/jj.lua b/lua/jjannotate/jj.lua index 1cb0db5..0d67609 100644 --- a/lua/jjannotate/jj.lua +++ b/lua/jjannotate/jj.lua @@ -16,6 +16,8 @@ local function parse_annotate(raw) local lines = {} ---@type jjannotate.Line[] local longest_timestamp = 0 local last_change_id ---@type string | nil + local row_index = {} ---@type jjannotate.Line[] + local change_id_index = {} ---@type table for i, line in ipairs(vim.split(raw, "\n")) do if #line > 0 then @@ -30,16 +32,26 @@ local function parse_annotate(raw) if parsed.change_id == last_change_id then lines[#lines].range._end = row + row_index[row] = lines[#lines] else table.insert(lines, parsed) longest_timestamp = math.max(longest_timestamp, #parsed.timestamp) + row_index[row] = parsed + local cid = parsed.change_id + if not change_id_index[cid] then change_id_index[cid] = {} end + table.insert(change_id_index[cid], parsed) end last_change_id = parsed.change_id end end - return { lines = lines, longest_timestamp = longest_timestamp } + return { + lines = lines, + longest_timestamp = longest_timestamp, + row_index = row_index, + change_id_index = change_id_index, + } end ---@param path string diff --git a/lua/jjannotate/view.lua b/lua/jjannotate/view.lua index 66b47d6..5c4f534 100644 --- a/lua/jjannotate/view.lua +++ b/lua/jjannotate/view.lua @@ -1,7 +1,7 @@ local color = require("jjannotate.color") ---@param path string ----@return jjannotate.BufWin | nil +---@return { win: integer, buf: integer} | nil local function find_visible_path(path) local target = vim.fn.fnamemodify(path, ":p") for _, win in ipairs(vim.api.nvim_list_wins()) do @@ -13,14 +13,11 @@ end ---@class jjannotate.View ---@field win integer? ---@field buf integer? ----@field original_win integer? +---@field original { win: integer, buf: integer }? ---@field original_opts table +---@field parsed jjannotate.ParseResult? ---@field ns integer ----@class jjannotate.BufWin ----@field win integer ----@field buf integer - local View = {} View.__index = View @@ -31,18 +28,22 @@ local enabled_opts = { wrap = false, } -local hl_cache = {} +local hl_cache = {} ---@type table function View:new() return setmetatable({ win = nil, buf = nil, - original_win = nil, + original = nil, original_opts = {}, + parsed = nil, ns = vim.api.nvim_create_namespace("jjannotate"), + ns_hl = vim.api.nvim_create_namespace("jjannotate.hl"), + augroup = vim.api.nvim_create_augroup("jjannotate", { clear = true }), }, self) end +---@return boolean function View:is_open() return self.win and vim.api.nvim_win_is_valid(self.win) end ---@return boolean whether the view closed @@ -54,7 +55,12 @@ function View:close() return false end +---@param path string +---@param parsed jjannotate.ParseResult +---@param opts jjannotate.Opts function View:open(path, parsed, opts) + self.parsed = parsed + -- create buffer self.buf = self.buf or vim.api.nvim_create_buf(false, true) vim.bo[self.buf].modifiable = true @@ -81,12 +87,14 @@ function View:open(path, parsed, opts) local change_id = line.change_id if not hl_cache[change_id] then - local hl_name = "JJAnnotate" .. change_id - vim.api.nvim_set_hl(0, hl_name, { fg = color.stable_from_string(change_id) }) - hl_cache[change_id] = hl_name + local full = "JJAnnotate" .. change_id + local dim = "JJAnnotateDim" .. change_id + vim.api.nvim_set_hl(0, full, { fg = color.stable_from_string(change_id, 0.6, 0.7) }) + vim.api.nvim_set_hl(0, dim, { bg = color.stable_from_string(change_id, 0.3, 0.125) }) + hl_cache[change_id] = { full = full, dim = dim } end - local hl = hl_cache[change_id] + local hl = hl_cache[change_id].full for row = line.range.start, line.range._end do vim.api.nvim_buf_set_extmark( self.buf, @@ -113,12 +121,11 @@ function View:open(path, parsed, opts) vim.api.nvim_win_set_width(self.win, longest_line + textoff + 3) vim.wo[self.win].winfixwidth = true - local original = find_visible_path(path) - if original then - self.original_win = original.win + self.original = find_visible_path(path) + if self.original then for opt, value in pairs(enabled_opts) do - self.original_opts[opt] = vim.api.nvim_get_option_value(opt, { win = original.win }) - vim.api.nvim_set_option_value(opt, value, { win = original.win }) + self.original_opts[opt] = vim.api.nvim_get_option_value(opt, { win = self.original.win }) + vim.api.nvim_set_option_value(opt, value, { win = self.original.win }) vim.api.nvim_set_option_value(opt, value, { win = self.win }) end end @@ -134,26 +141,76 @@ function View:open(path, parsed, opts) self:_autocmds() - if original then vim.api.nvim_win_set_cursor(self.win, { vim.api.nvim_win_get_cursor(original.win)[1], 0 }) end + if self.original then + vim.api.nvim_win_set_cursor(self.win, { vim.api.nvim_win_get_cursor(self.original.win)[1], 0 }) + end vim.api.nvim_set_current_win(self.win) vim.cmd.syncbind() end function View:_autocmds() + local function cursor_moved() + local row = vim.api.nvim_win_get_cursor(self.win)[1] - 1 + local change_id = self.parsed.row_index[row].change_id + local lines = self.parsed.change_id_index[change_id] + local dim_hl = hl_cache[change_id].dim + + vim.api.nvim_buf_clear_namespace(self.buf, self.ns_hl, 0, -1) + vim.api.nvim_buf_clear_namespace(self.original.buf, self.ns_hl, 0, -1) + + for _, line in ipairs(lines) do + vim.api.nvim_buf_set_extmark(self.buf, self.ns_hl, line.range.start, 0, { + end_row = line.range._end + 1, + hl_group = dim_hl, + hl_eol = true, + strict = false, + }) + vim.api.nvim_buf_set_extmark(self.original.buf, self.ns_hl, line.range.start, 0, { + end_row = line.range._end + 1, + hl_group = dim_hl, + hl_eol = true, + strict = false, + }) + end + end + + vim.api.nvim_create_autocmd("CursorMoved", { + group = self.augroup, + buffer = self.buf, + callback = vim.schedule_wrap(cursor_moved), + }) + + if self.original then + vim.api.nvim_create_autocmd("CursorMoved", { + group = self.augroup, + buffer = self.original.buf, + callback = vim.schedule_wrap(cursor_moved), + }) + end + vim.api.nvim_create_autocmd({ "BufHidden", "BufUnload" }, { + group = self.augroup, once = true, buffer = self.buf, - callback = vim.schedule_wrap(function() - if self.original_win and vim.api.nvim_win_is_valid(self.original_win) then - for opt, value in pairs(self.original_opts) do - vim.api.nvim_set_option_value(opt, value, { win = self.original_win }) + callback = function() + vim.api.nvim_clear_autocmds({ group = self.augroup }) + + vim.api.nvim_buf_clear_namespace(self.buf, self.ns_hl, 0, -1) + + if self.original then + vim.api.nvim_buf_clear_namespace(self.original.buf, self.ns_hl, 0, -1) + if vim.api.nvim_win_is_valid(self.original.win) then + for opt, value in pairs(self.original_opts) do + vim.api.nvim_set_option_value(opt, value, { win = self.original.win }) + end end end + self.win = nil self.buf = nil - self.original_win = nil + self.original = nil self.original_opts = {} - end), + end, }) end -- 2.51.2