aboutsummaryrefslogtreecommitdiffstats
path: root/plugin/50-extmarks.lua
blob: 2b08472ba29529d2882a6f09abc2b6baf1c39b96 (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
--[[ 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" }
)