summaryrefslogtreecommitdiffstats
path: root/plugin/50-extmarks.lua
blob: 2cb78625ef37333cf800a1b66fa9646afe892aaf (plain)
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" }
)