Your data can be represented in a Merkle-tree. You provide operations add-node, delete-node, move-node etc., on the Merkle-tree and build a command processor that takes as input a checkpointed Merkle-tree snapshot (could be empty when you start) and a log of operations (essentially an sequence of operations). The command processor then applies the series of tree operations in the log to build the final state of the tree. The special checkpoint operation saves a snapshot of the Merkle-tree.
This basic set of operations should allow you to do unlimited undo/redo/transaction playback capabilities.
Also see: Command Pattern, https://en.wikipedia.org/wiki/Command_pattern
It’s the number one feature I miss when I’m using another editor.
If you want to store something more like a diff, there are choices. For example, you could have code to undo each operation, but you can also implement undo by going back to some checkpoint and then recomputing all operations between the checkpoint and now. Latter consumes more CPU, but comparable memory and a lot less code surface for bugs.