-- -- 50-extmarks.lua -- -- * Marks "TODO" strings through the dotfiles.extmark engine. -- * Only "TODO" in comments is marked (in markdown, "TODO" outside code), classified from the -- treesitter tree; with no parser for the filetype, every "TODO" is marked. -- * Multi-line patterns are not supported. -- -- User commands: -- ExtmarkToggle: toggle the extended-marks highlight namespace -- local extmark = require("dotfiles.extmark") -- Parses the treesitter tree synchronously, including injections, in the 0-based, end-exclusive -- line range `first`:`last` of buffer `buf`. --- @return boolean parser_exists local function parse_tree(buf, first, last) local parser = vim.treesitter.get_parser(buf) if parser then parser:parse({ first, last }) end return parser ~= nil end -- Returns a predicate telling whether a "TODO" string should be marked at a given position. -- In markdown buffers, positions inside code are invalid, otherwise positions outside comments are -- invalid. All positions are valid if there is no parser for the buffer filetype. -- Only the line range `first`:`last` is parsed; the predicate must not be called outside it. --- @param buf number --- @return fun(row: number, col: number): boolean predicate taking a 0-based position local function todo_filter(buf, first, last) local has_parser = parse_tree(buf, first, last) return function(row, col) assert(first <= row and row < last, "Position outside the parsed line range") if not has_parser then return true end local block = vim.treesitter.get_node({ bufnr = buf, pos = { row, col }, }) assert(block, "Expected node to be truthy, is the tree parsed?") if vim.bo[buf].filetype == "markdown" then if string.match(block:type(), "code") then return false end local inline = vim.treesitter.get_node({ bufnr = buf, ignore_injections = false, pos = { row, col }, }) assert(inline, "Expected node to be truthy, is the tree parsed?") return inline:type() ~= "code_span" else return string.match(block:type(), "comment") ~= nil end end end -- Returns an iterator over the positions of valid "TODO" strings in buffer `buf` in the line range -- `first`:`last`. Each item is a table with 0-based fields: row `y`, start column `x1` (inclusive) -- and end column `x2` (exclusive). See `todo_filter()` for what makes a position valid. local function find_todo(buf, first, last) local lines = vim.api.nvim_buf_get_lines(buf, first, last, false) local should_mark = todo_filter(buf, first, first + #lines) local x1, x2, y = nil, nil, 1 return function() while y <= #lines do x1, x2 = string.find(lines[y], "TODO", (x2 or 0) + 1) if x1 and x2 then local row = y + first - 1 if should_mark(row, x1 - 1) then return { y = row, x1 = x1 - 1, x2 = x2 } end else x1, x2, y = 0, 0, y + 1 end end return nil end end extmark.register("dotfiles.Todo", { link = "Todo" }, find_todo) ---------------------------------------------------------------------------------------------------- vim.api.nvim_create_user_command( "ExtmarkToggle", extmark.toggle, { desc = "Toggle extended marks" } )