Skip to content

Commit 921b11d

Browse files
committed
perf(stars): Cache leading-star positions
The on_line hook ran a tree query per visible line per redraw, making leading-star highlighting a hot path while typing. Cache end_col per buffer line. Shift/drop entries on buffer edits via nvim_buf_attach (on_bytes/on_reload), and rebuild the changed ranges from on_changedtree once the parser has produced a new tree. on_line is now a plain table lookup. Mirrors the cache pattern already used by foldtext.
1 parent 7ab5900 commit 921b11d

2 files changed

Lines changed: 119 additions & 9 deletions

File tree

lua/orgmode/colors/highlighter/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function OrgHighlighter:_on_win(_, win, bufnr, topline, botline)
7272
self.foldtext:on_win(bufnr, win, topline, botline)
7373
if not self.buffers[bufnr] then
7474
self.buffers[bufnr] = { language_tree = vim.treesitter.get_parser(bufnr, 'org') }
75+
self.stars:attach(bufnr, self.buffers[bufnr].language_tree)
7576
self:_parse_tree(bufnr, win, false)
7677
self.buffers[bufnr].language_tree:register_cbs({
7778
on_detach = function(buf)
Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,140 @@
1-
local ts_utils = require('orgmode.utils.treesitter')
21
local config = require('orgmode.config')
32

3+
local query = vim.treesitter.query.parse('org', '(stars) @stars')
4+
45
---@class OrgStarsHighlighter
56
---@field highlighter OrgHighlighter
7+
---@field private cache table<number, table<number, number>>
8+
---@field private attached table<number, boolean>
69
local OrgStars = {}
710

811
---@param opts { highlighter: OrgHighlighter }
912
function OrgStars:new(opts)
1013
local data = {
1114
highlighter = opts.highlighter,
15+
cache = setmetatable({}, { __mode = 'k' }),
16+
attached = {},
1217
}
1318
setmetatable(data, self)
1419
self.__index = self
1520
return data
1621
end
1722

18-
function OrgStars:on_line(bufnr, line)
19-
if not config:hide_leading_stars(bufnr) then
23+
---Mirror of foldtext._apply_change: drop entries in the changed range and
24+
---shift entries after the boundary by the row delta. Keeps cached end_col of
25+
---unchanged headlines valid across edits without a tree query.
26+
---For single-line edits past the cached `(stars)` end_col, the entry is kept:
27+
---those edits cannot affect the leading-star span, so dropping it would only
28+
---cause a one-frame flicker until on_changedtree rebuilds.
29+
---@param bufnr number
30+
---@param start_row number 0-based
31+
---@param start_col number 0-based byte column of the first changed byte
32+
---@param old_end_row number rows in old range (relative to start_row)
33+
---@param new_end_row number rows in new range (relative to start_row)
34+
function OrgStars:_apply_change(bufnr, start_row, start_col, old_end_row, new_end_row)
35+
local cache = self.cache[bufnr]
36+
if not cache then
2037
return
2138
end
2239

23-
local node = ts_utils.get_node({
24-
bufnr = bufnr,
25-
pos = { line, 0 },
40+
if old_end_row == new_end_row then
41+
for line = start_row, start_row + old_end_row do
42+
local cached = cache[line]
43+
if not cached or line ~= start_row or start_col <= cached then
44+
cache[line] = nil
45+
end
46+
end
47+
return
48+
end
49+
50+
local delta = new_end_row - old_end_row
51+
local boundary = start_row + old_end_row
52+
local shifted = {}
53+
for k, v in pairs(cache) do
54+
if k < start_row then
55+
shifted[k] = v
56+
elseif k > boundary then
57+
shifted[k + delta] = v
58+
end
59+
end
60+
self.cache[bufnr] = shifted
61+
end
62+
63+
---Re-query the tree for `(stars)` nodes within each changed range. Fires
64+
---after the parser produced a new tree, so ranges are in new-tree coordinates
65+
---and the lookup is authoritative.
66+
---@param bufnr number
67+
---@param tree TSTree
68+
---@param changes Range6[]
69+
function OrgStars:_apply_changedtree(bufnr, tree, changes)
70+
if not self.attached[bufnr] then
71+
return
72+
end
73+
local cache = self.cache[bufnr]
74+
if not cache then
75+
cache = {}
76+
self.cache[bufnr] = cache
77+
end
78+
local root = tree:root()
79+
for _, range in ipairs(changes) do
80+
local start_row, end_row = range[1], range[4]
81+
for line = start_row, end_row do
82+
cache[line] = nil
83+
end
84+
for _, node in query:iter_captures(root, bufnr, start_row, end_row + 1) do
85+
local sr, _, er, ec = node:range()
86+
if sr == er and ec > 0 then
87+
cache[sr] = ec
88+
end
89+
end
90+
end
91+
end
92+
93+
---Attach buffer + language_tree listeners. Idempotent per buffer.
94+
---@param bufnr number
95+
---@param language_tree vim.treesitter.LanguageTree
96+
function OrgStars:attach(bufnr, language_tree)
97+
if self.attached[bufnr] then
98+
return
99+
end
100+
local ok = vim.api.nvim_buf_attach(bufnr, false, {
101+
on_bytes = function(_, b, _tick, start_row, start_col, _, old_end_row, _, _, new_end_row)
102+
self:_apply_change(b, start_row, start_col, old_end_row, new_end_row)
103+
end,
104+
on_reload = function(_, b)
105+
self.cache[b] = nil
106+
end,
107+
on_detach = function(_, b)
108+
self:on_detach(b)
109+
end,
26110
})
111+
if not ok then
112+
return
113+
end
114+
self.attached[bufnr] = true
115+
language_tree:register_cbs({
116+
on_changedtree = function(changes, tree)
117+
if tree then
118+
self:_apply_changedtree(bufnr, tree, changes)
119+
end
120+
end,
121+
})
122+
end
27123

28-
if not node or node:type() ~= 'stars' then
124+
---@param bufnr number
125+
---@param line number
126+
function OrgStars:on_line(bufnr, line)
127+
if not config:hide_leading_stars(bufnr) then
29128
return
30129
end
31130

32-
local _, end_col = node:end_()
131+
local cache = self.cache[bufnr]
132+
if not cache then
133+
return
134+
end
33135

34-
if end_col <= 0 then
136+
local end_col = cache[line]
137+
if not end_col or end_col <= 0 then
35138
return
36139
end
37140

@@ -43,4 +146,10 @@ function OrgStars:on_line(bufnr, line)
43146
})
44147
end
45148

149+
---@param bufnr number
150+
function OrgStars:on_detach(bufnr)
151+
self.cache[bufnr] = nil
152+
self.attached[bufnr] = nil
153+
end
154+
46155
return OrgStars

0 commit comments

Comments
 (0)