This commit is contained in:
NoM0Re
2025-07-22 16:49:18 +02:00
committed by GitHub
parent d2b59a3f88
commit d6ae3e020b
47 changed files with 1056 additions and 192 deletions
+347
View File
@@ -0,0 +1,347 @@
local AddonName = ...
local Private = select(2, ...)
Private.Features:Register({
id = "undo",
autoEnable = {"dev", "pr", "alpha"},
enabled = true,
persist = true,
})
local TimeMachine = {
next = {
forward = {},
backward = {}
},
transaction = false,
changes = {},
actions = {},
effects = {},
index = 0,
sub = Private.CreateSubscribableObject(),
}
Private.TimeMachine = TimeMachine
local function resolveKey(data, path)
if type(path) ~= 'table' then
return data, path
end
local tbl = data
local i = 1
while i < #path do
if tbl[path[i]] == nil then
tbl[path[i]] = {}
elseif type(tbl[path[i]]) ~= 'table' then
error("Path is not valid: " .. table.concat(path, '.') .. " at " .. path[i])
end
tbl = tbl[path[i]]
i = i + 1
end
return tbl, path[#path]
end
local function copy(tbl, key)
if type(tbl[key]) == "table" then
return CopyTable(tbl[key])
else
return tbl[key]
end
end
function TimeMachine:RegisterEffect(tag, func, idempotent)
if self.effects[tag] then
error("Effect already registered: " .. tag)
end
self.effects[tag] = {
idempotent = idempotent,
func = func
}
end
TimeMachine:RegisterEffect("add", function(uid, data)
Private.Add(data)
end, true)
TimeMachine:RegisterEffect("options_cu", function(uid, data)
if WeakAuras.IsOptionsOpen() then
WeakAuras.ClearAndUpdateOptions(data.id, true)
end
end, true)
function TimeMachine:RegisterAction(actionType, actor, inverter, autoEffects)
if self.actions[actionType] then
error("Action already registered: " .. actionType)
end
self.actions[actionType] = {
actor = actor,
inverter = inverter,
autoEffects = autoEffects,
}
end
TimeMachine:RegisterAction("none",
function(_data, _path)
end,
function(_data, path)
return 'none', path, nil
end
)
TimeMachine:RegisterAction("set",
function(data, path, value)
local tbl, key = resolveKey(data, path)
tbl[key] = value
end,
function(data, path)
local tbl, key = resolveKey(data, path)
return 'set', path, copy(tbl, key)
end,
{"add", "options_cu"}
)
TimeMachine:RegisterAction("setmany",
function(data, path, values)
local tbl, key = resolveKey(data, path)
for k, v in pairs(values) do
tbl[key][k] = v
end
end,
function(data, path, values)
local tbl, key = resolveKey(data, path)
local inverse = {}
for k, v in pairs(values) do
inverse[k] = copy(tbl[key], k)
end
return 'setmany', path, inverse
end,
{"add", "options_cu"}
)
TimeMachine:RegisterAction("insert",
function(data, path, payload)
local tbl, key = resolveKey(data, path)
if payload.index == nil then
table.insert(tbl[key], payload.value)
else
table.insert(tbl[key], payload.index, payload.value)
end
end,
function(data, path, payload)
return 'remove', path, payload.index
end,
{"add", "options_cu"}
)
TimeMachine:RegisterAction("remove",
function(data, path, payload)
local tbl, key = resolveKey(data, path)
if payload == nil then
table.remove(tbl[key])
else
table.remove(tbl[key], payload)
end
end,
function(data, path, payload)
local tbl, key = resolveKey(data, path)
return 'insert', path, {index = payload, value = copy(tbl[key], payload or #tbl[key])}
end,
{"add", "options_cu"}
)
TimeMachine:RegisterAction("swap",
function(data, path, payload)
local tbl, key = resolveKey(data, path)
tbl[key][payload[1]], tbl[key][payload[2]] = tbl[key][payload[2]], tbl[key][payload[1]]
end,
function(data, path, payload)
return 'swap', path, {payload[2], payload[1]}
end,
{"add", "options_cu"}
)
TimeMachine:RegisterAction("move",
function(data, path, payload)
local tbl, key = resolveKey(data, path)
local value = table.remove(tbl, payload[1])
table.insert(tbl[key], payload[2], value)
end,
function(data, path, payload)
return 'move', path, {payload[2], payload[1]}
end,
{"add", "options_cu"}
)
local function keyPathToString(path)
if type(path) == 'table' then
return table.concat(path, '.')
else
return path
end
end
local function invertEffects(effects)
local inverted = {}
for i = #effects, 1, -1 do
table.insert(inverted, effects[i])
end
return inverted
end
function TimeMachine:StartTransaction()
if self.transaction then
WeakAuras.prettyPrint("If you're reading this, a time machine transaction was started, but there was already one in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
self:Reject()
end
self.transaction = true
end
function TimeMachine:Append(record)
local action = self.actions[record.actionType]
Private.DebugPrint("Forward action", record.actionType, "for", record.uid, "at", keyPathToString(record.path), "with", record.payload)
if not action then
error("No action for actionType: " .. record.actionType)
end
local inverter = action.inverter
if not inverter then
error("No inverter for action: " .. record.actionType)
end
local actionType, path, payload = inverter(Private.GetDataByUID(record.uid), record.path, record.payload)
local inverseRecord = {
uid = record.uid,
actionType = actionType,
path = path,
payload = payload,
suppressAutoEffects = record.suppressAutoEffects and CopyTable(record.suppressAutoEffects) or nil,
effects = record.effects and invertEffects(record.effects) or nil,
}
Private.DebugPrint("Backward action", actionType, "for", record.uid, "at", keyPathToString(path), "with", payload)
table.insert(self.next.forward, record)
table.insert(self.next.backward, 1, inverseRecord)
if not self.transaction then
self:Commit(true)
end
end
function TimeMachine:AppendMany(records)
local commit = false
if not self.transaction then
self:StartTransaction()
commit = true
end
for _, record in ipairs(records) do
self:Append(record)
end
if commit then
self:Commit()
end
end
function TimeMachine:Reject()
self.next = {
forward = {},
backward = {}
}
self.transaction = false
end
function TimeMachine:Commit(instant)
if not self.transaction and not instant then
WeakAuras.prettyPrint("If you're reading this, a time machine transaction was committed, but there was no transaction in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
return
end
while self.index < #self.changes do
table.remove(self.changes)
end
table.insert(self.changes, self.next)
self.next = {
forward = {},
backward = {}
}
self.transaction = false
return self:StepForward()
end
function TimeMachine:Apply(records, delayedEffects)
for _, record in ipairs(records) do
local action = self.actions[record.actionType]
if not action then
error("No action for actionType: " .. record.actionType)
end
local data = Private.GetDataByUID(record.uid)
action.actor(data, record.path, record.payload)
if action.autoEffects or record.effects then
local effects = {}
if action.autoEffects then
for _, effect in ipairs(action.autoEffects) do
if not record.suppressAutoEffects or not record.suppressAutoEffects[effect] then
table.insert(effects, effect)
end
end
end
if record.effects then
for _, effect in ipairs(record.effects) do
table.insert(effects, effect)
end
end
for _, effectType in ipairs(effects) do
local effect = self.effects[effectType]
if not effect then
error("No effect for effectType: " .. effect)
end
if not delayedEffects or not effect.idempotent then
if not record.effects or record.suppressAutoEffects then
effect.func(record.uid, data)
end
else
delayedEffects[record.uid] = delayedEffects[record.uid] or {}
delayedEffects[record.uid][effectType] = true
end
end
end
end
return delayedEffects
end
function TimeMachine:StepForward()
if self.index < #self.changes then
self.index = self.index + 1
self:Apply(self.changes[self.index].forward)
if self.sub:HasSubscribers("Step") then
self.sub:Notify("Step", self.index)
end
end
end
function TimeMachine:StepBackward()
if self.index > 0 then
self:Apply(self.changes[self.index].backward)
self.index = self.index - 1
if self.sub:HasSubscribers("Step") then
self.sub:Notify("Step", self.index)
end
end
end
--- much safer than the name suggests!
function TimeMachine:DestroyTheUniverse(id)
if self.transaction then
WeakAuras.prettyPrint("If you're reading this, a time machine transaction was destroyed, but there was one in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
self:Reject()
end
if #self.changes > 0 then
Private.DebugPrint(string.format("Destroying the universe where %i change(s) happpened, because an unexpected change happened to %q.", #self.changes, id))
end
self.changes = {}
self.index = 0
if self.sub:HasSubscribers("Step") then
self.sub:Notify("Step", self.index)
end
end
function TimeMachine:DescribeNext()
return self.changes[self.index + 1] and self.changes[self.index + 1].forward
end
function TimeMachine:DescribePrevious()
return self.changes[self.index] and self.changes[self.index].backward
end