代码编辑器重做与历史管理完全指南

撤销和重做是编辑器最核心的功能之一。好的历史管理不仅支持撤销重做,还支持时间旅行和状态比较。本文详细介绍实现方案。

操作历史栈设计

使用两个栈来管理操作历史: 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方法
  • 相邻的可合并操作进行合并,减少历史记录
  • 复合操作保证原子性
  • 时间旅行支持跳转到任意状态