summaryrefslogtreecommitdiffstats
path: root/lua/dotfiles/extmark.lua
blob: ddab83bbff23432cd2316c08f71d4700044983bb (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
93
94
95
96
97
98
99
100
101
102
103
104
105
-- Extmark engine. Maintains a global highlight namespace of extended marks: features register a
-- highlight group plus a finder that locates the positions to mark, and the engine marks every
-- buffer on 'FileType' and keeps the marks in sync as the buffer changes. The whole namespace can
-- be toggled on and off. Marks are registered in plugin/50-extmarks.lua.
--
-- Marks follow the theme on their own: register highlight groups that link to a built-in group
-- (e.g. `Todo`), so a colorscheme / 'background' change re-colors them with no extra wiring.
--
-- Multi-line patterns are not supported.
--
-- Global variables:
--   vim.g.hl_ns — the active global highlight namespace (the engine's namespace, or 0 when off)
--
-- API:
--   register(hl_group, hl_def, find) — define `hl_group` (with the highlight `hl_def`) in the
--                                      engine's namespace and mark the positions yielded by
--                                      `find(buf, first, last)` with it. `find` returns an iterator
--                                      of { y, x1, x2 } tables: 0-based row, start column inclusive,
--                                      end column exclusive.
--   toggle() — toggle the whole extended-marks namespace on or off

local M = {}

-- The engine's extended-marks namespace.
local ns = vim.api.nvim_create_namespace("dotfiles_extmarks")

-- Registered marks, applied in registration order. Each is { hl_group, find }.
M.marks = {}

----------------------------------------------------------------------------------------------------
-- Highlight namespace toggle
----------------------------------------------------------------------------------------------------

vim.g.hl_ns = 0

-- Set the global highlight namespace to `hl_ns` and remember it in `vim.g.hl_ns`; the global
-- highlight namespace is not stored anywhere by default.
local function set_global_hl_ns(hl_ns)
	vim.g.hl_ns = hl_ns
	vim.api.nvim_set_hl_ns(hl_ns)
end

-- Toggle the extended-marks namespace on (the engine's namespace) or off (the default 0).
function M.toggle()
	set_global_hl_ns(vim.g.hl_ns == 0 and ns or 0)
end

----------------------------------------------------------------------------------------------------
-- Marking
----------------------------------------------------------------------------------------------------

-- Register the highlight group `hl_group` (defined in the engine's namespace as `hl_def`) and mark
-- the positions yielded by `find` with it. See the header for the `find` contract.
function M.register(hl_group, hl_def, find)
	assert(type(hl_group) == "string", "extmark.register: hl_group must be a string")
	assert(type(hl_def) == "table", "extmark.register: hl_def must be a table")
	assert(type(find) == "function", "extmark.register: find must be a function")
	vim.api.nvim_set_hl(ns, hl_group, hl_def)
	M.marks[#M.marks + 1] = { hl_group = hl_group, find = find }
end

-- Refresh the marks in buffer `buf` in range `first`:`last`. Clears the namespace there, then for
-- each registered mark sets an extmark on every position its `find` yields.
local function mark_all(buf, first, last)
	vim.api.nvim_buf_clear_namespace(buf, ns, first, last)
	for _, mark in ipairs(M.marks) do
		for p in mark.find(buf, first, last) do
			vim.api.nvim_buf_set_extmark(buf, ns, p.y, p.x1, {
				end_col = p.x2,
				hl_group = mark.hl_group,
			})
		end
	end
end

-- Mark buffer `ev.buf` and, once per buffer, attach to keep its marks in sync as lines change.
local function watch_extmarks(ev)
	mark_all(ev.buf, 0, -1)
	if vim.b[ev.buf].dotfiles_extmarks_attached then
		return
	end
	vim.b[ev.buf].dotfiles_extmarks_attached = true
	vim.api.nvim_buf_attach(ev.buf, false, {
		on_lines = function(_, b, _, first, _, last_new)
			vim.schedule(function()
				if vim.api.nvim_buf_is_valid(b) then
					mark_all(b, first, last_new)
				end
			end)
		end,
	})
end

----------------------------------------------------------------------------------------------------

vim.api.nvim_create_autocmd("FileType", {
	desc = "Set extended marks watcher",
	group = vim.g.dotfiles.augroup,
	callback = watch_extmarks,
})

-- Turn the namespace on at startup.
set_global_hl_ns(ns)

return M