local config = require('pending.config') local parse = require('pending.parse') ---@class pending.ParsedEntry ---@field type 'task'|'header'|'blank' ---@field id? integer ---@field description? string ---@field priority? integer ---@field status? string ---@field category? string ---@field due? string ---@field rec? string ---@field rec_mode? string ---@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 = state_char == 'x' and 'done' or 'pending' 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, status = status, category = metadata.cat or current_category or config.get().default_category, due = metadata.due, rec = metadata.rec, rec_mode = metadata.rec_mode, 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 lines string[] ---@param s pending.Store ---@param hidden_ids? table ---@return nil function M.apply(lines, s, hidden_ids) local parsed = M.parse_buffer(lines) local now = timestamp() local data = s: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 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, }) 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 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.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, }) 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() end return M