撤销和重做是编辑器最核心的功能之一。好的历史管理不仅支持撤销重做,还支持时间旅行和状态比较。本文详细介绍实现方案。
操作历史栈设计
使用两个栈来管理操作历史: undo栈保存可撤销的操作,redo栈保存可重做的操作:
/* 操作基类 */
interface Operation {
type: string;
execute(editor: Editor): void;
undo(editor: Editor): void;
merge(prev: Operation): boolean;
}
class HistoryManager {
private undoStack: Operation[] = [];
private redoStack: Operation[] = [];
private maxSize = 200;
push(op: Operation) {
this.undoStack.push(op);
/* 新操作会清除重做栈 */
this.redoStack = [];
/* 限制历史大小 */
if (this.undoStack.length > this.maxSize) {
this.undoStack.shift();
}
}
undo(editor: Editor): boolean {
const op = this.undoStack.pop();
if (!op) return false;
op.undo(editor);
this.redoStack.push(op);
return true;
}
redo(editor: Editor): boolean {
const op = this.redoStack.pop();
if (!op) return false;
op.execute(editor);
this.undoStack.push(op);
return true;
}
canUndo(): boolean {
return this.undoStack.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
}
插入操作
文本插入操作需要记录插入位置和内容:
/* 插入操作 */
class InsertOperation implements Operation {
constructor(
private position: Position,
private text: string
) {}
execute(editor: Editor) {
editor.insertText(this.position, this.text);
}
undo(editor: Editor) {
const endPos = offsetPosition(this.position, this.text.length);
editor.deleteRange(this.position, endPos);
}
merge(prev: Operation): boolean {
/* 如果是连续的插入操作,合并 */
if (prev instanceof InsertOperation) {
const prevEnd = offsetPosition(prev.position, prev.text.length);
return positionsEqual(prevEnd, this.position);
}
return false;
}
}
删除操作
删除操作需要保存被删除的文本,以便撤销:
/* 删除操作 */
class DeleteOperation implements Operation {
constructor(
private start: Position,
private end: Position,
private deletedText: string
) {}
execute(editor: Editor) {
editor.deleteRange(this.start, this.end);
}
undo(editor: Editor) {
editor.insertText(this.start, this.deletedText);
}
merge(prev: Operation): boolean {
/* 合并连续的删除 */
if (prev instanceof DeleteOperation) {
const prevEnd = prev.end;
return positionsEqual(prevEnd, this.start);
}
return false;
}
}
复合操作
将多个操作组合成一个原子操作,比如批量替换:
/* 复合操作 */
class CompositeOperation implements Operation {
constructor(private operations: Operation[]) {}
execute(editor: Editor) {
for (const op of this.operations) {
op.execute(editor);
}
}
undo(editor: Editor) {
/* 逆序撤销 */
for (let i = this.operations.length - 1; i >= 0; i--) {
this.operations[i].undo(editor);
}
}
}
时间旅行
支持跳转到任意历史状态:
/* 时间旅行支持 */
class TimeTravelHistory extends HistoryManager {
/* 跳转到指定历史索引 */
travelTo(editor: Editor, index: number) {
const currentIndex = this.undoStack.length;
if (index === currentIndex) return;
if (index < currentIndex) {
/* 需要撤销 */
while (this.undoStack.length > index) {
const op = this.undoStack.pop();
op?.undo(editor);
this.redoStack.push(op!);
}
} else {
/* 需要重做 */
while (this.undoStack.length < index) {
const op = this.redoStack.pop();
op?.execute(editor);
this.undoStack.push(op!);
}
}
}
/* 获取历史快照用于UI显示 */
getHistoryStates(): HistoryState[] {
return this.undoStack.map((op, i) => ({
index: i,
description: getOperationDescription(op)
}));
}
}
性能优化
对于大文档,可以对历史操作进行序列化和压缩,只在内存中保留最近的N个状态,更早的历史可以持久化到磁盘。
总结
- 使用两个栈管理undo和redo
- 每个操作需要实现execute和undo方法
- 相邻的可合并操作进行合并,减少历史记录
- 复合操作保证原子性
- 时间旅行支持跳转到任意状态