feat(buffer): add configurable category-level folds (#91)
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:
parent
012bd9b043
commit
d8324e6a6d
3 changed files with 84 additions and 6 deletions
|
|
@ -38,7 +38,7 @@ Features: ~
|
||||||
- Multi-level undo (up to 20 `:w` saves, persisted across sessions)
|
- Multi-level undo (up to 20 `:w` saves, persisted across sessions)
|
||||||
- Quick-add from the command line with `:Pending add`
|
- Quick-add from the command line with `:Pending add`
|
||||||
- Quickfix list of overdue/due-today tasks via `:Pending due`
|
- 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>`)
|
- Omnifunc completion for `cat:`, `due:`, and `rec:` tokens (`<C-x><C-o>`)
|
||||||
- Google Calendar one-way push via OAuth PKCE
|
- Google Calendar one-way push via OAuth PKCE
|
||||||
- Google Tasks bidirectional sync 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`)
|
`U` Undo the last `:w` save (`undo`)
|
||||||
`o` Insert a new task line below (`open_line`)
|
`o` Insert a new task line below (`open_line`)
|
||||||
`O` Insert a new task line above (`open_line_above`)
|
`O` Insert a new task line above (`open_line_above`)
|
||||||
`zc` Fold the current category section (category view only)
|
`zc` Fold the current category section (requires `folding`)
|
||||||
`zo` Unfold the current category section (category view only)
|
`zo` Unfold the current category section (requires `folding`)
|
||||||
|
|
||||||
Text objects (operator-pending and visual): ~
|
Text objects (operator-pending and visual): ~
|
||||||
|
|
||||||
|
|
@ -595,6 +595,7 @@ loads: >lua
|
||||||
date_syntax = 'due',
|
date_syntax = 'due',
|
||||||
recur_syntax = 'rec',
|
recur_syntax = 'rec',
|
||||||
someday_date = '9999-12-30',
|
someday_date = '9999-12-30',
|
||||||
|
folding = true,
|
||||||
category_order = {},
|
category_order = {},
|
||||||
keymaps = {
|
keymaps = {
|
||||||
close = 'q',
|
close = 'q',
|
||||||
|
|
@ -684,6 +685,35 @@ Fields: ~
|
||||||
given order. Categories not in the list are appended
|
given order. Categories not in the list are appended
|
||||||
after the ordered ones in their natural order.
|
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*
|
{keymaps} (table, default: see below) *pending.Keymaps*
|
||||||
Buffer-local key bindings. Each field maps an action
|
Buffer-local key bindings. Each field maps an action
|
||||||
name to a key string. Set a field to `false` to
|
name to a key string. Set a field to `false` to
|
||||||
|
|
|
||||||
|
|
@ -236,8 +236,30 @@ local function setup_highlights()
|
||||||
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
|
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
|
||||||
end
|
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)
|
local function snapshot_folds(bufnr)
|
||||||
if current_view ~= 'category' then
|
if current_view ~= 'category' or not config.resolve_folding().enabled then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
|
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
|
||||||
|
|
@ -256,7 +278,7 @@ local function snapshot_folds(bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function restore_folds(bufnr)
|
local function restore_folds(bufnr)
|
||||||
if current_view ~= 'category' then
|
if current_view ~= 'category' or not config.resolve_folding().enabled then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
|
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
|
||||||
|
|
@ -328,12 +350,18 @@ function M.render(bufnr)
|
||||||
setup_syntax(bufnr)
|
setup_syntax(bufnr)
|
||||||
apply_extmarks(bufnr, line_meta)
|
apply_extmarks(bufnr, line_meta)
|
||||||
|
|
||||||
|
local folding = config.resolve_folding()
|
||||||
for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
|
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].foldmethod = 'expr'
|
||||||
vim.wo[winid].foldexpr = 'v:lua.require("pending.buffer").get_fold()'
|
vim.wo[winid].foldexpr = 'v:lua.require("pending.buffer").get_fold()'
|
||||||
vim.wo[winid].foldlevel = 99
|
vim.wo[winid].foldlevel = 99
|
||||||
vim.wo[winid].foldenable = true
|
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
|
else
|
||||||
vim.wo[winid].foldmethod = 'manual'
|
vim.wo[winid].foldmethod = 'manual'
|
||||||
vim.wo[winid].foldenable = false
|
vim.wo[winid].foldenable = false
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
---@class pending.FoldingConfig
|
||||||
|
---@field foldtext? string|false
|
||||||
|
|
||||||
|
---@class pending.ResolvedFolding
|
||||||
|
---@field enabled boolean
|
||||||
|
---@field foldtext string|false
|
||||||
|
|
||||||
---@class pending.Icons
|
---@class pending.Icons
|
||||||
---@field pending string
|
---@field pending string
|
||||||
---@field done string
|
---@field done string
|
||||||
|
|
@ -55,6 +62,7 @@
|
||||||
---@field drawer_height? integer
|
---@field drawer_height? integer
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field keymaps pending.Keymaps
|
---@field keymaps pending.Keymaps
|
||||||
|
---@field folding? boolean|pending.FoldingConfig
|
||||||
---@field sync? pending.SyncConfig
|
---@field sync? pending.SyncConfig
|
||||||
---@field icons pending.Icons
|
---@field icons pending.Icons
|
||||||
|
|
||||||
|
|
@ -70,6 +78,7 @@ local defaults = {
|
||||||
date_syntax = 'due',
|
date_syntax = 'due',
|
||||||
recur_syntax = 'rec',
|
recur_syntax = 'rec',
|
||||||
someday_date = '9999-12-30',
|
someday_date = '9999-12-30',
|
||||||
|
folding = true,
|
||||||
category_order = {},
|
category_order = {},
|
||||||
keymaps = {
|
keymaps = {
|
||||||
close = 'q',
|
close = 'q',
|
||||||
|
|
@ -119,4 +128,15 @@ function M.reset()
|
||||||
_resolved = nil
|
_resolved = nil
|
||||||
end
|
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
|
return M
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue