In editor application development, the undo and redo functions are essential basic functions. Undo refers to undoing the most recent document editing action or command (action/command), returning the document to the state before the action/command was executed. Recovery (redo) refers to the reverse operation of undo, re-executing the recently undone action or command. These two functions usually appear in the [Edit] submenu of the menu bar, and are bound to different shortcut keys according to the operating system.

Normal web front-end development does not involve this function, but there is a very similar area, which is browser history and routing. The browser’s back and forward functions are similar to undo and restore. In addition, some editors (such as Photoshop) not only support undo and redo commands, but can also display document editing history and jump to any previous document history state.

Document State History Document State History
a. Each edit to the document will generate a new document status, and then these document statuses constitute the document status history, whose structure is a list structure as shown below. Among them, S0, S1 and S2 represent the document state (Document State), A1 and A2 represent a series of editing actions (Action) on the document, and the current document state is S2. At this time, only the undo operation can be performed on the document, and the recovery operation cannot be performed.

Current
+
|
v
+——+ A1 +——+ A2 +—+–+
| S0 +—–>+ S1 +—–>+ S2 |
+——+ +——+ +——+
b. After undoing the document status, the document status history becomes as shown below. At this time, the document status history is still there, but the current document status has changed to S1, and undo and restore operations can be performed on the document.

Current
+
|
v
+——+ A1 +—+–+ A2 +——+
| S0 +—–>+ S1 +—–>+ S2 |
+——+ +——+ +——+
c1. If the restore operation is performed, the document status history becomes as shown below. The current document status is S2, which is the status before the undo action is performed.

Current
+
|
v
+——+ A1 +——+ A2 +—+–+
| S0 +—–>+ S1 +—–>+ S2 |
+——+ +——+ +——+
c2. If other editing actions are performed, the document status history after the current document status will be lost, and the document status history will become the following figure.

Current
+
|
v
+——+ A1 +——+ A3 +—+–+
| S0 +—–>+ S1 +—–>+ S3 |
+——+ +——+ +——+
Implementation plan
Here are two basic implementation solutions: one is to save the document status after each edit, as shown in the document status history section at the beginning of this article; the other is to generate a corresponding reverse action for each editing action.

Solution 1: Save document status history
As mentioned above, this solution refers to saving the document status after each edit. Undo and restore only point to the current document status differently. This solution is relatively simple to implement and is applicable to all editing actions. The disadvantage is that saving the document status after each edit may occupy a relatively large amount of memory. The reason why I say “possibly” here is because it depends on the implementation and whether it is optimized. If you use technologies such as ImmutableJS, the memory usage will be greatly reduced, and the overhead of saving the state will be very small. Or you can save some key editing document status (for example, if the editing action takes a long time or the document status is saved every five edits), then when restoring, first jump to the most recent file status, and then re-execute the relevant editing action, or It can reduce memory usage to a certain extent.

Option 2: Save the reverse action of the editing action
This solution means that each editing action generates a corresponding reverse action, and then when the undo is performed, the reverse action is performed. However, because not every editing action is reversible, for example, it involves random generation. Moreover, it has poor scalability and complex implementation, and requires implementing its inverse action for each action, so this solution is rarely used.

Simple implementation
Here is the implementation of option one.

interface State {
[key: string]: any;
}

interface StateHistoryOptions {
max?: number;
}

class StateHistory {
static defaults: StateHistoryOptions = {
max: 20
}

constructor(initialState: State, options?: StateHistoryOptions = {}) {
this.opts = Object.assign({}, StateHistory.defaults, options)
this.stateHistory = [initialState]
this.currentIndex = 0
}

get size(): number {
return this.stateHistory.length
}

get current(): State {
return this.stateHistory[this.currentIndex]
}

get canUndo(): boolean {
return this.currentIndex > 0
}

getcanRedo(): boolean {
return this.currentIndex < this.size – 1
}

insert(state: State): void {
if (this.currentIndex < this.size – 1) {
this.stateHistory.splice(this.currentIndex + 1, this.size – 1 – this.currentIndex)
}
if (this.size >= this.opts.max) {
this.stateHistory.unshift()
}
this.stateHistory.push(state)
this.currentIndex++
}

undo(): void {
if (this.canUndo) {
this.currentIndex–
}
}

redo(): void {
if (this.canRedo) {
this.currentIndex++
}
}
}
History list
Some editors support displaying a list of recent editing actions and jumping directly to a historical record. The following code needs to be added:

class StateHistory {
// All document states before the current document state
get past(): State[] {
return this.stateHistory.slice(0, this.currentIndex)
}

// All document states after the current document state
get future(): State[]) {
return this.stateHistory.slice(this.currentIndex + 1)
}

jump(index: number) {
if (index > -1 && index < this.size – 1) {
this.currentIndex = index
}
}
}