Problem: parse_buffer classified /id/-prefixed task lines as headers because '/' matches the '^%S' header pattern. Store timestamp test was flaky when add and update ran within the same second. Solution: check for task line patterns (id prefix or 2-space indent) before falling through to the header branch. Backdate the initial modified timestamp in the store update test.
147 lines
3.5 KiB
Lua
147 lines
3.5 KiB
Lua
local config = require('todo.config')
|
|
local parse = require('todo.parse')
|
|
local store = require('todo.store')
|
|
|
|
---@class todo.ParsedEntry
|
|
---@field type 'task'|'header'|'blank'
|
|
---@field id? integer
|
|
---@field description? string
|
|
---@field priority? integer
|
|
---@field category? string
|
|
---@field due? string
|
|
---@field lnum integer
|
|
|
|
---@class todo.diff
|
|
local M = {}
|
|
|
|
---@return string
|
|
local function timestamp()
|
|
return os.date('!%Y-%m-%dT%H:%M:%SZ')
|
|
end
|
|
|
|
---@param lines string[]
|
|
---@return todo.ParsedEntry[]
|
|
function M.parse_buffer(lines)
|
|
local result = {}
|
|
local current_category = nil
|
|
|
|
for i, line in ipairs(lines) do
|
|
local id, body = line:match('^/(%d+)/( .+)$')
|
|
if not id then
|
|
body = line:match('^( .+)$')
|
|
end
|
|
if line == '' then
|
|
table.insert(result, { type = 'blank', lnum = i })
|
|
elseif id or body then
|
|
local stripped = body:match('^ (.+)$') or body
|
|
local priority = 0
|
|
if stripped:match('^! ') then
|
|
priority = 1
|
|
stripped = stripped:sub(3)
|
|
end
|
|
local description, metadata = parse.body(stripped)
|
|
if description and description ~= '' then
|
|
table.insert(result, {
|
|
type = 'task',
|
|
id = id and tonumber(id) or nil,
|
|
description = description,
|
|
priority = priority,
|
|
category = metadata.cat or current_category or config.get().default_category,
|
|
due = metadata.due,
|
|
lnum = i,
|
|
})
|
|
end
|
|
elseif line:match('^%S') then
|
|
current_category = line
|
|
table.insert(result, { type = 'header', category = line, lnum = i })
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
---@param lines string[]
|
|
function M.apply(lines)
|
|
local parsed = M.parse_buffer(lines)
|
|
local now = timestamp()
|
|
local data = store.data()
|
|
|
|
local old_by_id = {}
|
|
for _, task in ipairs(data.tasks) do
|
|
if task.status ~= 'deleted' then
|
|
old_by_id[task.id] = task
|
|
end
|
|
end
|
|
|
|
local seen_ids = {}
|
|
local order_counter = 0
|
|
|
|
for _, entry in ipairs(parsed) do
|
|
if entry.type ~= 'task' then
|
|
goto continue
|
|
end
|
|
|
|
order_counter = order_counter + 1
|
|
|
|
if entry.id and old_by_id[entry.id] then
|
|
if seen_ids[entry.id] then
|
|
store.add({
|
|
description = entry.description,
|
|
category = entry.category,
|
|
priority = entry.priority,
|
|
due = entry.due,
|
|
order = order_counter,
|
|
})
|
|
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 task.priority ~= entry.priority then
|
|
task.priority = entry.priority
|
|
changed = true
|
|
end
|
|
if task.due ~= entry.due then
|
|
task.due = entry.due
|
|
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
|
|
store.add({
|
|
description = entry.description,
|
|
category = entry.category,
|
|
priority = entry.priority,
|
|
due = entry.due,
|
|
order = order_counter,
|
|
})
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
for id, task in pairs(old_by_id) do
|
|
if not seen_ids[id] then
|
|
task.status = 'deleted'
|
|
task['end'] = now
|
|
task.modified = now
|
|
end
|
|
end
|
|
|
|
store.save()
|
|
end
|
|
|
|
return M
|