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
|
--[[ 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 of buffer `buf` synchronously, returning whether a parser exists.
Parses the 0-based, end-exclusive line range `first`:`last`, including injections. ]]
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 0-based 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. ]]
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 valid "TODO" positions in the line range `first`:`last` of `buf`.
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" }
)
|