1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
--
-- 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" }
)
|