pending.nvim/lua/todo/diff.lua
Barrett Ruth 79cef994ce 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.
2026-02-24 15:09:36 -05:00

149 lines
3.6 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
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