Skip to content

Commit 2fc30d3

Browse files
authored
fix(folds): update fold boundaries after headline edits (#1078)
* fix(foldtext): invalidate cache on buffer changes The foldtext highlight cache was keyed by line number but never invalidated when buffer content changed. After edits that insert or remove lines, cached entries mapped to wrong content. Track changedtick per buffer and clear the cache in on_win when it changes. This is checked once per window per redraw cycle. * fix(folds): update boundaries after edits Neovim's treesitter foldexpr only re-evaluates fold levels for lines reported in on_changedtree, which typically covers just the edited headline. But fold boundaries extend to the end of the section, so they go stale after TODO, priority or tag changes. Capture the section range before the edit (TS nodes become stale after buffer changes), then schedule vim._foldupdate for the full section range after treesitter has reparsed.
1 parent 9deee54 commit 2fc30d3

3 files changed

Lines changed: 47 additions & 1 deletion

File tree

lua/orgmode/colors/highlighter/foldtext.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,32 @@ local config = require('orgmode.config')
66
---@field highlighter OrgHighlighter
77
---@field namespace number
88
---@field cache table<number, table<number, OrgFoldtextLineValue>>
9+
---@field cache_tick table<number, number>
910
local OrgFoldtext = {}
1011

1112
---@param opts { highlighter: OrgHighlighter }
1213
function OrgFoldtext:new(opts)
1314
local data = {
1415
highlighter = opts.highlighter,
1516
cache = setmetatable({}, { __mode = 'k' }),
17+
cache_tick = {},
1618
}
1719
setmetatable(data, self)
1820
self.__index = self
1921
return data
2022
end
2123

24+
---Invalidate cache for a buffer if its content has changed since the last render.
25+
---Call this once per redraw from on_win, not per line.
26+
---@param bufnr number
27+
function OrgFoldtext:check_cache(bufnr)
28+
local tick = vim.api.nvim_buf_get_changedtick(bufnr)
29+
if self.cache_tick[bufnr] ~= tick then
30+
self.cache[bufnr] = nil
31+
self.cache_tick[bufnr] = tick
32+
end
33+
end
34+
2235
---@param bufnr number
2336
---@param line number
2437
---@param value OrgFoldtextLineValue
@@ -101,6 +114,7 @@ end
101114

102115
function OrgFoldtext:on_detach(bufnr)
103116
self.cache[bufnr] = nil
117+
self.cache_tick[bufnr] = nil
104118
end
105119

106120
return OrgFoldtext

lua/orgmode/colors/highlighter/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function OrgHighlighter:_on_win(_, win, bufnr, topline, botline)
7878
end,
7979
})
8080
else
81+
self.foldtext:check_cache(bufnr)
8182
self:_parse_tree(bufnr, win, { topline, botline + 1 })
8283
if self.parsing[win] then
8384
for line = topline, botline do

lua/orgmode/org/mappings.lua

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@ local Promise = require('orgmode.utils.promise')
1717
local Input = require('orgmode.ui.input')
1818
local Footnote = require('orgmode.objects.footnote')
1919

20+
---Schedule a fold update for the given range. Call after buffer edits.
21+
---OrgRange is 1-indexed; vim._foldupdate expects 0-indexed lines.
22+
---@param range OrgRange
23+
local function schedule_fold_update(range)
24+
local bufnr = vim.api.nvim_get_current_buf()
25+
vim.schedule(function()
26+
if not vim.api.nvim_buf_is_valid(bufnr) then
27+
return
28+
end
29+
local start_line = range.start_line - 1
30+
local end_line = math.min(range.end_line, vim.api.nvim_buf_line_count(bufnr))
31+
for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do
32+
if vim.wo[win].foldmethod == 'expr' then
33+
vim._foldupdate(win, start_line, end_line)
34+
end
35+
end
36+
end)
37+
end
38+
2039
---@class OrgMappings
2140
---@field capture OrgCapture
2241
---@field agenda OrgAgenda
@@ -50,6 +69,8 @@ function OrgMappings:set_tags(tags)
5069
local headline = self.files:get_closest_headline()
5170
local headline_tags = headline:get_own_tags()
5271
local current_tags = utils.tags_to_string(headline_tags)
72+
-- Capture range before promise chain — TS nodes become stale after edits
73+
local range = headline:get_range()
5374

5475
return Promise.resolve()
5576
:next(function()
@@ -69,14 +90,17 @@ function OrgMappings:set_tags(tags)
6990
return
7091
end
7192

72-
return headline:set_tags(new_tags)
93+
headline:set_tags(new_tags)
94+
schedule_fold_update(range)
7395
end)
7496
end
7597

7698
---@return nil
7799
function OrgMappings:toggle_archive_tag()
78100
local headline = self.files:get_closest_headline()
101+
local range = headline:get_range()
79102
headline:toggle_tag('ARCHIVE')
103+
schedule_fold_update(range)
80104
end
81105

82106
function OrgMappings:cycle()
@@ -344,7 +368,9 @@ function OrgMappings:set_priority(direction)
344368
end
345369
end
346370

371+
local range = headline:get_range()
347372
headline:set_priority(new_priority)
373+
schedule_fold_update(range)
348374
end
349375

350376
function OrgMappings:todo_next_state()
@@ -419,6 +445,9 @@ function OrgMappings:_todo_change_state(direction)
419445
local headline = self.files:get_closest_headline()
420446
local old_state = headline:get_todo()
421447
local was_done = headline:is_done()
448+
449+
local range = headline:get_range()
450+
422451
local changed = self:_change_todo_state(direction, true)
423452

424453
if not changed then
@@ -428,6 +457,8 @@ function OrgMappings:_todo_change_state(direction)
428457
local item = self.files:get_closest_headline()
429458
EventManager.dispatch(events.TodoChanged:new(item, old_state, was_done))
430459

460+
schedule_fold_update(range)
461+
431462
local is_done = item:is_done() and not was_done
432463
local is_undone = not item:is_done() and was_done
433464

0 commit comments

Comments
 (0)