Problem: no way to narrow the pending buffer to a subset of tasks without manual scrolling; filtered-out tasks would be silently deleted on :w because diff.apply() marks unseen IDs as deleted. Solution: add a FILTER: line rendered at the top of the buffer when a filter is active. The line is editable — :w re-parses it and updates the hidden set. diff.apply() gains a hidden_ids param that prevents filtered-out tasks from being marked deleted. Predicates: cat:X, overdue, today, priority (space-separated AND). :Pending filter sets it programmatically; :Pending filter clear removes it.
261 lines
8.9 KiB
Lua
261 lines
8.9 KiB
Lua
require('spec.helpers')
|
|
|
|
local config = require('pending.config')
|
|
local store = require('pending.store')
|
|
local diff = require('pending.diff')
|
|
|
|
describe('filter', function()
|
|
local tmpdir
|
|
local pending
|
|
local buffer
|
|
|
|
before_each(function()
|
|
tmpdir = vim.fn.tempname()
|
|
vim.fn.mkdir(tmpdir, 'p')
|
|
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
|
config.reset()
|
|
store.unload()
|
|
package.loaded['pending'] = nil
|
|
package.loaded['pending.buffer'] = nil
|
|
pending = require('pending')
|
|
buffer = require('pending.buffer')
|
|
buffer.set_filter({}, {})
|
|
end)
|
|
|
|
after_each(function()
|
|
vim.fn.delete(tmpdir, 'rf')
|
|
vim.g.pending = nil
|
|
config.reset()
|
|
store.unload()
|
|
package.loaded['pending'] = nil
|
|
package.loaded['pending.buffer'] = nil
|
|
end)
|
|
|
|
describe('filter predicates', function()
|
|
it('cat: hides tasks with non-matching category', function()
|
|
store.load()
|
|
store.add({ description = 'Work task', category = 'Work' })
|
|
store.add({ description = 'Home task', category = 'Home' })
|
|
store.save()
|
|
pending.filter('cat:Work')
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local work_task = nil
|
|
local home_task = nil
|
|
for _, t in ipairs(tasks) do
|
|
if t.category == 'Work' then work_task = t end
|
|
if t.category == 'Home' then home_task = t end
|
|
end
|
|
assert.is_not_nil(work_task)
|
|
assert.is_not_nil(home_task)
|
|
assert.is_nil(hidden[work_task.id])
|
|
assert.is_true(hidden[home_task.id])
|
|
end)
|
|
|
|
it('cat: hides tasks with no category (default category)', function()
|
|
store.load()
|
|
store.add({ description = 'Work task', category = 'Work' })
|
|
store.add({ description = 'Inbox task' })
|
|
store.save()
|
|
pending.filter('cat:Work')
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local inbox_task = nil
|
|
for _, t in ipairs(tasks) do
|
|
if t.category ~= 'Work' then inbox_task = t end
|
|
end
|
|
assert.is_not_nil(inbox_task)
|
|
assert.is_true(hidden[inbox_task.id])
|
|
end)
|
|
|
|
it('overdue hides non-overdue tasks', function()
|
|
store.load()
|
|
store.add({ description = 'Old task', due = '2020-01-01' })
|
|
store.add({ description = 'Future task', due = '2099-01-01' })
|
|
store.add({ description = 'No due task' })
|
|
store.save()
|
|
pending.filter('overdue')
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local overdue_task, future_task, nodue_task
|
|
for _, t in ipairs(tasks) do
|
|
if t.due == '2020-01-01' then overdue_task = t end
|
|
if t.due == '2099-01-01' then future_task = t end
|
|
if not t.due then nodue_task = t end
|
|
end
|
|
assert.is_nil(hidden[overdue_task.id])
|
|
assert.is_true(hidden[future_task.id])
|
|
assert.is_true(hidden[nodue_task.id])
|
|
end)
|
|
|
|
it('today hides non-today tasks', function()
|
|
store.load()
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
store.add({ description = 'Today task', due = today })
|
|
store.add({ description = 'Old task', due = '2020-01-01' })
|
|
store.add({ description = 'Future task', due = '2099-01-01' })
|
|
store.save()
|
|
pending.filter('today')
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local today_task, old_task, future_task
|
|
for _, t in ipairs(tasks) do
|
|
if t.due == today then today_task = t end
|
|
if t.due == '2020-01-01' then old_task = t end
|
|
if t.due == '2099-01-01' then future_task = t end
|
|
end
|
|
assert.is_nil(hidden[today_task.id])
|
|
assert.is_true(hidden[old_task.id])
|
|
assert.is_true(hidden[future_task.id])
|
|
end)
|
|
|
|
it('priority hides non-priority tasks', function()
|
|
store.load()
|
|
store.add({ description = 'Important', priority = 1 })
|
|
store.add({ description = 'Normal' })
|
|
store.save()
|
|
pending.filter('priority')
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local important_task, normal_task
|
|
for _, t in ipairs(tasks) do
|
|
if t.priority and t.priority > 0 then important_task = t end
|
|
if not t.priority or t.priority == 0 then normal_task = t end
|
|
end
|
|
assert.is_nil(hidden[important_task.id])
|
|
assert.is_true(hidden[normal_task.id])
|
|
end)
|
|
|
|
it('multi-predicate AND: cat:Work + overdue', function()
|
|
store.load()
|
|
store.add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
|
store.add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
|
store.add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
|
store.save()
|
|
pending.filter('cat:Work overdue')
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local work_overdue, work_future, home_overdue
|
|
for _, t in ipairs(tasks) do
|
|
if t.description == 'Work overdue' then work_overdue = t end
|
|
if t.description == 'Work future' then work_future = t end
|
|
if t.description == 'Home overdue' then home_overdue = t end
|
|
end
|
|
assert.is_nil(hidden[work_overdue.id])
|
|
assert.is_true(hidden[work_future.id])
|
|
assert.is_true(hidden[home_overdue.id])
|
|
end)
|
|
|
|
it('filter clear removes all predicates and hidden ids', function()
|
|
store.load()
|
|
store.add({ description = 'Work task', category = 'Work' })
|
|
store.add({ description = 'Home task', category = 'Home' })
|
|
store.save()
|
|
pending.filter('cat:Work')
|
|
assert.are.equal(1, #buffer.filter_predicates())
|
|
pending.filter('clear')
|
|
assert.are.equal(0, #buffer.filter_predicates())
|
|
assert.are.same({}, buffer.hidden_ids())
|
|
end)
|
|
|
|
it('filter empty string clears filter', function()
|
|
store.load()
|
|
store.add({ description = 'Work task', category = 'Work' })
|
|
store.save()
|
|
pending.filter('cat:Work')
|
|
assert.are.equal(1, #buffer.filter_predicates())
|
|
pending.filter('')
|
|
assert.are.equal(0, #buffer.filter_predicates())
|
|
end)
|
|
|
|
it('filter predicates persist across set_filter calls', function()
|
|
store.load()
|
|
store.add({ description = 'Work task', category = 'Work' })
|
|
store.add({ description = 'Home task', category = 'Home' })
|
|
store.save()
|
|
pending.filter('cat:Work')
|
|
local preds = buffer.filter_predicates()
|
|
assert.are.equal(1, #preds)
|
|
assert.are.equal('cat:Work', preds[1])
|
|
local hidden = buffer.hidden_ids()
|
|
local tasks = store.active_tasks()
|
|
local home_task
|
|
for _, t in ipairs(tasks) do
|
|
if t.category == 'Home' then home_task = t end
|
|
end
|
|
assert.is_true(hidden[home_task.id])
|
|
end)
|
|
end)
|
|
|
|
describe('diff.apply with hidden_ids', function()
|
|
it('does not mark hidden tasks as deleted', function()
|
|
store.load()
|
|
store.add({ description = 'Visible task' })
|
|
store.add({ description = 'Hidden task' })
|
|
store.save()
|
|
local tasks = store.active_tasks()
|
|
local hidden_task
|
|
for _, t in ipairs(tasks) do
|
|
if t.description == 'Hidden task' then hidden_task = t end
|
|
end
|
|
local hidden_ids = { [hidden_task.id] = true }
|
|
local lines = {
|
|
'/1/- [ ] Visible task',
|
|
}
|
|
diff.apply(lines, hidden_ids)
|
|
store.unload()
|
|
store.load()
|
|
local hidden = store.get(hidden_task.id)
|
|
assert.are.equal('pending', hidden.status)
|
|
end)
|
|
|
|
it('marks tasks deleted when not hidden and not in buffer', function()
|
|
store.load()
|
|
store.add({ description = 'Keep task' })
|
|
store.add({ description = 'Delete task' })
|
|
store.save()
|
|
local tasks = store.active_tasks()
|
|
local keep_task, delete_task
|
|
for _, t in ipairs(tasks) do
|
|
if t.description == 'Keep task' then keep_task = t end
|
|
if t.description == 'Delete task' then delete_task = t end
|
|
end
|
|
local lines = {
|
|
'/' .. keep_task.id .. '/- [ ] Keep task',
|
|
}
|
|
diff.apply(lines, {})
|
|
store.unload()
|
|
store.load()
|
|
local deleted = store.get(delete_task.id)
|
|
assert.are.equal('deleted', deleted.status)
|
|
end)
|
|
|
|
it('strips FILTER: line before parsing', function()
|
|
store.load()
|
|
store.add({ description = 'My task' })
|
|
store.save()
|
|
local tasks = store.active_tasks()
|
|
local task = tasks[1]
|
|
local lines = {
|
|
'FILTER: cat:Work',
|
|
'/' .. task.id .. '/- [ ] My task',
|
|
}
|
|
diff.apply(lines, {})
|
|
store.unload()
|
|
store.load()
|
|
local t = store.get(task.id)
|
|
assert.are.equal('pending', t.status)
|
|
end)
|
|
|
|
it('parse_buffer skips FILTER: header line', function()
|
|
local lines = {
|
|
'FILTER: overdue',
|
|
'/1/- [ ] A task',
|
|
}
|
|
local result = diff.parse_buffer(lines)
|
|
assert.are.equal(1, #result)
|
|
assert.are.equal('task', result[1].type)
|
|
assert.are.equal('A task', result[1].description)
|
|
end)
|
|
end)
|
|
end)
|