第5讲:撤销与重做

前四讲我们实现了文档引擎、OT算法、WebSocket通信和操作同步。现在文档已经可以多人实时协作了,但还缺一个至关重要的用户体验功能——撤销(Undo)和重做(Redo)

单机环境下的撤销很简单:维护一个操作栈。但在协作环境下,撤销变得复杂——你不能撤销别人的操作

这一讲,我们来实现协作友好的撤销/重做机制。


一、撤销的挑战

1.1 协作撤销的问题

场景:三个人同时编辑

Alice: 插入 "Hello"  (op1)
Bob:   插入 " World" (op2)
Carol: 插入 "!!!"    (op3)

文档状态: "Hello World!!!"

问题:如果 Alice 想撤销自己的操作,会发生什么?

Alice 的 op1 是 "插入 Hello at 0"
但此时文档已经是 "Hello World!!!"
位置 0 已经被 Bob 和 Carol 的操作改变了!

简单撤销会导致:
- 删除了 "H" 而不是 "Hello"
- 破坏了 Bob 和 Carol 的操作

解决方案:使用 OT 变换来"逆向"操作

1.2 撤销的类型

类型

说明

实现难度

本地撤销

只撤销自己的上一个操作

简单

选择性撤销

撤销任意一个历史操作

中等

协作撤销

在多用户环境下正确撤销

困难

时间旅行

回退到任意历史版本

困难


二、撤销栈管理

2.1 撤销栈实现

# core/undo/undostack.py
"""
撤销栈管理

支持协作环境下的撤销和重做。
"""

from __future__ import annotations
from typing import List, Optional, Dict, Tuple
from dataclasses import dataclass, field
from enum import Enum
import time
import logging

from core.ot.operation import OTOperation, OpType
from core.ot.transform import transform, invert

logger = logging.getLogger(__name__)


class UndoGroup(Enum):
    """撤销分组"""
    TYPING = "typing"          # 打字(自动合并)
    PASTE = "paste"            # 粘贴
    DELETE = "delete"          # 删除
    FORMAT = "format"          # 格式化
    OTHER = "other"            # 其他


@dataclass
class UndoEntry:
    """
    撤销条目
    
    记录一次操作及其上下文信息。
    """
    op: OTOperation            # 原始操作
    user_id: str               # 执行操作的用户
    group: UndoGroup = UndoGroup.OTHER  # 分组
    timestamp: float = 0.0     # 时间戳
    merged: bool = False       # 是否已被合并
    
    def __post_init__(self):
        if not self.timestamp:
            self.timestamp = time.time()
    
    @property
    def inverse_op(self) -> OTOperation:
        """获取逆操作"""
        return invert(self.op)
    
    def to_dict(self) -> dict:
        return {
            'op': self.op.to_dict(),
            'user_id': self.user_id,
            'group': self.group.value,
            'timestamp': self.timestamp,
            'merged': self.merged
        }


class UndoStack:
    """
    撤销栈
    
    管理操作历史,支持撤销和重做。
    特点:
    - 按用户隔离(只能撤销自己的操作)
    - 自动合并连续的同类型操作
    - 支持选择性撤销
    """
    
    def __init__(self, max_size: int = 1000):
        self.max_size = max_size
        
        # 撤销栈
        self.undo_stack: List[UndoEntry] = []
        
        # 重做栈
        self.redo_stack: List[UndoEntry] = []
        
        # 合并配置(毫秒)
        self.merge_window_ms = 500  # 500ms 内的连续操作合并
        
        # 回调
        self.on_undo: Optional[callable] = None
        self.on_redo: Optional[callable] = None
    
    # ---------- 操作记录 ----------
    
    def push(self, op: OTOperation, user_id: str,
             group: UndoGroup = UndoGroup.OTHER):
        """
        记录操作到撤销栈
        
        Args:
            op: 操作
            user_id: 用户 ID
            group: 操作分组
        """
        entry = UndoEntry(
            op=op,
            user_id=user_id,
            group=group
        )
        
        # 尝试合并
        if self._try_merge(entry):
            logger.debug(f"Merged op into existing entry: {op}")
            return
        
        # 添加到撤销栈
        self.undo_stack.append(entry)
        
        # 限制大小
        if len(self.undo_stack) > self.max_size:
            self.undo_stack.pop(0)
        
        # 新操作清空重做栈
        self.redo_stack.clear()
        
        logger.debug(f"Pushed to undo stack: {op}")
    
    def _try_merge(self, new_entry: UndoEntry) -> bool:
        """
        尝试合并操作
        
        合并条件:
        1. 同一个用户
        2. 同一分组
        3. 时间在合并窗口内
        4. 操作类型相同
        """
        if not self.undo_stack:
            return False
        
        last = self.undo_stack[-1]
        
        if last.user_id != new_entry.user_id:
            return False
        
        if last.group != new_entry.group:
            return False
        
        elapsed = (new_entry.timestamp - last.timestamp) * 1000
        if elapsed > self.merge_window_ms:
            return False
        
        # 尝试合并操作
        from core.ot.transform import compose
        merged_op = compose(last.op, new_entry.op)
        
        if merged_op != new_entry.op:  # 如果可以合并
            last.op = merged_op
            last.timestamp = new_entry.timestamp
            last.merged = True
            return True
        
        return False
    
    # ---------- 撤销 ----------
    
    def undo(self, user_id: str) -> Optional[UndoEntry]:
        """
        撤销用户的上一个操作
        
        Args:
            user_id: 用户 ID
        Returns:
            撤销的条目(包含逆操作)
        """
        # 找到该用户最近的未合并操作
        for i in range(len(self.undo_stack) - 1, -1, -1):
            entry = self.undo_stack[i]
            if entry.user_id == user_id and not entry.merged:
                # 从撤销栈移除
                self.undo_stack.pop(i)
                
                # 添加到重做栈
                self.redo_stack.append(entry)
                
                logger.info(f"Undo: {entry.op}")
                
                if self.on_undo:
                    self.on_undo(entry)
                
                return entry
        
        return None
    
    def undo_specific(self, index: int) -> Optional[UndoEntry]:
        """
        撤销指定索引的操作
        
        Args:
            index: 在撤销栈中的索引
        Returns:
            撤销的条目
        """
        if index < 0 or index >= len(self.undo_stack):
            return None
        
        entry = self.undo_stack.pop(index)
        self.redo_stack.append(entry)
        
        logger.info(f"Selective undo: {entry.op}")
        
        if self.on_undo:
            self.on_undo(entry)
        
        return entry
    
    # ---------- 重做 ----------
    
    def redo(self, user_id: str) -> Optional[UndoEntry]:
        """
        重做用户的上一个撤销
        
        Args:
            user_id: 用户 ID
        Returns:
            重做的条目
        """
        for i in range(len(self.redo_stack) - 1, -1, -1):
            entry = self.redo_stack[i]
            if entry.user_id == user_id:
                self.redo_stack.pop(i)
                self.undo_stack.append(entry)
                
                logger.info(f"Redo: {entry.op}")
                
                if self.on_redo:
                    self.on_redo(entry)
                
                return entry
        
        return None
    
    # ---------- 状态查询 ----------
    
    def can_undo(self, user_id: str) -> bool:
        """用户是否可以撤销"""
        return any(
            e.user_id == user_id and not e.merged
            for e in self.undo_stack
        )
    
    def can_redo(self, user_id: str) -> bool:
        """用户是否可以重做"""
        return any(
            e.user_id == user_id
            for e in self.redo_stack
        )
    
    def get_user_history(self, user_id: str) -> List[UndoEntry]:
        """获取用户的操作历史"""
        return [
            e for e in self.undo_stack
            if e.user_id == user_id and not e.merged
        ]
    
    def get_stats(self) -> dict:
        """获取统计信息"""
        return {
            'undo_size': len(self.undo_stack),
            'redo_size': len(self.redo_stack),
            'max_size': self.max_size,
            'users': len(set(e.user_id for e in self.undo_stack))
        }

三、协作撤销引擎

3.1 撤销操作变换

# core/undo/undo_engine.py
"""
协作撤销引擎

处理协作环境下的撤销操作变换。
"""

from __future__ import annotations
from typing import List, Optional, Tuple
from dataclasses import dataclass
import logging

from core.ot.operation import OTOperation, OpType
from core.ot.transform import transform, invert
from core.undo.undostack import UndoStack, UndoEntry

logger = logging.getLogger(__name__)


class UndoEngine:
    """
    撤销引擎
    
    处理撤销操作的 OT 变换,确保在协作环境下的正确性。
    """
    
    def __init__(self, site_id: str):
        self.site_id = site_id
        self.undo_stack = UndoStack()
        
        # 待处理的撤销操作
        self.pending_undos: List[UndoEntry] = []
    
    def record_op(self, op: OTOperation):
        """
        记录操作
        
        Args:
            op: 执行的操作
        """
        self.undo_stack.push(op, self.site_id)
    
    def perform_undo(self) -> Optional[OTOperation]:
        """
        执行撤销
        
        获取逆操作,并根据后续操作进行变换。
        
        Returns:
            变换后的逆操作
        """
        entry = self.undo_stack.undo(self.site_id)
        if not entry:
            return None
        
        # 获取逆操作
        inverse = entry.inverse_op
        
        # 对逆操作进行变换(考虑后续的操作)
        transformed_inverse = self._transform_for_undo(inverse, entry)
        
        logger.info(f"Performing undo: {entry.op} → {transformed_inverse}")
        return transformed_inverse
    
    def perform_redo(self) -> Optional[OTOperation]:
        """
        执行重做
        
        Returns:
            变换后的原操作
        """
        entry = self.undo_stack.redo(self.site_id)
        if not entry:
            return None
        
        # 对原操作进行变换
        transformed_op = self._transform_for_redo(entry.op)
        
        logger.info(f"Performing redo: {entry.op} → {transformed_op}")
        return transformed_op
    
    def _transform_for_undo(self, inverse: OTOperation,
                            entry: UndoEntry) -> OTOperation:
        """
        为撤销变换逆操作
        
        由于在原始操作之后可能有其他操作,
        逆操作需要根据这些操作进行变换。
        
        Args:
            inverse: 逆操作
            entry: 被撤销的条目
        Returns:
            变换后的逆操作
        """
        transformed = inverse.clone()
        
        # 找到被撤销操作之后的所有操作
        later_ops = self._get_later_ops(entry)
        
        # 依次变换
        for later_op in later_ops:
            _, transformed = transform(later_op, transformed)
        
        return transformed
    
    def _transform_for_redo(self, op: OTOperation) -> OTOperation:
        """
        为重做变换操作
        
        Args:
            op: 原始操作
        Returns:
            变换后的操作
        """
        transformed = op.clone()
        
        # 找到该操作之后的所有操作
        later_ops = self._get_later_ops_since(op)
        
        for later_op in later_ops:
            _, transformed = transform(later_op, transformed)
        
        return transformed
    
    def _get_later_ops(self, entry: UndoEntry) -> List[OTOperation]:
        """
        获取某个条目之后的所有操作
        
        Args:
            entry: 目标条目
        Returns:
            之后的操作列表
        """
        later_ops = []
        found = False
        
        for e in self.undo_stack.undo_stack:
            if e is entry:
                found = True
                continue
            if found and not e.merged:
                later_ops.append(e.op)
        
        # 也包括重做栈中的操作
        for e in self.undo_stack.redo_stack:
            later_ops.append(e.op)
        
        return later_ops
    
    def _get_later_ops_since(self, op: OTOperation) -> List[OTOperation]:
        """
        获取某个操作之后的所有操作
        
        Args:
            op: 目标操作
        Returns:
            之后的操作列表
        """
        later_ops = []
        found = False
        
        for e in self.undo_stack.undo_stack:
            if e.op is op:
                found = True
                continue
            if found and not e.merged:
                later_ops.append(e.op)
        
        return later_ops
    
    def get_status(self) -> dict:
        """获取状态"""
        return {
            'can_undo': self.undo_stack.can_undo(self.site_id),
            'can_redo': self.undo_stack.can_redo(self.site_id),
            'stack_stats': self.undo_stack.get_stats()
        }

四、服务端撤销管理

4.1 服务端撤销处理

# server/undo_handler.py
"""
服务端撤销处理器

管理所有客户端的撤销请求。
"""

from __future__ import annotations
from typing import Dict, List, Optional
import logging

from core.ot.operation import OTOperation, OpType
from core.ot.transform import transform
from core.undo.undostack import UndoStack, UndoEntry

logger = logging.getLogger(__name__)


class ServerUndoManager:
    """
    服务端撤销管理器
    
    维护全局操作历史,处理客户端的撤销请求。
    """
    
    def __init__(self):
        # 全局操作历史
        self.global_history: List[OTOperation] = []
        
        # 每个用户的撤销栈
        self.user_stacks: Dict[str, UndoStack] = {}
        
        # 操作到用户的映射
        self.op_owners: Dict[str, str] = {}  # op_id -> user_id
    
    def record_op(self, op: OTOperation, user_id: str):
        """
        记录操作
        
        Args:
            op: 操作
            user_id: 用户 ID
        """
        self.global_history.append(op)
        self.op_owners[op.id] = user_id
        
        # 记录到用户的撤销栈
        if user_id not in self.user_stacks:
            self.user_stacks[user_id] = UndoStack()
        
        self.user_stacks[user_id].push(op, user_id)
    
    def handle_undo_request(self, user_id: str) -> Optional[OTOperation]:
        """
        处理撤销请求
        
        1. 找到用户最近的未撤销操作
        2. 生成逆操作
        3. 对逆操作进行全局变换
        
        Args:
            user_id: 请求撤销的用户
        Returns:
            变换后的逆操作
        """
        stack = self.user_stacks.get(user_id)
        if not stack:
            return None
        
        entry = stack.undo(user_id)
        if not entry:
            return None
        
        # 获取逆操作
        inverse = entry.inverse_op
        
        # 找到该操作之后的所有全局操作
        later_global_ops = self._get_later_global_ops(entry.op)
        
        # 依次变换
        transformed = inverse.clone()
        for later_op in later_global_ops:
            _, transformed = transform(later_op, transformed)
        
        logger.info(f"Server undo for {user_id}: {entry.op} → {transformed}")
        return transformed
    
    def handle_redo_request(self, user_id: str) -> Optional[OTOperation]:
        """
        处理重做请求
        
        Args:
            user_id: 请求重做的用户
        Returns:
            变换后的操作
        """
        stack = self.user_stacks.get(user_id)
        if not stack:
            return None
        
        entry = stack.redo(user_id)
        if not entry:
            return None
        
        # 对该操作进行变换
        transformed = entry.op.clone()
        later_ops = self._get_later_global_ops(entry.op)
        
        for later_op in later_ops:
            _, transformed = transform(later_op, transformed)
        
        logger.info(f"Server redo for {user_id}: {entry.op} → {transformed}")
        return transformed
    
    def _get_later_global_ops(self, op: OTOperation) -> List[OTOperation]:
        """
        获取全局历史中某个操作之后的操作
        
        Args:
            op: 目标操作
        Returns:
            之后的操作列表
        """
        later_ops = []
        found = False
        
        for hist_op in self.global_history:
            if hist_op.id == op.id:
                found = True
                continue
            if found:
                later_ops.append(hist_op)
        
        return later_ops
    
    def can_undo(self, user_id: str) -> bool:
        """用户是否可以撤销"""
        stack = self.user_stacks.get(user_id)
        return stack.can_undo(user_id) if stack else False
    
    def can_redo(self, user_id: str) -> bool:
        """用户是否可以重做"""
        stack = self.user_stacks.get(user_id)
        return stack.can_redo(user_id) if stack else False

五、集成到文档引擎

5.1 UndoDocument

# core/undo/undo_document.py
"""
支持撤销的文档引擎
"""

from typing import Optional, List
import logging

from core.ot.ot_document import OTDocument
from core.ot.operation import OTOperation, OpType
from core.undo.undo_engine import UndoEngine

logger = logging.getLogger(__name__)


class UndoDocument(OTDocument):
    """
    支持撤销的文档引擎
    
    继承自 OTDocument,增加撤销/重做功能。
    """
    
    def __init__(self, document_id: str, site_id: str,
                 initial_text: str = ""):
        super().__init__(document_id, site_id, initial_text)
        
        self.undo_engine = UndoEngine(site_id)
        
        # 撤销/重做回调
        self.on_undo_performed: Optional[callable] = None
        self.on_redo_performed: Optional[callable] = None
    
    # ---------- 操作记录 ----------
    
    def insert(self, position: int, text: str) -> OTOperation:
        """插入文本(自动记录撤销信息)"""
        op = super().insert(position, text)
        self.undo_engine.record_op(op)
        return op
    
    def delete(self, position: int, length: int) -> OTOperation:
        """删除文本(自动记录撤销信息)"""
        op = super().delete(position, length)
        self.undo_engine.record_op(op)
        return op
    
    # ---------- 撤销/重做 ----------
    
    def undo(self) -> Optional[OTOperation]:
        """
        撤销上一个操作
        
        Returns:
            执行的逆操作
        """
        inverse_op = self.undo_engine.perform_undo()
        if not inverse_op:
            return None
        
        # 应用逆操作
        if inverse_op.op_type == OpType.INSERT:
            super().insert(inverse_op.position, inverse_op.text)
        elif inverse_op.op_type == OpType.DELETE:
            super().delete(inverse_op.position, 
                          len(inverse_op.deleted_text))
        
        self.version += 1
        
        if self.on_undo_performed:
            self.on_undo_performed(inverse_op)
        
        return inverse_op
    
    def redo(self) -> Optional[OTOperation]:
        """
        重做上一个撤销
        
        Returns:
            执行的操作
        """
        op = self.undo_engine.perform_redo()
        if not op:
            return None
        
        # 应用操作
        if op.op_type == OpType.INSERT:
            super().insert(op.position, op.text)
        elif op.op_type == OpType.DELETE:
            super().delete(op.position, len(op.deleted_text))
        
        self.version += 1
        
        if self.on_redo_performed:
            self.on_redo_performed(op)
        
        return op
    
    # ---------- 状态查询 ----------
    
    def can_undo(self) -> bool:
        """是否可以撤销"""
        return self.undo_engine.undo_stack.can_undo(self.site_id)
    
    def can_redo(self) -> bool:
        """是否可以重做"""
        return self.undo_engine.undo_stack.can_redo(self.site_id)
    
    def get_undo_status(self) -> dict:
        """获取撤销状态"""
        return self.undo_engine.get_status()

六、演示

6.1 撤销功能演示

# examples/undo_demo.py
"""
撤销/重做功能演示
"""

import logging
import sys

sys.path.insert(0, '..')

from core.undo.undo_document import UndoDocument
from core.undo.undo_engine import UndoEngine
from core.ot.operation import OTOperation, OpType

logging.basicConfig(level=logging.INFO)

def demonstrate_basic_undo():
    """演示基本撤销功能"""
    print("=" * 66)
    print("↩️  基本撤销/重做演示")
    print("=" * 66)
    
    doc = UndoDocument("doc-1", "Alice", "")
    
    print(f"\n初始文档: '{doc.get_text()}'")
    
    # 输入 "Hello"
    print("\n--- 输入 'Hello' ---")
    doc.insert(0, "H")
    doc.insert(1, "e")
    doc.insert(2, "l")
    doc.insert(3, "l")
    doc.insert(4, "o")
    print(f"文档: '{doc.get_text()}'")
    
    # 撤销
    print("\n--- 撤销 3 次 ---")
    for i in range(3):
        op = doc.undo()
        print(f"撤销: {op}")
        print(f"文档: '{doc.get_text()}'")
    
    # 重做
    print("\n--- 重做 2 次 ---")
    for i in range(2):
        op = doc.redo()
        print(f"重做: {op}")
        print(f"文档: '{doc.get_text()}'")
    
    print(f"\n最终文档: '{doc.get_text()}'")
    print(f"撤销状态: {doc.get_undo_status()}")

def demonstrate_collaborative_undo():
    """演示协作撤销"""
    print("\n" + "=" * 66)
    print("👥 协作撤销演示")
    print("=" * 66)
    
    # 创建两个用户的文档
    alice_doc = UndoDocument("doc-1", "Alice", "")
    bob_doc = UndoDocument("doc-1", "Bob", "")
    
    print(f"\n初始文档: '{alice_doc.get_text()}'")
    
    # Alice 输入
    print("\n--- Alice 输入 'Hello' ---")
    op_a1 = alice_doc.insert(0, "Hello")
    print(f"Alice 文档: '{alice_doc.get_text()}'")
    
    # Bob 同步 Alice 的操作
    bob_doc.receive_remote_op(op_a1)
    print(f"Bob 文档: '{bob_doc.get_text()}'")
    
    # Bob 输入
    print("\n--- Bob 输入 ' World' ---")
    op_b1 = bob_doc.insert(5, " World")
    print(f"Bob 文档: '{bob_doc.get_text()}'")
    
    # Alice 同步 Bob 的操作
    alice_doc.receive_remote_op(op_b1)
    print(f"Alice 文档: '{alice_doc.get_text()}'")
    
    # Alice 撤销自己的操作
    print("\n--- Alice 撤销 'Hello' ---")
    inverse = alice_doc.undo()
    print(f"Alice 撤销: {inverse}")
    print(f"Alice 文档: '{alice_doc.get_text()}'")
    
    # Bob 同步撤销
    bob_doc.receive_remote_op(inverse)
    print(f"Bob 文档: '{bob_doc.get_text()}'")
    
    print(f"\n最终一致: {alice_doc.get_text() == bob_doc.get_text()}")

def demonstrate_selective_undo():
    """演示选择性撤销"""
    print("\n" + "=" * 66)
    print("🎯 选择性撤销演示")
    print("=" * 66)
    
    doc = UndoDocument("doc-1", "Alice", "")
    
    # 执行一系列操作
    print("\n--- 执行操作序列 ---")
    ops = []
    ops.append(doc.insert(0, "A"))   # op1
    ops.append(doc.insert(1, "B"))   # op2
    ops.append(doc.insert(2, "C"))   # op3
    ops.append(doc.insert(3, "D"))   # op4
    print(f"文档: '{doc.get_text()}'")
    
    # 查看撤销栈
    print("\n撤销栈:")
    for i, entry in enumerate(doc.undo_engine.undo_stack.undo_stack):
        print(f"  [{i}] {entry.op}")
    
    # 选择性撤销 op2(插入 "B")
    print("\n--- 选择性撤销 op2 (插入 'B') ---")
    entry = doc.undo_engine.undo_stack.undo_specific(2)
    if entry:
        inverse = entry.inverse_op
        # 变换和应用省略...
        print(f"选择的条目: {entry.op}")
        print(f"逆操作: {inverse}")

def main():
    demonstrate_basic_undo()
    demonstrate_collaborative_undo()
    demonstrate_selective_undo()

if __name__ == "__main__":
    main()

七、测试

7.1 撤销测试

# tests/test_undo.py
import pytest
from core.undo.undostack import UndoStack, UndoEntry, UndoGroup
from core.undo.undo_engine import UndoEngine
from core.undo.undo_document import UndoDocument
from core.ot.operation import OTOperation, OpType

class TestUndoStack:
    """撤销栈测试"""
    
    def test_push_and_undo(self):
        stack = UndoStack()
        
        op = OTOperation(OpType.INSERT, 0, text="Hello", 
                        site_id="Alice", sequence=1)
        stack.push(op, "Alice")
        
        assert stack.can_undo("Alice")
        assert not stack.can_undo("Bob")
        
        entry = stack.undo("Alice")
        assert entry is not None
        assert entry.op is op
        
        assert not stack.can_undo("Alice")
        assert stack.can_redo("Alice")
    
    def test_redo(self):
        stack = UndoStack()
        
        op = OTOperation(OpType.INSERT, 0, text="Hello",
                        site_id="Alice", sequence=1)
        stack.push(op, "Alice")
        
        stack.undo("Alice")
        entry = stack.redo("Alice")
        
        assert entry is not None
        assert entry.op is op
    
    def test_merge_typing(self):
        stack = UndoStack(merge_window_ms=1000)
        
        import time
        ops = []
        for i, ch in enumerate("Hello"):
            op = OTOperation(OpType.INSERT, i, text=ch,
                           site_id="Alice", sequence=i)
            stack.push(op, "Alice", UndoGroup.TYPING)
            ops.append(op)
            time.sleep(0.01)
        
        # 应该合并为一个操作
        assert len(stack.undo_stack) == 1
        assert stack.undo_stack[0].merged

class TestUndoEngine:
    """撤销引擎测试"""
    
    def test_undo_inverse(self):
        engine = UndoEngine("Alice")
        
        op = OTOperation(OpType.INSERT, 0, text="Hello",
                        site_id="Alice", sequence=1)
        engine.record_op(op)
        
        inverse = engine.perform_undo()
        assert inverse is not None
        assert inverse.op_type == OpType.DELETE
        assert inverse.position == 0
        assert inverse.deleted_text == "Hello"

class TestUndoDocument:
    """撤销文档测试"""
    
    def test_undo_redo_cycle(self):
        doc = UndoDocument("doc-1", "Alice", "")
        
        doc.insert(0, "Hello")
        assert doc.get_text() == "Hello"
        
        doc.undo()
        assert doc.get_text() == ""
        
        doc.redo()
        assert doc.get_text() == "Hello"
    
    def test_collaborative_undo(self):
        alice = UndoDocument("doc-1", "Alice", "")
        bob = UndoDocument("doc-1", "Bob", "")
        
        # Alice 输入
        op_a = alice.insert(0, "Hello")
        bob.receive_remote_op(op_a)
        
        # Bob 输入
        op_b = bob.insert(5, " World")
        alice.receive_remote_op(op_b)
        
        assert alice.get_text() == "Hello World"
        assert bob.get_text() == "Hello World"
        
        # Alice 撤销
        inverse = alice.undo()
        assert inverse is not None
        
        # Bob 同步撤销
        bob.receive_remote_op(inverse)
        
        assert alice.get_text() == bob.get_text()

if __name__ == "__main__":
    pytest.main([__file__, "-v"])

八、总结

8.1 本讲成果

组件

文件

功能

UndoStack

core/undo/undostack.py

撤销栈管理,支持合并

UndoEngine

core/undo/undo_engine.py

撤销操作变换

ServerUndoManager

server/undo_handler.py

服务端撤销管理

UndoDocument

core/undo/undo_document.py

支持撤销的文档引擎

8.2 核心知识点

  • 撤销的本质:执行逆操作,但需要对后续操作进行变换

  • 操作合并:连续的同类型操作合并为一个,提高撤销效率

  • 用户隔离:每个用户只能撤销自己的操作

  • 协作撤销:撤销操作本身也是一个 OT 操作,需要同步给其他人

8.3 下一讲预告

第6讲:CRDT 实现

我们将探索另一种协同编辑方案——CRDT(无冲突复制数据类型):

  • CRDT 与 OT 的区别

  • LWW-Register 实现

  • RGA(可重放增长数组)

  • 操作日志合并策略

准备好了吗?让我们在第6讲再见!


🧰 开发之余的小工具推荐

处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top(子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值