Skip to content

Instantly share code, notes, and snippets.

@badosu
Last active November 1, 2025 16:13
Show Gist options
  • Select an option

  • Save badosu/da39c03847e6d3dd93f34d0cda08f489 to your computer and use it in GitHub Desktop.

Select an option

Save badosu/da39c03847e6d3dd93f34d0cda08f489 to your computer and use it in GitHub Desktop.
-- 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