TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

Writing a tiny undo/redo stack in JavaScript

170 pointsby julikabout 2 months ago

19 comments

infogulchabout 2 months ago
In 2016 I was frustrated by vscode vim mode undo&#x2F;redo stack: when you activated undo it would process each individual character insertion&#x2F;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&#x2F;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:&#x2F;&#x2F;github.com&#x2F;VSCodeVim&#x2F;Vim&#x2F;commit&#x2F;0576f199cb7a765efb3cd7a7828fd19ff3af094b#diff-4f43ed31c28406a288f2c26932c9827db19761ce9449e7d6c24b2339b19a3080R100-R140" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;VSCodeVim&#x2F;Vim&#x2F;commit&#x2F;0576f199cb7a765efb3c...</a><p>[2]: <a href="https:&#x2F;&#x2F;github.com&#x2F;VSCodeVim&#x2F;Vim&#x2F;blame&#x2F;v1.29.0&#x2F;src&#x2F;history&#x2F;historyTracker.ts#L156-L201" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;VSCodeVim&#x2F;Vim&#x2F;blame&#x2F;v1.29.0&#x2F;src&#x2F;history&#x2F;h...</a>
评论 #43494739 未加载
londons_exploreabout 2 months ago
In reality, you&#x27;re gonna quickly run into battles with state&#x27;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&#x27;ll either end up with a buggy undo implementation, or end up spending a lot of engineering time hunting corner cases.
评论 #43491966 未加载
评论 #43491200 未加载
评论 #43492269 未加载
hyperbolablablaabout 2 months ago
In my C code, I implement undo&#x2F;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&#x27;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&#x27;t have to explicitly write &quot;actions&quot; for everything you want to undo&#x2F;redo. All in-memory actions are handled automatically.<p>Obviously this means that things like FS changes or network IO changes aren&#x27;t captured, but sometimes that&#x27;s fine, and I&#x27;m sure there&#x27;s a way to extend this scheme to handle those things somehow
评论 #43491827 未加载
评论 #43492291 未加载
评论 #43491389 未加载
评论 #43492045 未加载
molszanskiabout 2 months ago
Thank you for sharing<p>Other notable inspirations <a href="https:&#x2F;&#x2F;immerjs.github.io&#x2F;immer&#x2F;patches&#x2F;" rel="nofollow">https:&#x2F;&#x2F;immerjs.github.io&#x2F;immer&#x2F;patches&#x2F;</a> <a href="https:&#x2F;&#x2F;github.com&#x2F;webstudio-is&#x2F;immerhin" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;webstudio-is&#x2F;immerhin</a>
评论 #43489155 未加载
balamatomabout 2 months ago
You can clear an array in JS by setting foo.length = 0? TIL.
评论 #43489807 未加载
评论 #43489041 未加载
评论 #43488406 未加载
评论 #43490583 未加载
评论 #43489338 未加载
nyanpasu64about 2 months ago
If you get clever with things you can define actions with a single &quot;apply_swap()&quot; 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&#x2F;removing items and redo&#x2F;undo, and allows applying edits on a real-time thread with zero memory allocations (after constructing an edit action).
noduermeabout 2 months ago
I think it&#x27;s a clever and elegant solution. Obviously, ymmv depending on the environment, or what exactly you&#x27;re trying to undo&#x2F;redo. But I like that it thinks of two stacks. StructuredClone is not something I&#x27;ve ever used or am familiar with... I&#x27;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&#x27;m kinda looking forward to playing with that next time I have a mess of event handlers.
评论 #43490788 未加载
catapartabout 2 months ago
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:&#x2F;&#x2F;github.com&#x2F;catapart&#x2F;magnitce-action-history" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;catapart&#x2F;magnitce-action-history</a>
评论 #43491982 未加载
neilparikhabout 2 months ago
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:&#x2F;&#x2F;www.st.cs.uni-saarland.de&#x2F;edu&#x2F;seminare&#x2F;2005&#x2F;advanced-fp&#x2F;docs&#x2F;huet-zipper.pdf" rel="nofollow">https:&#x2F;&#x2F;www.st.cs.uni-saarland.de&#x2F;edu&#x2F;seminare&#x2F;2005&#x2F;advanced...</a>
2bitabout 2 months ago
You can find a similar method in zundo, a zustand middleware for undo&#x2F;redo. It has the concept of pastStates and futureStates.
hyperhelloabout 2 months ago
It&#x27;s very nice, but it doesn&#x27;t do system undo. You can shake iPhones to get an Undo&#x2F;Redo dialog, for example.<p>So you can probably trap Meta-Z but some people use menus or the other affordances.
评论 #43488287 未加载
评论 #43487635 未加载
评论 #43489139 未加载
atum47about 2 months ago
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.
benatkinabout 2 months ago
Using two stacks doesn&#x27;t actually make it more robust. I think using a private property for the stack would, though.
评论 #43492010 未加载
im3w1labout 2 months ago
In this code past uses push&#x2F;pop and future uses shift&#x2F;unshift. But they can both use push&#x2F;pop.
评论 #43497146 未加载
z3t4about 2 months ago
If you have Collaborative Text Editing ( Operational Transform or CRDT) you get undo&#x2F;redo for free!
evanrelfabout 2 months ago
A zipper!
ulrischaabout 2 months ago
I wonder why undo redo is not a built in feature. It is such a core functionality
评论 #43493550 未加载
samchonabout 2 months ago
The background is yellow and it hurts my eyes ToT
fergieabout 2 months ago
Very elegant code. Somebody will surely find a way to make it much less robust and readable by converting it to Typescript
评论 #43522837 未加载