feat(buffer): persist fold state across sessions (#94)

Problem: folded category headers are lost when Neovim exits because
`_fold_state` only lives in memory. Users must re-fold categories
every session.

Solution: store folded category names in the JSON data file as a
top-level `folded_categories` field. On first render, `restore_folds`
seeds from the store instead of the empty in-memory state. Folds are
persisted on `M.close()` and `VimLeavePre`.
This commit is contained in:
Barrett Ruth 2026-03-07 20:18:34 -05:00
parent 61eeed58f7
commit 22d0b4a3d4
4 changed files with 121 additions and 1 deletions

View file

@ -1,4 +1,5 @@
local config = require('pending.config')
local log = require('pending.log')
local views = require('pending.views')
---@class pending.buffer
@ -18,6 +19,8 @@ local current_view = nil
local _meta = {}
---@type table<integer, table<string, boolean>>
local _fold_state = {}
---@type boolean
local _initial_fold_loaded = false
---@type string[]
local _filter_predicates = {}
---@type table<integer, true>
@ -89,12 +92,52 @@ function M.clear_marks(b)
vim.api.nvim_buf_clear_namespace(b or task_bufnr, task_ns, 0, -1)
end
---@return nil
function M.persist_folds()
log.debug(('persist_folds: view=%s store=%s'):format(tostring(current_view), tostring(_store ~= nil)))
if current_view ~= 'category' or not _store then
log.debug('persist_folds: early return (view or store)')
return
end
local bufnr = task_bufnr
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
log.debug('persist_folds: early return (no valid bufnr)')
return
end
local folded = {}
local seen = {}
local wins = vim.fn.win_findbuf(bufnr)
log.debug(('persist_folds: checking %d windows for bufnr=%d, meta has %d entries'):format(#wins, bufnr, #_meta))
for _, winid in ipairs(wins) do
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_call(winid, function()
for lnum, m in ipairs(_meta) do
if m.type == 'header' and m.category and not seen[m.category] then
local closed = vim.fn.foldclosed(lnum)
log.debug(('persist_folds: win=%d lnum=%d cat=%s foldclosed=%d'):format(winid, lnum, m.category, closed))
if closed ~= -1 then
seen[m.category] = true
table.insert(folded, m.category)
end
end
end
end)
end
end
log.debug(('persist_folds: saving %d folded categories: %s'):format(#folded, table.concat(folded, ', ')))
_store:set_folded_categories(folded)
end
---@return nil
function M.close()
if not task_winid or not vim.api.nvim_win_is_valid(task_winid) then
task_winid = nil
return
end
M.persist_folds()
if _store then
_store:save()
end
local wins = vim.api.nvim_list_wins()
if #wins == 1 then
vim.cmd.enew()
@ -275,7 +318,7 @@ local function snapshot_folds(bufnr)
return
end
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
if _fold_state[winid] == nil then
if _fold_state[winid] == nil and _initial_fold_loaded then
local state = {}
vim.api.nvim_win_call(winid, function()
for lnum, m in ipairs(_meta) do
@ -292,18 +335,39 @@ local function snapshot_folds(bufnr)
end
local function restore_folds(bufnr)
log.debug(('restore_folds: view=%s folding_enabled=%s'):format(
tostring(current_view), tostring(config.resolve_folding().enabled)))
if current_view ~= 'category' or not config.resolve_folding().enabled then
return
end
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
local state = _fold_state[winid]
_fold_state[winid] = nil
log.debug(('restore_folds: win=%d has_fold_state=%s initial_loaded=%s has_store=%s'):format(
winid, tostring(state ~= nil), tostring(_initial_fold_loaded), tostring(_store ~= nil)))
if not state and not _initial_fold_loaded and _store then
_initial_fold_loaded = true
local cats = _store:get_folded_categories()
log.debug(('restore_folds: loaded %d categories from store: %s'):format(#cats, table.concat(cats, ', ')))
if #cats > 0 then
state = {}
for _, cat in ipairs(cats) do
state[cat] = true
end
end
end
if state and next(state) ~= nil then
local applying = {}
for k in pairs(state) do
table.insert(applying, k)
end
log.debug(('restore_folds: applying folds for: %s'):format(table.concat(applying, ', ')))
vim.api.nvim_win_call(winid, function()
vim.cmd('normal! zx')
local saved = vim.api.nvim_win_get_cursor(0)
for lnum, m in ipairs(_meta) do
if m.type == 'header' and m.category and state[m.category] then
log.debug(('restore_folds: folding lnum=%d cat=%s'):format(lnum, m.category))
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
vim.cmd('normal! zc')
end