feat(diff): disallow editing done tasks by default

Problem: Done tasks could be freely edited in the buffer, leading to
accidental modifications of completed work.

Solution: Add a `lock_done` config option (default `true`) and a guard
in `diff.apply()` that rejects field changes to done tasks unless the
user toggles the checkbox back to pending first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Barrett Ruth 2026-03-10 22:19:15 -04:00
parent e62f2f818c
commit ea8ba7f44c
3 changed files with 148 additions and 76 deletions

View file

@ -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',

View file

@ -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