feat(views): make queue view sort order configurable (#154)
Problem: the queue/priority view sort in `sort_tasks_priority()` uses a hardcoded tiebreak chain (status, priority, due, order, id). Users who care more about due dates than priority have no way to reorder it. Solution: add `view.queue.sort` config field (string[]) that defines an ordered tiebreak chain. `build_queue_comparator()` maps each key to a comparison function and returns a single comparator. Unknown keys emit a `log.warn`. The default matches the previous hardcoded behavior.
This commit is contained in:
parent
0707caf83c
commit
32efcbcf5c
4 changed files with 169 additions and 12 deletions
|
|
@ -542,11 +542,12 @@ Category view (default): ~ *pending-view-category*
|
|||
`zc` and `zo`.
|
||||
|
||||
Queue view: ~ *pending-view-queue*
|
||||
A flat list of all tasks sorted by status (wip → pending → blocked →
|
||||
done), then by priority, then by due date (tasks without a due date sort
|
||||
last), then by internal order. Category names are shown as right-aligned virtual
|
||||
text alongside the due date virtual text so tasks remain identifiable
|
||||
across categories. The buffer is named `pending://queue`.
|
||||
A flat list of all tasks sorted by a configurable tiebreak chain
|
||||
(default: status → priority → due → order → id). See
|
||||
`view.queue.sort` in |pending-config| for customization. Category
|
||||
names are shown as right-aligned virtual text alongside the due date
|
||||
virtual text so tasks remain identifiable across categories. The
|
||||
buffer is named `pending://queue`.
|
||||
|
||||
==============================================================================
|
||||
FILTERS *pending-filters*
|
||||
|
|
@ -749,7 +750,9 @@ loads: >lua
|
|||
order = {},
|
||||
folding = true,
|
||||
},
|
||||
queue = {},
|
||||
queue = {
|
||||
sort = { 'status', 'priority', 'due', 'order', 'id' },
|
||||
},
|
||||
},
|
||||
keymaps = {
|
||||
close = 'q',
|
||||
|
|
@ -891,6 +894,24 @@ Fields: ~
|
|||
|
||||
{queue} (table) *pending.QueueViewConfig*
|
||||
Queue (priority) view settings.
|
||||
{sort} (string[], default:
|
||||
`{ 'status', 'priority', 'due',
|
||||
'order', 'id' }`)
|
||||
Ordered tiebreak chain for the
|
||||
queue view sort. Each element is a
|
||||
sort key; the comparator walks the
|
||||
list and returns on the first
|
||||
non-equal comparison. Valid keys:
|
||||
`status` wip < pending <
|
||||
blocked < done
|
||||
`priority` higher number first
|
||||
`due` sooner first, no-due
|
||||
last
|
||||
`order` ascending
|
||||
`id` ascending
|
||||
`age` alias for `id`
|
||||
Unknown keys are ignored with a
|
||||
warning.
|
||||
|
||||
Examples: >lua
|
||||
vim.g.pending = {
|
||||
|
|
@ -901,6 +922,10 @@ Fields: ~
|
|||
order = { 'Work', 'Personal' },
|
||||
folding = { foldtext = '%c: %n items' },
|
||||
},
|
||||
queue = {
|
||||
sort = { 'status', 'due', 'priority',
|
||||
'order', 'id' },
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
---@field hide_done_categories? boolean
|
||||
|
||||
---@class pending.QueueViewConfig
|
||||
---@field sort? string[]
|
||||
|
||||
---@class pending.ViewConfig
|
||||
---@field default? 'category'|'priority'
|
||||
|
|
@ -133,7 +134,9 @@ local defaults = {
|
|||
folding = true,
|
||||
hide_done_categories = false,
|
||||
},
|
||||
queue = {},
|
||||
queue = {
|
||||
sort = { 'status', 'priority', 'due', 'order', 'id' },
|
||||
},
|
||||
},
|
||||
keymaps = {
|
||||
close = 'q',
|
||||
|
|
|
|||
|
|
@ -106,17 +106,23 @@ local function sort_tasks(tasks)
|
|||
end)
|
||||
end
|
||||
|
||||
---@param tasks pending.Task[]
|
||||
local function sort_tasks_priority(tasks)
|
||||
table.sort(tasks, function(a, b)
|
||||
---@type table<string, fun(a: pending.Task, b: pending.Task): boolean?>
|
||||
local sort_key_comparators = {
|
||||
status = function(a, b)
|
||||
local ra = status_rank[a.status] or 1
|
||||
local rb = status_rank[b.status] or 1
|
||||
if ra ~= rb then
|
||||
return ra < rb
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
priority = function(a, b)
|
||||
if a.priority ~= b.priority then
|
||||
return a.priority > b.priority
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
due = function(a, b)
|
||||
local a_due = a.due or ''
|
||||
local b_due = b.due or ''
|
||||
if a_due ~= b_due then
|
||||
|
|
@ -128,11 +134,56 @@ local function sort_tasks_priority(tasks)
|
|||
end
|
||||
return a_due < b_due
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
order = function(a, b)
|
||||
if a.order ~= b.order then
|
||||
return a.order < b.order
|
||||
end
|
||||
return a.id < b.id
|
||||
end)
|
||||
return nil
|
||||
end,
|
||||
id = function(a, b)
|
||||
if a.id ~= b.id then
|
||||
return a.id < b.id
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
age = function(a, b)
|
||||
if a.id ~= b.id then
|
||||
return a.id < b.id
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
|
||||
---@return fun(a: pending.Task, b: pending.Task): boolean
|
||||
local function build_queue_comparator()
|
||||
local log = require('pending.log')
|
||||
local keys = config.get().view.queue.sort or { 'status', 'priority', 'due', 'order', 'id' }
|
||||
local comparators = {}
|
||||
for _, key in ipairs(keys) do
|
||||
local cmp = sort_key_comparators[key]
|
||||
if cmp then
|
||||
table.insert(comparators, cmp)
|
||||
else
|
||||
log.warn('unknown queue sort key: ' .. key)
|
||||
end
|
||||
end
|
||||
return function(a, b)
|
||||
for _, cmp in ipairs(comparators) do
|
||||
local result = cmp(a, b)
|
||||
if result ~= nil then
|
||||
return result
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
---@param tasks pending.Task[]
|
||||
local function sort_tasks_priority(tasks)
|
||||
local cmp = build_queue_comparator()
|
||||
table.sort(tasks, cmp)
|
||||
end
|
||||
|
||||
---@param tasks pending.Task[]
|
||||
|
|
|
|||
|
|
@ -529,5 +529,83 @@ describe('views', function()
|
|||
end
|
||||
assert.is_nil(task_meta.recur)
|
||||
end)
|
||||
|
||||
it('sorts by due before priority when sort config is reordered', function()
|
||||
vim.g.pending = {
|
||||
data_path = tmpdir .. '/tasks.json',
|
||||
view = { queue = { sort = { 'status', 'due', 'priority', 'order', 'id' } } },
|
||||
}
|
||||
config.reset()
|
||||
s:add({ description = 'High no due', category = 'Work', priority = 2 })
|
||||
s:add({ description = 'Low with due', category = 'Work', priority = 0, due = '2050-01-01' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local due_row, nodue_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
if lines[i]:find('Low with due') then
|
||||
due_row = i
|
||||
elseif lines[i]:find('High no due') then
|
||||
nodue_row = i
|
||||
end
|
||||
end
|
||||
end
|
||||
assert.is_true(due_row < nodue_row)
|
||||
end)
|
||||
|
||||
it('uses default sort when config sort is nil', function()
|
||||
vim.g.pending = {
|
||||
data_path = tmpdir .. '/tasks.json',
|
||||
view = { queue = {} },
|
||||
}
|
||||
config.reset()
|
||||
s:add({ description = 'Low', category = 'Work', priority = 0 })
|
||||
s:add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local high_row, low_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
if lines[i]:find('High') then
|
||||
high_row = i
|
||||
elseif lines[i]:find('Low') then
|
||||
low_row = i
|
||||
end
|
||||
end
|
||||
end
|
||||
assert.is_true(high_row < low_row)
|
||||
end)
|
||||
|
||||
it('ignores unknown sort keys with a warning', function()
|
||||
vim.g.pending = {
|
||||
data_path = tmpdir .. '/tasks.json',
|
||||
view = { queue = { sort = { 'bogus', 'status', 'id' } } },
|
||||
}
|
||||
config.reset()
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Work' })
|
||||
local lines = views.priority_view(s:active_tasks())
|
||||
assert.is_true(#lines == 2)
|
||||
end)
|
||||
|
||||
it('supports age sort key as alias for id', function()
|
||||
vim.g.pending = {
|
||||
data_path = tmpdir .. '/tasks.json',
|
||||
view = { queue = { sort = { 'age' } } },
|
||||
}
|
||||
config.reset()
|
||||
s:add({ description = 'Older', category = 'Work' })
|
||||
s:add({ description = 'Newer', category = 'Work' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local older_row, newer_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
if lines[i]:find('Older') then
|
||||
older_row = i
|
||||
elseif lines[i]:find('Newer') then
|
||||
newer_row = i
|
||||
end
|
||||
end
|
||||
end
|
||||
assert.is_true(older_row < newer_row)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue