diff --git a/doc/pending.txt b/doc/pending.txt index a1f8198..be369b5 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -268,30 +268,6 @@ COMMANDS *pending-commands* `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 Undo the last `:w` save, restoring the task store to its previous state. @@ -445,47 +421,6 @@ Queue view: ~ *pending-view-queue* text alongside the due date virtual text so tasks remain identifiable 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* @@ -825,11 +760,6 @@ PendingRecur Applied to the recurrence indicator virtual text shown alongside due dates for recurring tasks. 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 vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true }) < diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 0372ef6..a427b68 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -16,10 +16,6 @@ local current_view = nil local _meta = {} ---@type table> local _fold_state = {} ----@type string[] -local _filter_predicates = {} ----@type table -local _hidden_ids = {} ---@return pending.LineMeta[] function M.meta() @@ -41,24 +37,6 @@ function M.current_view_name() return current_view end ----@return string[] -function M.filter_predicates() - return _filter_predicates -end - ----@return table -function M.hidden_ids() - return _hidden_ids -end - ----@param predicates string[] ----@param hidden table ----@return nil -function M.set_filter(predicates, hidden) - _filter_predicates = predicates - _hidden_ids = hidden -end - ---@return nil function M.clear_winid() task_winid = nil @@ -146,13 +124,7 @@ local function apply_extmarks(bufnr, line_meta) vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1) for i, m in ipairs(line_meta) do local row = i - 1 - 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 + if m.type == 'task' then local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue' local virt_parts = {} if m.show_category and m.category then @@ -198,7 +170,6 @@ local function setup_highlights() 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, 'PendingRecur', { link = 'DiagnosticInfo', default = true }) - vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true }) end local function snapshot_folds(bufnr) @@ -254,13 +225,7 @@ function M.render(bufnr) current_view = current_view or config.get().default_view local view_label = current_view == 'priority' and 'queue' or current_view vim.api.nvim_buf_set_name(bufnr, 'pending://' .. view_label) - 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 tasks = store.active_tasks() local lines, line_meta if current_view == 'priority' then @@ -269,11 +234,6 @@ function M.render(bufnr) lines, line_meta = views.category_view(tasks) 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 snapshot_folds(bufnr) diff --git a/lua/pending/diff.lua b/lua/pending/diff.lua index 4fd83c3..daab788 100644 --- a/lua/pending/diff.lua +++ b/lua/pending/diff.lua @@ -27,13 +27,8 @@ end function M.parse_buffer(lines) local result = {} local current_category = nil - local start = 1 - if lines[1] and lines[1]:match('^FILTER:') then - start = 2 - end - for i = start, #lines do - local line = lines[i] + for i, line in ipairs(lines) do local id, body = line:match('^/(%d+)/(- %[.%] .*)$') if not id then body = line:match('^(- %[.%] .*)$') @@ -70,9 +65,8 @@ function M.parse_buffer(lines) end ---@param lines string[] ----@param hidden_ids? table ---@return nil -function M.apply(lines, hidden_ids) +function M.apply(lines) local parsed = M.parse_buffer(lines) local now = timestamp() local data = store.data() @@ -166,7 +160,7 @@ function M.apply(lines, hidden_ids) end for id, task in pairs(old_by_id) do - if not seen_ids[id] and not (hidden_ids and hidden_ids[id]) then + if not seen_ids[id] then task.status = 'deleted' task['end'] = now task.modified = now diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 7409fb5..cae13a9 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -94,47 +94,6 @@ function M.has_due() return c.overdue > 0 or c.today > 0 end ----@param tasks pending.Task[] ----@param predicates string[] ----@return table -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 function M.open() local bufnr = buffer.open() @@ -143,30 +102,6 @@ function M.open() return bufnr 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 ---@return nil function M._setup_autocmds(bufnr) @@ -311,27 +246,13 @@ end ---@return nil function M._on_write(bufnr) 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 stack = store.undo_stack() table.insert(stack, snapshot) if #stack > UNDO_MAX then table.remove(stack, 1) end - diff.apply(lines, hidden) + diff.apply(lines) M._recompute_counts() buffer.render(bufnr) end @@ -797,8 +718,6 @@ function M.command(args) M.archive(d) elseif cmd == 'due' then M.due() - elseif cmd == 'filter' then - M.filter(rest) elseif cmd == 'undo' then M.undo_write() else diff --git a/lua/pending/views.lua b/lua/pending/views.lua index 286db9a..32cc2fb 100644 --- a/lua/pending/views.lua +++ b/lua/pending/views.lua @@ -2,7 +2,7 @@ local config = require('pending.config') local parse = require('pending.parse') ---@class pending.LineMeta ----@field type 'task'|'header'|'blank'|'filter' +---@field type 'task'|'header'|'blank' ---@field id? integer ---@field due? string ---@field raw_due? string diff --git a/plugin/pending.lua b/plugin/pending.lua index be546c5..839b351 100644 --- a/plugin/pending.lua +++ b/plugin/pending.lua @@ -164,34 +164,10 @@ end, { bar = true, nargs = '*', complete = function(arg_lead, cmd_line) - local subcmds = { 'add', 'archive', 'due', 'edit', 'filter', 'sync', 'undo' } + local subcmds = { 'add', 'archive', 'due', 'edit', 'sync', 'undo' } if not cmd_line:match('^Pending%s+%S') then return filter_candidates(arg_lead, subcmds) 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 return complete_edit(arg_lead, cmd_line) end diff --git a/spec/filter_spec.lua b/spec/filter_spec.lua deleted file mode 100644 index 8756c5f..0000000 --- a/spec/filter_spec.lua +++ /dev/null @@ -1,297 +0,0 @@ -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)