Last active
November 1, 2025 16:13
-
-
Save badosu/da39c03847e6d3dd93f34d0cda08f489 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- Vendored from https://github.com/folke/todo-comments.nvim/blob/main/lua/todo-comments/highlight.lua | |
| -- FIX: dddddd | |
| -- TODO: foobar | |
| -- dddddd | |
| local namespace_name = "todo-comments" | |
| local augroup_name = "Todo" | |
| local sign_group_name = "todo-signs" | |
| local M = {} | |
| M.enabled = false | |
| M.bufs = {} | |
| M.wins = {} | |
| M.hl_regex = {} | |
| M.keywords = {} | |
| M.ns = vim.api.nvim_create_namespace(namespace_name) | |
| M.state = {} | |
| M.timer = nil | |
| M.options = { | |
| signs = true, -- show icons in the signs column | |
| sign_priority = 8, -- sign priority | |
| -- keywords recognized as todo comments | |
| keywords = { | |
| FIX = { | |
| icon = " ", -- icon used for the sign, and in search results | |
| color = "error", -- can be a hex color, or a named color (see below) | |
| alt = { "FIXME", "BUG", "FIXIT", "ISSUE" }, -- a set of other keywords that all map to this FIX keywords | |
| -- signs = false, -- configure signs for some keywords individually | |
| }, | |
| TODO = { icon = " ", color = "info" }, | |
| HACK = { icon = " ", color = "warning" }, | |
| WARN = { icon = " ", color = "warning", alt = { "WARNING", "XXX" } }, | |
| PERF = { icon = " ", alt = { "OPTIM", "PERFORMANCE", "OPTIMIZE" } }, | |
| NOTE = { icon = " ", color = "hint", alt = { "INFO" } }, | |
| TEST = { icon = "⏲ ", color = "test", alt = { "TESTING", "PASSED", "FAILED" } }, | |
| }, | |
| gui_style = { | |
| ---@type vim.api.keyset.highlight | |
| fg = {}, -- The gui style to use for the fg highlight group. | |
| ---@type vim.api.keyset.highlight | |
| bg = { bold = true }, -- The gui style to use for the bg highlight group. | |
| }, | |
| -- highlighting of the line containing the todo comment | |
| -- * before: highlights before the keyword (typically comment characters) | |
| -- * keyword: highlights of the keyword | |
| -- * after: highlights after the keyword (todo text) | |
| highlight = { | |
| multiline = true, -- enable multine todo comments | |
| multiline_pattern = "^.", -- lua pattern to match the next multiline from the start of the matched keyword | |
| multiline_context = 10, -- extra lines that will be re-evaluated when changing a line | |
| before = "", -- "fg" or "bg" or empty | |
| keyword = "wide", -- "fg", "bg", "wide" or empty. (wide is the same as bg, but will also highlight surrounding characters) | |
| after = "fg", -- "fg" or "bg" or empty | |
| -- pattern can be a string, or a table of regexes that will be checked | |
| pattern = [[.*<(KEYWORDS)\s*:]], -- pattern or table of patterns, used for highlightng (vim regex) | |
| -- pattern = { [[.*<(KEYWORDS)\s*:]], [[.*\@(KEYWORDS)\s*]] }, -- pattern used for highlightng (vim regex) | |
| comments_only = true, -- uses treesitter to match keywords in comments only | |
| max_line_len = 400, -- ignore lines longer than this | |
| exclude = {}, -- list of file types to exclude highlighting | |
| throttle = 200, | |
| }, | |
| -- list of named colors where we try to extract the guifg from the | |
| -- list of hilight groups or use the hex color if hl not found as a fallback | |
| colors = { | |
| error = { "DiagnosticError", "ErrorMsg", "#DC2626" }, | |
| warning = { "DiagnosticWarn", "WarningMsg", "#FBBF24" }, | |
| info = { "DiagnosticInfo", "#2563EB" }, | |
| hint = { "DiagnosticHint", "#10B981" }, | |
| default = { "Identifier", "#7C3AED" }, | |
| test = { "Identifier", "#FF00FF" }, | |
| }, | |
| } | |
| local function get_hl(name) | |
| local hl = vim.api.nvim_get_hl(0, { name = name }) | |
| if not next(hl) then | |
| return | |
| end | |
| local rethl = {} | |
| for _, key in pairs({ "fg", "bg", "sp" }) do | |
| if hl[key] then | |
| rethl[key] = string.format("#%06x", hl[key]) | |
| end | |
| end | |
| return rethl | |
| end | |
| local function rgb_to_linear(c) | |
| c = c / 255 | |
| return c <= 0.04045 and c / 12.92 or ((c + 0.055) / 1.055) ^ 2.4 | |
| end | |
| local function hex2linear_srgb(hex) | |
| hex = hex:gsub("#", "") | |
| return { | |
| r = rgb_to_linear(tonumber("0x" .. hex:sub(1, 2))), | |
| g = rgb_to_linear(tonumber("0x" .. hex:sub(3, 4))), | |
| b = rgb_to_linear(tonumber("0x" .. hex:sub(5, 6))), | |
| } | |
| end | |
| local function relative_luminance(color) | |
| return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b | |
| end | |
| local function contrast_ratio(c1, c2) | |
| local lum1 = relative_luminance(c1) | |
| local lum2 = relative_luminance(c2) | |
| if lum1 < lum2 then | |
| lum1, lum2 = lum2, lum1 | |
| end | |
| return (lum1 + 0.05) / (lum2 + 0.05) | |
| end | |
| local function maximize_contrast(base, fg1, fg2) | |
| base = hex2linear_srgb(base) | |
| return contrast_ratio(base, hex2linear_srgb(fg1)) > contrast_ratio(base, hex2linear_srgb(fg2)) and fg1 or fg2 | |
| end | |
| -- PERF: fully optimised | |
| -- FIXME: ddddddasdasdasdasdasda | |
| -- PERF: dddd | |
| -- ddddd | |
| -- dddddd | |
| -- ddddddd | |
| -- FIXME: dddddd | |
| -- FIX: ddd | |
| -- HACK: hmmm, this looks a bit funky | |
| -- TODO: What else? | |
| -- NOTE: adding a note | |
| -- | |
| -- FIX: this needs fixing | |
| -- WARNING: ??? | |
| -- FIX: ddddd | |
| -- continuation | |
| -- @TODO foobar | |
| -- @hack foobar | |
| ---@return number? start, number? finish, string? kw | |
| function M.match(str, patterns) | |
| local max_line_len = M.options.highlight.max_line_len | |
| if max_line_len and #str > max_line_len then | |
| return | |
| end | |
| patterns = patterns or M.hl_regex | |
| if not type(patterns) == "table" then | |
| patterns = { patterns } | |
| end | |
| for _, pattern in pairs(patterns) do | |
| local m = vim.fn.matchlist(str, [[\v\C]] .. pattern) | |
| if #m > 1 and m[2] then | |
| local match = m[2] | |
| local kw = m[3] ~= "" and m[3] or m[2] | |
| local start = str:find(match, 1, true) | |
| return start, start + #match, kw | |
| end | |
| end | |
| end | |
| -- This method returns nil if this buf doesn't have a treesitter parser | |
| --- @return boolean? true or false otherwise | |
| function M.is_comment(buf, row, col) | |
| if vim.treesitter.highlighter.active[buf] then | |
| local captures = vim.treesitter.get_captures_at_pos(buf, row, col) | |
| for _, c in ipairs(captures) do | |
| if c.capture == "comment" then | |
| return true | |
| end | |
| end | |
| else | |
| local win = vim.fn.bufwinid(buf) | |
| return win ~= -1 | |
| and vim.api.nvim_win_call(win, function() | |
| for _, i1 in ipairs(vim.fn.synstack(row + 1, col)) do | |
| local i2 = vim.fn.synIDtrans(i1) | |
| local n1 = vim.fn.synIDattr(i1, "name") | |
| local n2 = vim.fn.synIDattr(i2, "name") | |
| if n1 == "Comment" or n2 == "Comment" then | |
| return true | |
| end | |
| end | |
| end) | |
| end | |
| end | |
| function M.redraw(buf, first, last) | |
| first = math.max(first - M.options.highlight.multiline_context, 0) | |
| last = math.min(last + M.options.highlight.multiline_context, vim.api.nvim_buf_line_count(buf)) | |
| M.state[buf] = M.state[buf] or { dirty = {}, comments = {} } | |
| local state = M.state[buf] | |
| for i = first, last do | |
| state.dirty[i] = true | |
| end | |
| if not M.timer then | |
| M.timer = vim.defer_fn(M.update, M.options.highlight.throttle) | |
| end | |
| end | |
| function M.update() | |
| if M.timer then | |
| M.timer:stop() | |
| end | |
| M.timer = nil | |
| for buf, state in pairs(M.state) do | |
| if vim.api.nvim_buf_is_valid(buf) then | |
| if not vim.tbl_isempty(state.dirty) then | |
| local dirty = vim.tbl_keys(state.dirty) | |
| table.sort(dirty) | |
| local i = 1 | |
| while i <= #dirty do | |
| local first = dirty[i] | |
| local last = dirty[i] | |
| while dirty[i + 1] == dirty[i] + 1 do | |
| i = i + 1 | |
| last = dirty[i] | |
| end | |
| M.highlight(buf, first, last) | |
| i = i + 1 | |
| end | |
| state.dirty = {} | |
| end | |
| else | |
| M.state[buf] = nil | |
| end | |
| end | |
| end | |
| local function get_kw_hl(keyword, hl_fg, hl_bg, start, finish) | |
| if keyword == "wide" or keyword == "wide_bg" then | |
| return hl_bg, math.max(start - 1, 0), finish + 1 | |
| elseif keyword == "wide_fg" then | |
| return hl_fg, math.max(start - 1, 0), finish + 1 | |
| elseif keyword == "bg" then | |
| return hl_bg, start, finish | |
| elseif keyword == "fg" then | |
| return hl_fg, start, finish | |
| end | |
| end | |
| -- highlights the range for the given buf | |
| function M.highlight(buf, first, last, _event) | |
| if not vim.api.nvim_buf_is_valid(buf) then | |
| return | |
| end | |
| vim.api.nvim_buf_clear_namespace(buf, M.ns, first, last + 1) | |
| -- clear signs | |
| for _, sign in pairs(vim.fn.sign_getplaced(buf, { group = sign_group_name })[1].signs) do | |
| if sign.lnum - 1 >= first and sign.lnum - 1 <= last then | |
| vim.fn.sign_unplace(sign_group_name, { buffer = buf, id = sign.id }) | |
| end | |
| end | |
| local lines = vim.api.nvim_buf_get_lines(buf, first, last + 1, false) | |
| ---@type {kw: string, start:integer}? | |
| local last_match | |
| for l, line in ipairs(lines) do | |
| local ok, start, finish, kw = pcall(M.match, line) | |
| local lnum = first + l - 1 | |
| if ok and start then | |
| ---@cast kw string | |
| if | |
| M.options.highlight.comments_only | |
| and not M.is_quickfix(buf) | |
| and not M.is_comment(buf, lnum, start - 1) | |
| then | |
| kw = nil | |
| else | |
| last_match = { kw = kw, start = start } | |
| end | |
| end | |
| local is_multiline = false | |
| if not kw and last_match and M.options.highlight.multiline then | |
| if | |
| M.is_comment(buf, lnum, last_match.start) | |
| and line:find(M.options.highlight.multiline_pattern, last_match.start) | |
| then | |
| kw = last_match.kw | |
| start = last_match.start | |
| finish = start | |
| is_multiline = true | |
| else | |
| last_match = nil | |
| end | |
| end | |
| if kw then | |
| kw = M.keywords[kw] or kw | |
| end | |
| local opts = M.options.keywords[kw] | |
| if opts then | |
| start = start - 1 | |
| finish = finish - 1 | |
| local hl_fg = "TodoFg" .. kw | |
| local hl_bg = "TodoBg" .. kw | |
| local hl = M.options.highlight | |
| if not is_multiline then | |
| -- signs | |
| local show_sign = opts.signs == nil and M.options.signs or opts.signs | |
| if show_sign then | |
| vim.fn.sign_place( | |
| 0, | |
| sign_group_name, | |
| "todo-sign-" .. kw, | |
| buf, | |
| { lnum = lnum + 1, priority = M.options.sign_priority } | |
| ) | |
| end | |
| -- before highlights | |
| local before_hl = hl.before == "fg" and hl_fg or (hl.before == "bg" and hl_bg or "") | |
| vim.hl.range(buf, M.ns, before_hl, { lnum, 0 }, { lnum, start }) | |
| -- tag highlights | |
| local kw_hl, kw_start, kw_finish = get_kw_hl(hl.keyword, hl_fg, hl_bg, start, finish) | |
| vim.hl.range(buf, M.ns, kw_hl, { lnum, kw_start }, { lnum, kw_finish }) | |
| end | |
| -- after highlights | |
| local after_hl = hl.after == "fg" and hl_fg or (hl.after == "bg" and hl_bg or "") | |
| vim.hl.range(buf, M.ns, after_hl, { lnum, finish }, { lnum, #line }) | |
| end | |
| end | |
| end | |
| -- highlights the visible range of the window | |
| function M.highlight_win(win, force) | |
| if force ~= true and not M.is_valid_win(win) then | |
| return | |
| end | |
| vim.api.nvim_win_set_hl_ns(win, M.ns) | |
| vim.api.nvim_win_call(win, function() | |
| local buf = vim.api.nvim_win_get_buf(win) | |
| local first = vim.fn.line("w0") - 1 | |
| local last = vim.fn.line("w$") | |
| M.redraw(buf, first, last) | |
| end) | |
| end | |
| function M.is_float(win) | |
| local opts = vim.api.nvim_win_get_config(win) | |
| return opts and opts.relative and opts.relative ~= "" | |
| end | |
| function M.is_valid_win(win) | |
| if not vim.api.nvim_win_is_valid(win) then | |
| return false | |
| end | |
| -- avoid E5108 after pressing q: | |
| if vim.fn.getcmdwintype() ~= "" then | |
| return false | |
| end | |
| -- dont do anything for floating windows | |
| if M.is_float(win) then | |
| return false | |
| end | |
| local buf = vim.api.nvim_win_get_buf(win) | |
| return M.is_valid_buf(buf) | |
| end | |
| function M.is_quickfix(buf) | |
| return vim.api.nvim_get_option_value("buftype", { buf = buf }) == "quickfix" | |
| end | |
| function M.is_valid_buf(buf) | |
| -- Skip special buffers | |
| local buftype = vim.api.nvim_get_option_value("buftype", { buf = buf }) | |
| if buftype ~= "" and buftype ~= "quickfix" then | |
| return false | |
| end | |
| local filetype = vim.api.nvim_get_option_value("filetype", { buf = buf }) | |
| if vim.tbl_contains(M.options.highlight.exclude, filetype) then | |
| return false | |
| end | |
| return true | |
| end | |
| -- will attach to the buf in the window and highlight the active buf if needed | |
| function M.attach(win) | |
| if not M.is_valid_win(win) then | |
| return | |
| end | |
| local buf = vim.api.nvim_win_get_buf(win) | |
| if not M.bufs[buf] then | |
| vim.api.nvim_buf_attach(buf, false, { | |
| on_lines = function(_event, _buf, _tick, first, _last, last_new) | |
| if not M.enabled then | |
| return true | |
| end | |
| -- detach from this buffer in case we no longer want it | |
| if not M.is_valid_buf(buf) then | |
| return true | |
| end | |
| M.redraw(buf, first, last_new) | |
| end, | |
| on_detach = function() | |
| M.state[buf] = nil | |
| M.bufs[buf] = nil | |
| end, | |
| }) | |
| local highlighter = require("vim.treesitter.highlighter") | |
| local hl = highlighter.active[buf] | |
| if hl then | |
| -- also listen to TS changes so we can properly update the buffer based on is_comment | |
| hl.tree:register_cbs({ | |
| on_bytes = function(_, _, row) | |
| M.redraw(buf, row, row + 1) | |
| end, | |
| on_changedtree = function(changes) | |
| for _, ch in ipairs(changes or {}) do | |
| M.redraw(buf, ch[1], ch[3] + 1) | |
| end | |
| end, | |
| }) | |
| end | |
| M.bufs[buf] = true | |
| M.highlight_win(win) | |
| M.wins[win] = true | |
| elseif not M.wins[win] then | |
| M.highlight_win(win) | |
| M.wins[win] = true | |
| end | |
| end | |
| function M.stop() | |
| M.enabled = false | |
| vim.api.nvim_del_augroup_by_name(augroup_name) | |
| M.wins = {} | |
| vim.fn.sign_unplace(sign_group_name) | |
| for buf, _ in pairs(M.bufs) do | |
| if vim.api.nvim_buf_is_valid(buf) then | |
| pcall(vim.api.nvim_buf_clear_namespace, buf, M.ns, 0, -1) | |
| end | |
| end | |
| M.bufs = {} | |
| end | |
| function M.start() | |
| if M.enabled then | |
| M.stop() | |
| end | |
| M.enabled = true | |
| local augroup = vim.api.nvim_create_augroup(augroup_name, { clear = true }) | |
| vim.api.nvim_create_autocmd({ "WinNew", "BufWinEnter" }, { | |
| group = augroup, | |
| pattern = "*", | |
| callback = function() | |
| M.attach(vim.api.nvim_get_current_win()) | |
| end, | |
| }) | |
| vim.api.nvim_create_autocmd("WinScrolled", { | |
| group = augroup, | |
| pattern = "*", | |
| callback = function() | |
| M.highlight_win(vim.api.nvim_get_current_win()) | |
| end, | |
| }) | |
| vim.api.nvim_create_autocmd("ColorScheme", { | |
| group = augroup, | |
| pattern = "*", | |
| callback = function() | |
| vim.defer_fn(M.colors, 10) | |
| end, | |
| }) | |
| -- attach to all bufs in visible windows | |
| for _, win in pairs(vim.api.nvim_list_wins()) do | |
| M.attach(win) | |
| end | |
| end | |
| function M.setup() | |
| for kw, opts in pairs(M.options.keywords) do | |
| M.keywords[kw] = kw | |
| for _, alt in pairs(opts.alt or {}) do | |
| M.keywords[alt] = kw | |
| end | |
| vim.fn.sign_define("todo-sign-" .. kw, { | |
| text = opts.icon, | |
| texthl = "TodoSign" .. kw, | |
| }) | |
| end | |
| local kws = vim.tbl_keys(M.keywords) | |
| table.sort(kws, function(a, b) | |
| return #b < #a | |
| end) | |
| local tags = table.concat(kws, "|") | |
| local hl_patterns = M.options.highlight.pattern | |
| ---@diagnostic disable-next-line: cast-local-type | |
| hl_patterns = type(hl_patterns) == "table" and hl_patterns or { hl_patterns } | |
| for _, p in pairs(hl_patterns) do | |
| p = p:gsub("KEYWORDS", tags) | |
| table.insert(M.hl_regex, p) | |
| end | |
| M.colors() | |
| end | |
| function M.colors() | |
| local default_dark = "#000000" | |
| local default_light = "#FFFFFF" | |
| local normal = get_hl("Normal") | |
| local normal_fg = normal.fg | |
| local normal_bg = normal.bg | |
| if not normal_fg and not normal_bg then | |
| normal_fg = default_light | |
| normal_bg = default_dark | |
| elseif not normal_fg then | |
| normal_fg = maximize_contrast(normal_bg, default_dark, default_light) | |
| elseif not normal_bg then | |
| normal_bg = maximize_contrast(normal_fg, default_dark, default_light) | |
| end | |
| for kw, opts in pairs(M.options.keywords) do | |
| vim.fn.sign_define("todo-sign-" .. kw, { | |
| text = opts.icon, | |
| texthl = "TodoSign" .. kw, | |
| }) | |
| local kw_color = opts.color or "default" | |
| local hex | |
| if kw_color:sub(1, 1) == "#" then | |
| hex = kw_color | |
| else | |
| local colors = M.options.colors[kw_color] | |
| colors = type(colors) == "string" and { colors } or colors | |
| for _, color in pairs(colors) do | |
| if color:sub(1, 1) == "#" then | |
| hex = color | |
| break | |
| end | |
| local c = get_hl(color) | |
| if c and c.fg then | |
| hex = c.fg | |
| break | |
| end | |
| end | |
| end | |
| if not hex then | |
| error("Todo: no color for " .. kw) | |
| end | |
| local sign_hl = get_hl("SignColumn") | |
| local sign_bg = (sign_hl and sign_hl.bg) and sign_hl.fg or "NONE" | |
| local fg = maximize_contrast(hex, normal_fg, normal_bg) | |
| local fg_gui = M.options.gui_style.fg | |
| local bg_gui = M.options.gui_style.bg | |
| vim.api.nvim_set_hl(M.ns, "TodoBg" .. kw, vim.tbl_extend("keep", bg_gui, { fg = fg, bg = hex })) | |
| vim.api.nvim_set_hl(M.ns, "TodoFg" .. kw, vim.tbl_extend("keep", fg_gui, { fg = hex })) | |
| vim.api.nvim_set_hl(M.ns, "TodoSign" .. kw, { fg = hex, bg = sign_bg }) | |
| end | |
| end | |
| M.setup() | |
| M.start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment