feat(buffer): add configurable category-level folds

Problem: category folds were hardcoded with no config option, no custom
foldtext, and no vimdoc coverage.

Solution: add `folding` config field (boolean or table with `foldtext`
format string). Default foldtext is `%c (%n tasks)` with automatic
singular/plural. Gate all fold logic on the config so `folding = false`
disables folds entirely. Document the new option in vimdoc.
This commit is contained in:
Barrett Ruth 2026-03-06 20:04:22 -05:00
parent 12b9295c34
commit 03d3ac5851
3 changed files with 84 additions and 6 deletions

View file

@ -38,7 +38,7 @@ Features: ~
- Multi-level undo (up to 20 `:w` saves, persisted across sessions)
- Quick-add from the command line with `:Pending add`
- Quickfix list of overdue/due-today tasks via `:Pending due`
- Foldable category sections (`zc`/`zo`) in category view
- Configurable category folds (`zc`/`zo`) with custom foldtext
- Omnifunc completion for `cat:`, `due:`, and `rec:` tokens (`<C-x><C-o>`)
- Google Calendar one-way push via OAuth PKCE
- Google Tasks bidirectional sync via OAuth PKCE
@ -274,8 +274,8 @@ Default buffer-local keys: ~
`U` Undo the last `:w` save (`undo`)
`o` Insert a new task line below (`open_line`)
`O` Insert a new task line above (`open_line_above`)
`zc` Fold the current category section (category view only)
`zo` Unfold the current category section (category view only)
`zc` Fold the current category section (requires `folding`)
`zo` Unfold the current category section (requires `folding`)
Text objects (operator-pending and visual): ~
@ -595,6 +595,7 @@ loads: >lua
date_syntax = 'due',
recur_syntax = 'rec',
someday_date = '9999-12-30',
folding = true,
category_order = {},
keymaps = {
close = 'q',
@ -684,6 +685,35 @@ Fields: ~
given order. Categories not in the list are appended
after the ordered ones in their natural order.
{folding} (boolean|table, default: true) *pending.FoldingConfig*
Controls category-level folds in category view. When
`true`, folds are enabled with the default foldtext
`'%c (%n tasks)'`. When `false`, folds are disabled
entirely. When a table, folds are enabled and the
table may contain:
{foldtext} (string|false, default: '%c (%n tasks)')
Custom foldtext format string. Set to
`false` to use Vim's built-in
foldtext. Two specifiers are
available:
`%c` category name
`%n` number of tasks in the fold
The category icon is prepended
automatically. When `false`, the
default Vim foldtext is used.
Folds only apply to category view; priority view
is always fold-free regardless of this setting.
Examples: >lua
vim.g.pending = { folding = true }
vim.g.pending = { folding = false }
vim.g.pending = {
folding = { foldtext = '%c (%n tasks)' },
}
<
{keymaps} (table, default: see below) *pending.Keymaps*
Buffer-local key bindings. Each field maps an action
name to a key string. Set a field to `false` to

View file

@ -236,8 +236,30 @@ local function setup_highlights()
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
end
---@return string
function M.get_foldtext()
local folding = config.resolve_folding()
if not folding.foldtext then
return vim.fn.foldtext()
end
local line = vim.fn.getline(vim.v.foldstart)
local cat = line:match('^#%s+(.+)$') or line
local task_count = vim.v.foldend - vim.v.foldstart
local icons = config.get().icons
local result = folding.foldtext
:gsub('%%c', cat)
:gsub('%%n', tostring(task_count))
:gsub('(%d+) (%w+)s%)', function(n, word)
if n == '1' then
return n .. ' ' .. word .. ')'
end
return n .. ' ' .. word .. 's)'
end)
return icons.category .. ' ' .. result
end
local function snapshot_folds(bufnr)
if current_view ~= 'category' then
if current_view ~= 'category' or not config.resolve_folding().enabled then
return
end
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
@ -256,7 +278,7 @@ local function snapshot_folds(bufnr)
end
local function restore_folds(bufnr)
if current_view ~= 'category' then
if current_view ~= 'category' or not config.resolve_folding().enabled then
return
end
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
@ -328,12 +350,18 @@ function M.render(bufnr)
setup_syntax(bufnr)
apply_extmarks(bufnr, line_meta)
local folding = config.resolve_folding()
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
if current_view == 'category' then
if current_view == 'category' and folding.enabled then
vim.wo[winid].foldmethod = 'expr'
vim.wo[winid].foldexpr = 'v:lua.require("pending.buffer").get_fold()'
vim.wo[winid].foldlevel = 99
vim.wo[winid].foldenable = true
if folding.foldtext then
vim.wo[winid].foldtext = 'v:lua.require("pending.buffer").get_foldtext()'
else
vim.wo[winid].foldtext = 'foldtext()'
end
else
vim.wo[winid].foldmethod = 'manual'
vim.wo[winid].foldenable = false

View file

@ -1,3 +1,10 @@
---@class pending.FoldingConfig
---@field foldtext? string|false
---@class pending.ResolvedFolding
---@field enabled boolean
---@field foldtext string|false
---@class pending.Icons
---@field pending string
---@field done string
@ -55,6 +62,7 @@
---@field drawer_height? integer
---@field debug? boolean
---@field keymaps pending.Keymaps
---@field folding? boolean|pending.FoldingConfig
---@field sync? pending.SyncConfig
---@field icons pending.Icons
@ -70,6 +78,7 @@ local defaults = {
date_syntax = 'due',
recur_syntax = 'rec',
someday_date = '9999-12-30',
folding = true,
category_order = {},
keymaps = {
close = 'q',
@ -119,4 +128,15 @@ function M.reset()
_resolved = nil
end
---@return pending.ResolvedFolding
function M.resolve_folding()
local raw = M.get().folding
if raw == false then
return { enabled = false, foldtext = false }
elseif raw == true or raw == nil then
return { enabled = true, foldtext = '%c (%n tasks)' }
end
return { enabled = true, foldtext = raw.foldtext or false }
end
return M