feat(filter): oil-like editable filter line (#43)
* feat(filter): oil-like editable filter line with predicate dispatch 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. * ci: format
This commit is contained in:
parent
3da23c924a
commit
dcb6a4781d
7 changed files with 526 additions and 8 deletions
|
|
@ -268,6 +268,30 @@ COMMANDS *pending-commands*
|
||||||
|
|
||||||
`gcal` Google Calendar one-way push. See |pending-gcal|.
|
`gcal` Google Calendar one-way push. See |pending-gcal|.
|
||||||
|
|
||||||
|
*:Pending-filter*
|
||||||
|
:Pending filter {predicates}
|
||||||
|
Apply a filter to the task buffer. {predicates} is a space-separated list
|
||||||
|
of one or more predicate tokens. Only tasks matching all predicates (AND
|
||||||
|
semantics) are shown. Hidden tasks are not deleted — they are preserved in
|
||||||
|
the store and reappear when the filter is cleared. >vim
|
||||||
|
:Pending filter cat:Work
|
||||||
|
:Pending filter overdue
|
||||||
|
:Pending filter cat:Work overdue
|
||||||
|
:Pending filter priority
|
||||||
|
:Pending filter clear
|
||||||
|
<
|
||||||
|
When a filter is active the buffer's first line shows: >
|
||||||
|
FILTER: cat:Work overdue
|
||||||
|
<
|
||||||
|
The user can edit this line inline and `:w` to change the active filter.
|
||||||
|
Deleting the `FILTER:` line entirely and saving clears the filter.
|
||||||
|
`:Pending filter clear` also clears the filter programmatically.
|
||||||
|
|
||||||
|
Tab completion after `:Pending filter ` lists available predicates and
|
||||||
|
category values. Already-used predicates are excluded from completions.
|
||||||
|
|
||||||
|
See |pending-filters| for the full list of supported predicates.
|
||||||
|
|
||||||
*:Pending-undo*
|
*:Pending-undo*
|
||||||
:Pending undo
|
:Pending undo
|
||||||
Undo the last `:w` save, restoring the task store to its previous state.
|
Undo the last `:w` save, restoring the task store to its previous state.
|
||||||
|
|
@ -421,6 +445,47 @@ Queue view: ~ *pending-view-queue*
|
||||||
text alongside the due date virtual text so tasks remain identifiable
|
text alongside the due date virtual text so tasks remain identifiable
|
||||||
across categories. The buffer is named `pending://queue`.
|
across categories. The buffer is named `pending://queue`.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
FILTERS *pending-filters*
|
||||||
|
|
||||||
|
Filters narrow the task buffer to a subset of tasks without deleting any data.
|
||||||
|
Hidden tasks are preserved in the store and reappear when the filter is
|
||||||
|
cleared. Filter state is session-local — it does not persist across Neovim
|
||||||
|
restarts.
|
||||||
|
|
||||||
|
Set a filter with |:Pending-filter| or by editing the `FILTER:` line: >vim
|
||||||
|
:Pending filter cat:Work overdue
|
||||||
|
<
|
||||||
|
|
||||||
|
Multiple predicates are separated by spaces and combined with AND logic — a
|
||||||
|
task must match every predicate to be shown.
|
||||||
|
|
||||||
|
Available predicates: ~
|
||||||
|
|
||||||
|
`cat:X` Show only tasks whose category is exactly `X`. Tasks with no
|
||||||
|
category (assigned to `default_category`) are hidden unless
|
||||||
|
`default_category` matches `X`.
|
||||||
|
|
||||||
|
`overdue` Show only pending tasks with a due date strictly before today.
|
||||||
|
|
||||||
|
`today` Show only pending tasks with a due date equal to today.
|
||||||
|
|
||||||
|
`priority` Show only tasks with priority > 0 (the `!` marker).
|
||||||
|
|
||||||
|
`clear` Special value for |:Pending-filter| — clears the active filter
|
||||||
|
and shows all tasks.
|
||||||
|
|
||||||
|
FILTER: line: ~ *pending-filter-line*
|
||||||
|
|
||||||
|
When a filter is active, the first line of the task buffer is: >
|
||||||
|
FILTER: cat:Work overdue
|
||||||
|
<
|
||||||
|
|
||||||
|
This line is editable. Write the buffer with `:w` to apply the updated
|
||||||
|
predicates. Deleting the `FILTER:` line and saving clears the filter. The
|
||||||
|
line is highlighted with |PendingFilter| and does not appear in the stored
|
||||||
|
task data.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
CONFIGURATION *pending-config*
|
CONFIGURATION *pending-config*
|
||||||
|
|
||||||
|
|
@ -760,6 +825,11 @@ PendingRecur Applied to the recurrence indicator virtual text shown
|
||||||
alongside due dates for recurring tasks.
|
alongside due dates for recurring tasks.
|
||||||
Default: links to `DiagnosticInfo`.
|
Default: links to `DiagnosticInfo`.
|
||||||
|
|
||||||
|
*PendingFilter*
|
||||||
|
PendingFilter Applied to the `FILTER:` header line shown at the top of
|
||||||
|
the buffer when a filter is active.
|
||||||
|
Default: links to `DiagnosticWarn`.
|
||||||
|
|
||||||
To override a group in your colorscheme or config: >lua
|
To override a group in your colorscheme or config: >lua
|
||||||
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
|
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
|
||||||
<
|
<
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ local current_view = nil
|
||||||
local _meta = {}
|
local _meta = {}
|
||||||
---@type table<integer, table<string, boolean>>
|
---@type table<integer, table<string, boolean>>
|
||||||
local _fold_state = {}
|
local _fold_state = {}
|
||||||
|
---@type string[]
|
||||||
|
local _filter_predicates = {}
|
||||||
|
---@type table<integer, true>
|
||||||
|
local _hidden_ids = {}
|
||||||
|
|
||||||
---@return pending.LineMeta[]
|
---@return pending.LineMeta[]
|
||||||
function M.meta()
|
function M.meta()
|
||||||
|
|
@ -37,6 +41,24 @@ function M.current_view_name()
|
||||||
return current_view
|
return current_view
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return string[]
|
||||||
|
function M.filter_predicates()
|
||||||
|
return _filter_predicates
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return table<integer, true>
|
||||||
|
function M.hidden_ids()
|
||||||
|
return _hidden_ids
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param predicates string[]
|
||||||
|
---@param hidden table<integer, true>
|
||||||
|
---@return nil
|
||||||
|
function M.set_filter(predicates, hidden)
|
||||||
|
_filter_predicates = predicates
|
||||||
|
_hidden_ids = hidden
|
||||||
|
end
|
||||||
|
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.clear_winid()
|
function M.clear_winid()
|
||||||
task_winid = nil
|
task_winid = nil
|
||||||
|
|
@ -124,7 +146,13 @@ local function apply_extmarks(bufnr, line_meta)
|
||||||
vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1)
|
||||||
for i, m in ipairs(line_meta) do
|
for i, m in ipairs(line_meta) do
|
||||||
local row = i - 1
|
local row = i - 1
|
||||||
if m.type == 'task' then
|
if m.type == 'filter' then
|
||||||
|
local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or ''
|
||||||
|
vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, {
|
||||||
|
end_col = #line,
|
||||||
|
hl_group = 'PendingFilter',
|
||||||
|
})
|
||||||
|
elseif m.type == 'task' then
|
||||||
local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue'
|
local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue'
|
||||||
local virt_parts = {}
|
local virt_parts = {}
|
||||||
if m.show_category and m.category then
|
if m.show_category and m.category then
|
||||||
|
|
@ -170,6 +198,7 @@ local function setup_highlights()
|
||||||
vim.api.nvim_set_hl(0, 'PendingDone', { link = 'Comment', default = true })
|
vim.api.nvim_set_hl(0, 'PendingDone', { link = 'Comment', default = true })
|
||||||
vim.api.nvim_set_hl(0, 'PendingPriority', { link = 'DiagnosticWarn', default = true })
|
vim.api.nvim_set_hl(0, 'PendingPriority', { link = 'DiagnosticWarn', default = true })
|
||||||
vim.api.nvim_set_hl(0, 'PendingRecur', { link = 'DiagnosticInfo', default = true })
|
vim.api.nvim_set_hl(0, 'PendingRecur', { link = 'DiagnosticInfo', default = true })
|
||||||
|
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
local function snapshot_folds(bufnr)
|
local function snapshot_folds(bufnr)
|
||||||
|
|
@ -225,7 +254,13 @@ function M.render(bufnr)
|
||||||
current_view = current_view or config.get().default_view
|
current_view = current_view or config.get().default_view
|
||||||
local view_label = current_view == 'priority' and 'queue' or current_view
|
local view_label = current_view == 'priority' and 'queue' or current_view
|
||||||
vim.api.nvim_buf_set_name(bufnr, 'pending://' .. view_label)
|
vim.api.nvim_buf_set_name(bufnr, 'pending://' .. view_label)
|
||||||
local tasks = store.active_tasks()
|
local all_tasks = store.active_tasks()
|
||||||
|
local tasks = {}
|
||||||
|
for _, task in ipairs(all_tasks) do
|
||||||
|
if not _hidden_ids[task.id] then
|
||||||
|
table.insert(tasks, task)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local lines, line_meta
|
local lines, line_meta
|
||||||
if current_view == 'priority' then
|
if current_view == 'priority' then
|
||||||
|
|
@ -234,6 +269,11 @@ function M.render(bufnr)
|
||||||
lines, line_meta = views.category_view(tasks)
|
lines, line_meta = views.category_view(tasks)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if #_filter_predicates > 0 then
|
||||||
|
table.insert(lines, 1, 'FILTER: ' .. table.concat(_filter_predicates, ' '))
|
||||||
|
table.insert(line_meta, 1, { type = 'filter' })
|
||||||
|
end
|
||||||
|
|
||||||
_meta = line_meta
|
_meta = line_meta
|
||||||
|
|
||||||
snapshot_folds(bufnr)
|
snapshot_folds(bufnr)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,13 @@ end
|
||||||
function M.parse_buffer(lines)
|
function M.parse_buffer(lines)
|
||||||
local result = {}
|
local result = {}
|
||||||
local current_category = nil
|
local current_category = nil
|
||||||
|
local start = 1
|
||||||
|
if lines[1] and lines[1]:match('^FILTER:') then
|
||||||
|
start = 2
|
||||||
|
end
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
for i = start, #lines do
|
||||||
|
local line = lines[i]
|
||||||
local id, body = line:match('^/(%d+)/(- %[.%] .*)$')
|
local id, body = line:match('^/(%d+)/(- %[.%] .*)$')
|
||||||
if not id then
|
if not id then
|
||||||
body = line:match('^(- %[.%] .*)$')
|
body = line:match('^(- %[.%] .*)$')
|
||||||
|
|
@ -65,8 +70,9 @@ function M.parse_buffer(lines)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param lines string[]
|
---@param lines string[]
|
||||||
|
---@param hidden_ids? table<integer, true>
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.apply(lines)
|
function M.apply(lines, hidden_ids)
|
||||||
local parsed = M.parse_buffer(lines)
|
local parsed = M.parse_buffer(lines)
|
||||||
local now = timestamp()
|
local now = timestamp()
|
||||||
local data = store.data()
|
local data = store.data()
|
||||||
|
|
@ -160,7 +166,7 @@ function M.apply(lines)
|
||||||
end
|
end
|
||||||
|
|
||||||
for id, task in pairs(old_by_id) do
|
for id, task in pairs(old_by_id) do
|
||||||
if not seen_ids[id] then
|
if not seen_ids[id] and not (hidden_ids and hidden_ids[id]) then
|
||||||
task.status = 'deleted'
|
task.status = 'deleted'
|
||||||
task['end'] = now
|
task['end'] = now
|
||||||
task.modified = now
|
task.modified = now
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,47 @@ function M.has_due()
|
||||||
return c.overdue > 0 or c.today > 0
|
return c.overdue > 0 or c.today > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param tasks pending.Task[]
|
||||||
|
---@param predicates string[]
|
||||||
|
---@return table<integer, true>
|
||||||
|
local function compute_hidden_ids(tasks, predicates)
|
||||||
|
if #predicates == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local hidden = {}
|
||||||
|
for _, task in ipairs(tasks) do
|
||||||
|
local visible = true
|
||||||
|
for _, pred in ipairs(predicates) do
|
||||||
|
local cat_val = pred:match('^cat:(.+)$')
|
||||||
|
if cat_val then
|
||||||
|
if task.category ~= cat_val then
|
||||||
|
visible = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
elseif pred == 'overdue' then
|
||||||
|
if not (task.status == 'pending' and task.due and parse.is_overdue(task.due)) then
|
||||||
|
visible = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
elseif pred == 'today' then
|
||||||
|
if not (task.status == 'pending' and task.due and parse.is_today(task.due)) then
|
||||||
|
visible = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
elseif pred == 'priority' then
|
||||||
|
if not (task.priority and task.priority > 0) then
|
||||||
|
visible = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not visible then
|
||||||
|
hidden[task.id] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return hidden
|
||||||
|
end
|
||||||
|
|
||||||
---@return integer bufnr
|
---@return integer bufnr
|
||||||
function M.open()
|
function M.open()
|
||||||
local bufnr = buffer.open()
|
local bufnr = buffer.open()
|
||||||
|
|
@ -102,6 +143,30 @@ function M.open()
|
||||||
return bufnr
|
return bufnr
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param pred_str string
|
||||||
|
---@return nil
|
||||||
|
function M.filter(pred_str)
|
||||||
|
if pred_str == 'clear' or pred_str == '' then
|
||||||
|
buffer.set_filter({}, {})
|
||||||
|
local bufnr = buffer.bufnr()
|
||||||
|
if bufnr then
|
||||||
|
buffer.render(bufnr)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local predicates = {}
|
||||||
|
for word in pred_str:gmatch('%S+') do
|
||||||
|
table.insert(predicates, word)
|
||||||
|
end
|
||||||
|
local tasks = store.active_tasks()
|
||||||
|
local hidden = compute_hidden_ids(tasks, predicates)
|
||||||
|
buffer.set_filter(predicates, hidden)
|
||||||
|
local bufnr = buffer.bufnr()
|
||||||
|
if bufnr then
|
||||||
|
buffer.render(bufnr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@return nil
|
---@return nil
|
||||||
function M._setup_autocmds(bufnr)
|
function M._setup_autocmds(bufnr)
|
||||||
|
|
@ -246,13 +311,27 @@ end
|
||||||
---@return nil
|
---@return nil
|
||||||
function M._on_write(bufnr)
|
function M._on_write(bufnr)
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
local predicates = buffer.filter_predicates()
|
||||||
|
if lines[1] and lines[1]:match('^FILTER:') then
|
||||||
|
local pred_str = lines[1]:match('^FILTER:%s*(.*)$') or ''
|
||||||
|
predicates = {}
|
||||||
|
for word in pred_str:gmatch('%S+') do
|
||||||
|
table.insert(predicates, word)
|
||||||
|
end
|
||||||
|
lines = vim.list_slice(lines, 2)
|
||||||
|
elseif #buffer.filter_predicates() > 0 then
|
||||||
|
predicates = {}
|
||||||
|
end
|
||||||
|
local tasks = store.active_tasks()
|
||||||
|
local hidden = compute_hidden_ids(tasks, predicates)
|
||||||
|
buffer.set_filter(predicates, hidden)
|
||||||
local snapshot = store.snapshot()
|
local snapshot = store.snapshot()
|
||||||
local stack = store.undo_stack()
|
local stack = store.undo_stack()
|
||||||
table.insert(stack, snapshot)
|
table.insert(stack, snapshot)
|
||||||
if #stack > UNDO_MAX then
|
if #stack > UNDO_MAX then
|
||||||
table.remove(stack, 1)
|
table.remove(stack, 1)
|
||||||
end
|
end
|
||||||
diff.apply(lines)
|
diff.apply(lines, hidden)
|
||||||
M._recompute_counts()
|
M._recompute_counts()
|
||||||
buffer.render(bufnr)
|
buffer.render(bufnr)
|
||||||
end
|
end
|
||||||
|
|
@ -718,6 +797,8 @@ function M.command(args)
|
||||||
M.archive(d)
|
M.archive(d)
|
||||||
elseif cmd == 'due' then
|
elseif cmd == 'due' then
|
||||||
M.due()
|
M.due()
|
||||||
|
elseif cmd == 'filter' then
|
||||||
|
M.filter(rest)
|
||||||
elseif cmd == 'undo' then
|
elseif cmd == 'undo' then
|
||||||
M.undo_write()
|
M.undo_write()
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ local config = require('pending.config')
|
||||||
local parse = require('pending.parse')
|
local parse = require('pending.parse')
|
||||||
|
|
||||||
---@class pending.LineMeta
|
---@class pending.LineMeta
|
||||||
---@field type 'task'|'header'|'blank'
|
---@field type 'task'|'header'|'blank'|'filter'
|
||||||
---@field id? integer
|
---@field id? integer
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field raw_due? string
|
---@field raw_due? string
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,34 @@ end, {
|
||||||
bar = true,
|
bar = true,
|
||||||
nargs = '*',
|
nargs = '*',
|
||||||
complete = function(arg_lead, cmd_line)
|
complete = function(arg_lead, cmd_line)
|
||||||
local subcmds = { 'add', 'archive', 'due', 'edit', 'sync', 'undo' }
|
local subcmds = { 'add', 'archive', 'due', 'edit', 'filter', 'sync', 'undo' }
|
||||||
if not cmd_line:match('^Pending%s+%S') then
|
if not cmd_line:match('^Pending%s+%S') then
|
||||||
return filter_candidates(arg_lead, subcmds)
|
return filter_candidates(arg_lead, subcmds)
|
||||||
end
|
end
|
||||||
|
if cmd_line:match('^Pending%s+filter') then
|
||||||
|
local after_filter = cmd_line:match('^Pending%s+filter%s+(.*)') or ''
|
||||||
|
local used = {}
|
||||||
|
for word in after_filter:gmatch('%S+') do
|
||||||
|
used[word] = true
|
||||||
|
end
|
||||||
|
local candidates = { 'clear', 'overdue', 'today', 'priority' }
|
||||||
|
local store = require('pending.store')
|
||||||
|
store.load()
|
||||||
|
local seen = {}
|
||||||
|
for _, task in ipairs(store.active_tasks()) do
|
||||||
|
if task.category and not seen[task.category] then
|
||||||
|
seen[task.category] = true
|
||||||
|
table.insert(candidates, 'cat:' .. task.category)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local filtered = {}
|
||||||
|
for _, c in ipairs(candidates) do
|
||||||
|
if not used[c] and (arg_lead == '' or c:find(arg_lead, 1, true) == 1) then
|
||||||
|
table.insert(filtered, c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return filtered
|
||||||
|
end
|
||||||
if cmd_line:match('^Pending%s+edit') then
|
if cmd_line:match('^Pending%s+edit') then
|
||||||
return complete_edit(arg_lead, cmd_line)
|
return complete_edit(arg_lead, cmd_line)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
297
spec/filter_spec.lua
Normal file
297
spec/filter_spec.lua
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
require('spec.helpers')
|
||||||
|
|
||||||
|
local config = require('pending.config')
|
||||||
|
local diff = require('pending.diff')
|
||||||
|
local store = require('pending.store')
|
||||||
|
|
||||||
|
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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue