In 2016 I was frustrated by vscode vim mode undo/redo stack: when you activated undo it would process each individual character insertion/deletion event in sequence exactly as you typed it, e.g. if you type and delete the same character 100 times it would replay that exact sequence even if it was all one logical INSERT. Replay speed was slow enough (maybe 15-20 changes per second?) that it was excruciating to use.<p>So I rewrote it and added logic to merge adjacent changes [1] which helped immensely, enough that undo/redo felt instant again. It looks like the core merging logic was rewritten a few years ago [2] (which caused a regression, is imo less readable, and removed useful comments -_-) but the basic idea is still there today.<p>[1]: <a href="https://github.com/VSCodeVim/Vim/commit/0576f199cb7a765efb3cd7a7828fd19ff3af094b#diff-4f43ed31c28406a288f2c26932c9827db19761ce9449e7d6c24b2339b19a3080R100-R140" rel="nofollow">https://github.com/VSCodeVim/Vim/commit/0576f199cb7a765efb3c...</a><p>[2]: <a href="https://github.com/VSCodeVim/Vim/blame/v1.29.0/src/history/historyTracker.ts#L156-L201" rel="nofollow">https://github.com/VSCodeVim/Vim/blame/v1.29.0/src/history/h...</a>
In reality, you're gonna quickly run into battles with state's that are complex and unclonable - in flight fetch requests, circular references, webasm modules that have internal state, etc.<p>Then there are things where the browser already implements undo functionality like navigation (back button) and editing text boxes. Your undo is going to have to work nicely with those.<p>You'll either end up with a buggy undo implementation, or end up spending a lot of engineering time hunting corner cases.
In my C code, I implement undo/redo using write protection (mprotect).<p>First you mark all of your app memory as read only, so that you can define a SIGBUS handler when attempts are made to write to it.<p>Then when you detect a write to a page in the app memory, you append a copy of that page's memory to a stack, and then mark the page as writeable so that the page can be updated.<p>Then undo is simply a matter of popping the stack and overwriting the app memory with the copy stored on the stack entry.<p>This means you don't have to explicitly write "actions" for everything you want to undo/redo. All in-memory actions are handled automatically.<p>Obviously this means that things like FS changes or network IO changes aren't captured, but sometimes that's fine, and I'm sure there's a way to extend this scheme to handle those things somehow
If you get clever with things you can define actions with a single "apply_swap()" method which applies an edit and saves the old state (on the first call and subsequent odd calls), and reverts the edit and saves the new state (on the second call and subsequent even calls). This avoids having to write separate implementations for adding/removing items and redo/undo, and allows applying edits on a real-time thread with zero memory allocations (after constructing an edit action).
I think it's a clever and elegant solution. Obviously, ymmv depending on the environment, or what exactly you're trying to undo/redo. But I like that it thinks of two stacks. StructuredClone is not something I've ever used or am familiar with... I'm used to storing and later de-referencing pointers to anonymous functions. But in your use case, a clone makes a lot more sense, and I'm kinda looking forward to playing with that next time I have a mess of event handlers.
Ha! I wrote my own version of this as a web component[0]. Very handy thing to have, for web apps!<p>Nice to see it distilled into something so straightforward! Thanks for sharing!<p>[0] <a href="https://github.com/catapart/magnitce-action-history" rel="nofollow">https://github.com/catapart/magnitce-action-history</a>
This data structure is called the zipper, and the neat thing is that you can generalize this to more complicated types like trees: <a href="https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/huet-zipper.pdf" rel="nofollow">https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced...</a>
It's very nice, but it doesn't do system undo. You can shake iPhones to get an Undo/Redo dialog, for example.<p>So you can probably trap Meta-Z but some people use menus or the other affordances.
I did something similar the other day but I end up using a circular buffer of fixed size. Everytime you push a new state the oldest state gets discarded.