From c03412837d9ed5e0ae60c50794107ff64343d41c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 24 Feb 2026 15:09:36 -0500 Subject: [PATCH] feat(diff): add buffer-to-store diff algorithm Problem: need to reconcile buffer edits against the JSON store on :w, handling creates, deletes, updates, reorders, and duplicate IDs from yank/paste. Solution: add diff module that parses buffer lines, matches against stored tasks by ID, creates new tasks for unknown or duplicate IDs, marks removed tasks as deleted, and updates changed fields. --- lua/todo/diff.lua | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 lua/todo/diff.lua diff --git a/lua/todo/diff.lua b/lua/todo/diff.lua new file mode 100644 index 0000000..902c064 --- /dev/null +++ b/lua/todo/diff.lua @@ -0,0 +1,149 @@ +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 + if line == '' then + table.insert(result, { type = 'blank', lnum = i }) + elseif line:match('^%S') then + current_category = line + table.insert(result, { type = 'header', category = line, lnum = i }) + else + local id, body = line:match('^/(%d+)/( .+)$') + if not id then + body = line:match('^( .+)$') + end + if 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 + end + 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