local config = require('pending.config') local forge = require('pending.forge') local parse = require('pending.parse') ---@class pending.ParsedEntry ---@field type 'task'|'header'|'blank' ---@field id? integer ---@field description? string ---@field priority? integer ---@field status? pending.TaskStatus ---@field category? string ---@field due? string ---@field rec? string ---@field rec_mode? pending.RecurMode ---@field forge_ref? pending.ForgeRef ---@field lnum integer ---@class pending.diff local M = {} ---@return string local function timestamp() return os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]] end ---@param lines string[] ---@return pending.ParsedEntry[] 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] 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 state_char = body:match('^- %[(.-)%]') or ' ' local priority = state_char == '!' and 1 or 0 local status if state_char == 'x' then status = 'done' elseif state_char == '>' then status = 'wip' elseif state_char == '=' then status = 'blocked' else status = 'pending' end local description, metadata = parse.body(stripped) if description and description ~= '' then local refs = forge.find_refs(description) local forge_ref = refs[1] and refs[1].ref or nil table.insert(result, { type = 'task', id = id and tonumber(id) or nil, description = description, priority = priority, status = status, category = metadata.cat or current_category or config.get().default_category, due = metadata.due, rec = metadata.rec, rec_mode = metadata.rec_mode, forge_ref = forge_ref, lnum = i, }) end elseif line:match('^# (.+)$') then current_category = line:match('^# (.+)$') table.insert(result, { type = 'header', category = current_category, lnum = i }) end end return result end ---@param a? pending.ForgeRef ---@param b? pending.ForgeRef ---@return boolean local function refs_equal(a, b) if not a or not b then return false end return a.forge == b.forge and a.owner == b.owner and a.repo == b.repo and a.number == b.number end ---@param lines string[] ---@param s pending.Store ---@param hidden_ids? table ---@return pending.ForgeRef[] function M.apply(lines, s, hidden_ids) local parsed = M.parse_buffer(lines) local now = timestamp() local data = s:data() local new_refs = {} ---@type pending.ForgeRef[] 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 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, }) if entry.forge_ref then table.insert(new_refs, entry.forge_ref) end 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 local old_ref = task._extra and task._extra._forge_ref or nil if not refs_equal(old_ref, entry.forge_ref) then table.insert(new_refs, entry.forge_ref) end 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, }) if entry.forge_ref then table.insert(new_refs, entry.forge_ref) end end ::continue:: end for id, task in pairs(old_by_id) do if not seen_ids[id] and not (hidden_ids and hidden_ids[id]) then task.status = 'deleted' task['end'] = now task.modified = now end end s:save() return new_refs end return M