diff --git a/lua/pending/config.lua b/lua/pending/config.lua index b1ab639..6942c1b 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -100,6 +100,7 @@ ---@field view pending.ViewConfig ---@field max_priority? integer ---@field sync? pending.SyncConfig +---@field lock_done? boolean ---@field forge? pending.ForgeConfig ---@field icons pending.Icons @@ -114,6 +115,7 @@ local defaults = { date_syntax = 'due', recur_syntax = 'rec', someday_date = '9999-12-30', + lock_done = true, max_priority = 3, view = { default = 'category', diff --git a/lua/pending/diff.lua b/lua/pending/diff.lua index 29c292b..a213584 100644 --- a/lua/pending/diff.lua +++ b/lua/pending/diff.lua @@ -1,5 +1,6 @@ local config = require('pending.config') local forge = require('pending.forge') +local log = require('pending.log') local parse = require('pending.parse') ---@class pending.ParsedEntry @@ -102,14 +103,91 @@ function M.apply(lines, s, hidden_ids) local order_counter = 0 for _, entry in ipairs(parsed) do - if entry.type ~= 'task' then - goto continue - end + if entry.type == 'task' then + order_counter = order_counter + 1 - order_counter = order_counter + 1 - - if entry.id and old_by_id[entry.id] then - if seen_ids[entry.id] then + if entry.id and old_by_id[entry.id] then + if seen_ids[entry.id] then + s:add({ + description = entry.description, + category = entry.category, + priority = entry.priority, + due = entry.due, + recur = entry.rec, + recur_mode = entry.rec_mode, + order = order_counter, + _extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil, + }) + else + seen_ids[entry.id] = true + local task = old_by_id[entry.id] + if + config.get().lock_done + and task.status == 'done' + and entry.status == 'done' + then + if task.order ~= order_counter then + task.order = order_counter + task.modified = now + end + log.warn('cannot edit done task — toggle status first') + else + local changed = false + if task.description ~= entry.description then + task.description = entry.description + changed = true + end + if task.category ~= entry.category then + task.category = entry.category + changed = true + end + if entry.priority == 0 and task.priority > 0 then + task.priority = 0 + changed = true + elseif entry.priority > 0 and task.priority == 0 then + task.priority = entry.priority + changed = true + end + if entry.due ~= nil and task.due ~= entry.due then + task.due = entry.due + changed = true + end + if entry.rec ~= nil then + if task.recur ~= entry.rec then + task.recur = entry.rec + changed = true + end + if task.recur_mode ~= entry.rec_mode then + task.recur_mode = entry.rec_mode + changed = true + end + end + if entry.forge_ref ~= nil then + if not task._extra then + task._extra = {} + end + task._extra._forge_ref = entry.forge_ref + changed = true + end + if entry.status and task.status ~= entry.status then + task.status = entry.status + if entry.status == 'done' then + task['end'] = now + else + task['end'] = nil + end + changed = true + end + if task.order ~= order_counter then + task.order = order_counter + changed = true + end + if changed then + task.modified = now + end + end + end + else s:add({ description = entry.description, category = entry.category, @@ -120,77 +198,8 @@ function M.apply(lines, s, hidden_ids) order = order_counter, _extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil, }) - else - seen_ids[entry.id] = true - local task = old_by_id[entry.id] - local changed = false - if task.description ~= entry.description then - task.description = entry.description - changed = true - end - if task.category ~= entry.category then - task.category = entry.category - changed = true - end - if entry.priority == 0 and task.priority > 0 then - task.priority = 0 - changed = true - elseif entry.priority > 0 and task.priority == 0 then - task.priority = entry.priority - changed = true - end - if entry.due ~= nil and task.due ~= entry.due then - task.due = entry.due - changed = true - end - if entry.rec ~= nil then - if task.recur ~= entry.rec then - task.recur = entry.rec - changed = true - end - if task.recur_mode ~= entry.rec_mode then - task.recur_mode = entry.rec_mode - changed = true - end - end - if entry.forge_ref ~= nil then - if not task._extra then - task._extra = {} - end - task._extra._forge_ref = entry.forge_ref - changed = true - end - if entry.status and task.status ~= entry.status then - task.status = entry.status - if entry.status == 'done' then - task['end'] = now - else - task['end'] = nil - end - changed = true - end - if task.order ~= order_counter then - task.order = order_counter - changed = true - end - if changed then - task.modified = now - end end - else - s:add({ - description = entry.description, - category = entry.category, - priority = entry.priority, - due = entry.due, - recur = entry.rec, - recur_mode = entry.rec_mode, - order = order_counter, - _extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil, - }) end - - ::continue:: end for id, task in pairs(old_by_id) do diff --git a/spec/diff_spec.lua b/spec/diff_spec.lua index 01d8aac..c897df1 100644 --- a/spec/diff_spec.lua +++ b/spec/diff_spec.lua @@ -287,5 +287,66 @@ describe('diff', function() local task = s:get(1) assert.are.equal(0, task.priority) end) + + it('rejects editing description of a done task', function() + local t = s:add({ description = 'Finished work', status = 'done' }) + t['end'] = '2026-03-01T00:00:00Z' + s:save() + local lines = { + '# Todo', + '/1/- [x] Changed description', + } + diff.apply(lines, s) + s:load() + local task = s:get(1) + assert.are.equal('Finished work', task.description) + assert.are.equal('done', task.status) + end) + + it('allows toggling done task back to pending', function() + local t = s:add({ description = 'Finished work', status = 'done' }) + t['end'] = '2026-03-01T00:00:00Z' + s:save() + local lines = { + '# Todo', + '/1/- [ ] Finished work', + } + diff.apply(lines, s) + s:load() + local task = s:get(1) + assert.are.equal('pending', task.status) + end) + + it('allows editing done task when lock_done is false', function() + local cfg = require('pending.config') + vim.g.pending = { lock_done = false } + cfg.reset() + local t = s:add({ description = 'Finished work', status = 'done' }) + t['end'] = '2026-03-01T00:00:00Z' + s:save() + local lines = { + '# Todo', + '/1/- [x] Changed description', + } + diff.apply(lines, s) + s:load() + local task = s:get(1) + assert.are.equal('Changed description', task.description) + vim.g.pending = {} + cfg.reset() + end) + + it('does not affect editing of pending tasks', function() + s:add({ description = 'Active task' }) + s:save() + local lines = { + '# Todo', + '/1/- [ ] Updated active task', + } + diff.apply(lines, s) + s:load() + local task = s:get(1) + assert.are.equal('Updated active task', task.description) + end) end) end)