基于Python打造支持Undo/Redo的文本编辑器命令系统

Written by

in

文章目录
  • 每天打开代码编辑器,你最常按的快捷键是什么? 对大多数开发者来说,Ctrl+Z(撤销)和 Ctrl+Y(重做)可能是仅次于保存的高频操作。写错了一段代码?撤销。误删了一个函数?撤销。一路 Redo 找回刚才的思路?重做。 这一切看似魔法的背后,正是软件工程中一个优雅而强大的设计模式:命令模式(Command Pattern)。 初学者可能会想:撤销不就是「保存历史状态,出错时回滚」吗?用一个列表记录每次操作前的文本不就行了? 这种方案在文本量小时可行,但在复杂系统中代价极高——每次操作都复制整个状态,内存消耗随历史深度线性增长,且无法区分「哪些操作可以合并」「哪些操作是原子的」。 命令模式的解法更加精妙:将每一个操作封装成独立的对象,对象内同时包含「执行」和「撤销」的逻辑。 撤销不是回滚状态,而是执行「反向命令」——插入的反操作是删除,移动的反操作是移回原位。 本文将从零构建一个功能完整的文本编辑器命令系统,覆盖基础命令实现、命令组合、宏命令、历史管理、以及生产级的持久化方案。
  • 在动手写命令之前,先构建一个功能完备的文本编辑器核心——它是所有命令操作的「接收者」: from dataclasses import dataclass, field from typing import Optional import re @dataclass class TextBuffer: “”” 文本缓冲区:编辑器的核心数据模型 支持光标定位、选区操作、文本增删 “”” content: str = ” cursor: int = 0 # 光标位置(字符索引) selection: Optional[tuple[int, int]] = None # 选区 (start, end) filename: str = ‘untitled.txt’ modified: bool = False def __post_init__(self): self._validate_cursor() def _validate_cursor(self): self.cursor = max(0, min(self.cursor, len(self.content))) # ===== 基础操作 ===== def insert(self, pos: int, text: str) -> None: “””在指定位置插入文本””” pos = max(0, min(pos, len(self.content))) self.content = self.content[:pos] + text + self.content[pos:] self.cursor = pos + len(text) self.modified = True def delete(self, start: int, end: int) -> str: “””删除 [start, end) 范围的文本,返回被删除内容””” start = max(0, min(start, len(self.content))) end = max(start, min(end, len(self.content))) deleted = self.content[start:end] self.content = self.content[:start] + self.content[end:] self.cursor = start self.modified = True return deleted def replace(self, start: int, end: int, new_text: str) -> str: “””替换 [start, end) 范围的文本,返回被替换内容””” old_text = self.delete(start, end) self.insert(start, new_text) return old_text def move_cursor(self, pos: int) -> int: “””移动光标,返回旧位置””” old_pos = self.cursor self.cursor = max(0, min(pos, len(self.content))) return old_pos def get_line_at(self, pos: int) -> tuple[int, int, str]: “””获取指定位置所在行的 (行首, 行尾, 行内容)””” line_start = self.content.rfind(‘n’, 0, pos) + 1 line_end = self.content.find(‘n’, pos) if line_end == -1: line_end = len(self.content) return line_start, line_end, self.content[line_start:line_end] def display(self, show_cursor: bool = True) -> str: “””渲染文本(带光标标记)””” if show_cursor: content = (self.content[:self.cursor] + ‘|’ + self.content[self.cursor:]) else: content = self.content lines = content.split(‘n’) result = [f”{‘─’ * 40}”, f”文件: {self.filename} {‘*’ if self.modified else ”}”, f”{‘─’ * 40}”] for i, line in enumerate(lines, 1): result.append(f”{i:3d} │ {line}”) result.append(f”{‘─’ * 40}”) result.append(f”光标: {self.cursor} | 字符数: {len(self.content)}”) return ‘n’.join(result)
  • from collections import deque import json from datetime import datetime class CommandHistory: “”” 命令历史管理器(Invoker) – 执行命令并记录到 undo 栈 – 支持多步 Undo/Redo – 支持历史限制(防止内存溢出) – 支持历史快照导出 “”” def __init__(self, max_history: int = 100): self._undo_stack: deque[Command] = deque(maxlen=max_history) self._redo_stack: deque[Command] = deque(maxlen=max_history) self._max_history = max_history self._execute_log: list[dict] = [] def execute(self, command: Command) -> None: “””执行命令并推入撤销栈””” command.execute() self._undo_stack.append(command) # 执行新命令后,清空 Redo 栈(与主流编辑器行为一致) self._redo_stack.clear() # 记录执行日志 self._execute_log.append({ ‘action’: ‘execute’, ‘command’: command.description, ‘timestamp’: datetime.now().isoformat() }) print(f” ✓ 执行: {command.description}”) def undo(self) -> bool: “””撤销最近一条命令””” if not self._undo_stack: print(” ⚠ 没有可撤销的操作”) return False command = self._undo_stack.pop() command.undo() self._redo_stack.append(command) self._execute_log.append({ ‘action’: ‘undo’, ‘command’: command.description, ‘timestamp’: datetime.now().isoformat() }) print(f” ↩ 撤销: {command.description}”) return True def redo(self) -> bool: “””重做最近一条被撤销的命令””” if not self._redo_stack: print(” ⚠ 没有可重做的操作”) return False command = self._redo_stack.pop() command.execute() self._undo_stack.append(command) self._execute_log.append({ ‘action’: ‘redo’, ‘command’: command.description, ‘timestamp’: datetime.now().isoformat() }) print(f” ↪ 重做: {command.description}”) return True def undo_many(self, steps: int) -> int: “””批量撤销多步””” count = 0 for _ in range(steps): if self.undo(): count += 1 else: break return count def redo_many(self, steps: int) -> int: “””批量重做多步””” count = 0 for _ in range(steps): if self.redo(): count += 1 else: break return count @property def can_undo(self) -> bool: return len(self._undo_stack) > 0 @property def can_redo(self) -> bool: return len(self._redo_stack) > 0 @property def undo_count(self) -> int: return len(self._undo_stack) @property def redo_count(self) -> int: return len(self._redo_stack) def get_history_summary(self) -> str: “””获取操作历史摘要””” lines = [f”{‘─’ * 50}”, f” 操作历史 (Undo栈: {self.undo_count} | Redo栈: {self.redo_count})”, f”{‘─’ * 50}”] lines.append(” [可撤销操作](从新到旧):”) for cmd in reversed(list(self._undo_stack)): lines.append(f” ↩ {cmd.description}”) if self._redo_stack: lines.append(” [可重做操作](从新到旧):”) for cmd in reversed(list(self._redo_stack)): lines.append(f” ↪ {cmd.description}”) lines.append(f”{‘─’ * 50}”) return ‘n’.join(lines) def export_log(self, filepath: str) -> None: “””导出操作日志到 JSON 文件””” with open(filepath, ‘w’, encoding=’utf-8′) as f: json.dump(self._execute_log, f, ensure_ascii=False, indent=2) print(f” 操作日志已导出至: {filepath}”)
  • class TextEditor: “”” 文本编辑器门面类(Facade) 将 TextBuffer(接收者)+ CommandHistory(调用者)整合为统一 API “”” def __init__(self, filename: str = ‘untitled.txt’): self.buffer = TextBuffer(filename=filename) self.history = CommandHistory(max_history=200) self._clipboard: str = ” # ===== 编辑操作(每个操作都封装为命令)===== def type_text(self, text: str) -> None: “””在当前光标位置输入文本””” cmd = InsertCommand(self.buffer, self.buffer.cursor, text) self.history.execute(cmd) def insert_at(self, pos: int, text: str) -> None: “””在指定位置插入文本””” cmd = InsertCommand(self.buffer, pos, text) self.history.execute(cmd) def delete_range(self, start: int, end: int) -> None: “””删除指定范围的文本””” cmd = DeleteCommand(self.buffer, start, end) self.history.execute(cmd) def replace_range(self, start: int, end: int, new_text: str) -> None: “””替换指定范围的文本””” cmd = ReplaceCommand(self.buffer, start, end, new_text) self.history.execute(cmd) def find_replace(self, pattern: str, replacement: str, use_regex: bool = False) -> None: “””查找并全局替换””” cmd = FindReplaceCommand(self.buffer, pattern, replacement, use_regex) self.history.execute(cmd) def indent_selection(self, start: int, end: int, indent: bool = True) -> None: “””缩进/反缩进选区””” cmd = IndentCommand(self.buffer, start, end, indent) self.history.execute(cmd) def cut(self, start: int, end: int) -> str: “””剪切:删除文本并存入剪贴板””” # 剪切 = 复制 + 删除(两步封装为宏命令,整体可撤销) self._clipboard = self.buffer.content[start:end] cmd = DeleteCommand(self.buffer, start, end) self.history.execute(cmd) print(f” 📋 已剪切 {len(self._clipboard)} 个字符到剪贴板”) return self._clipboard def paste(self, pos: int = None) -> None: “””粘贴:如有选区则先删除再插入(原子宏命令)””” if not self._clipboard: print(” ⚠ 剪贴板为空”) return target_pos = pos if pos is not None else self.buffer.cursor cmd = InsertCommand(self.buffer, target_pos, self._clipboard) self.history.execute(cmd) print(f” 📋 粘贴 {len(self._clipboard)} 个字符”) def move_cursor(self, pos: int) -> None: “””移动光标(不加入 Undo 历史,与主流编辑器一致)””” self.buffer.move_cursor(pos) # ===== Undo/Redo ===== def undo(self, steps: int = 1) -> None: self.history.undo_many(steps) def redo(self, steps: int = 1) -> None: self.history.redo_many(steps) # ===== 显示 ===== def show(self) -> None: print(self.buffer.display()) def show_history(self) -> None: print(self.history.get_history_summary())
  • def demo_text_editor(): print(“=” * 60) print(” Python 文本编辑器 · 命令模式演示”) print(“=” * 60) editor = TextEditor(‘hello_world.py’) # ===== 第一阶段:输入初始代码 ===== print(“n【阶段一:输入代码】”) editor.type_text(‘def hello():n’) editor.type_text(‘ print(“Hello, wrold!”)n’) # 故意拼错 wrold editor.type_text(‘nhello()’) editor.show() # ===== 第二阶段:发现错误,修复拼写 ===== print(“n【阶段二:修复拼写错误】”) editor.find_replace(‘wrold’, ‘world’) editor.show() # ===== 第三阶段:撤销修复(看看原来的错误)===== print(“n【阶段三:撤销查找替换】”) editor.undo() editor.show() # ===== 第四阶段:重做修复 ===== print(“n【阶段四:重做修复】”) editor.redo() editor.show() # ===== 第五阶段:插入注释(宏命令演示)===== print(“n【阶段五:在开头插入文件注释(宏命令)】”) header = ‘# -*- coding: utf-8 -*-n# Author: Clauden# Date: 2025-02-25nn’ macro = MacroCommand([ InsertCommand(editor.buffer, 0, header), ], macro_name=’插入文件头注释’) editor.history.execute(macro) editor.show() # ===== 第六阶段:缩进操作 ===== print(“n【阶段六:对函数体进行缩进调整】”) content = editor.buffer.content body_start = content.index(‘ print’) body_end = body_start + len(‘ print(“Hello, world!”)’) editor.indent_selection(body_start, body_end, indent=True) # ===== 第七阶段:剪切粘贴演示 ===== print(“n【阶段七:剪切并移动代码块】”) content = editor.buffer.content call_start = content.rfind(‘nhello()’) call_end = len(content) editor.cut(call_start, call_end) editor.type_text(‘nnif __name__ == “__main__”:’) editor.type_text(‘n hello()’) editor.show() # ===== 第八阶段:多步撤销 ===== print(“n【阶段八:连续撤销 3 步】”) editor.undo(3) editor.show() # ===== 查看历史 ===== print(“n【操作历史摘要】”) editor.show_history() # ===== 统计信息 ===== print(f”n【统计信息】”) print(f” 当前文本: {len(editor.buffer.content)} 字符”) print(f” 可撤销: {editor.history.undo_count} 步”) print(f” 可重做: {editor.history.redo_count} 步”) demo_text_editor()
  • 真实编辑器中,连续输入字符不会每个字符都生成一条 Undo 记录,而是将短时间内的连续输入合并为一条: import time class MergeableInsertCommand(InsertCommand): “””可合并的插入命令(连续输入自动合并)””” MERGE_INTERVAL = 0.5 # 500ms 内的连续输入合并为一条 def __init__(self, buffer: TextBuffer, pos: int, text: str): super().__init__(buffer, pos, text) self.timestamp = time.monotonic() def try_merge(self, other: ‘MergeableInsertCommand’) -> bool: “”” 尝试与后续命令合并 条件:时间间隔短 + 位置连续 + 不含换行 “”” if not isinstance(other, MergeableInsertCommand): return False time_ok = (other.timestamp – self.timestamp) < self.MERGE_INTERVAL pos_ok = (self.pos + len(self.text) == other.pos) content_ok = ‘n’ not in other.text if time_ok and pos_ok and content_ok: self.text += other.text # 合并文本 self.timestamp = other.timestamp return True return False class SmartCommandHistory(CommandHistory): “””智能历史管理器:支持命令合并””” def execute(self, command: Command) -> None: # 尝试与栈顶命令合并 if (self._undo_stack and isinstance(command, MergeableInsertCommand) and isinstance(self._undo_stack[-1], MergeableInsertCommand)): top = self._undo_stack[-1] if top.try_merge(command): # 合并成功:直接执行而不入栈 command.execute() print(f” ✓ 合并输入: 「{top.text}」”) return super().execute(command)
  • 最佳实践一:命令必须是完全自包含的。 每个命令对象应该包含执行和撤销所需的全部信息,不依赖外部可变状态。 最佳实践二:Undo 必须精确还原,而非近似还原。 特别是光标位置、选区状态,都应在执行前保存,在 Undo 时精确恢复。 最佳实践三:宏命令的异常回滚。 如示例所示,宏命令中任何子步骤失败都应触发已执行步骤的逆序回滚,保证原子性。 常见陷阱一:Redo 栈的清理时机。 执行新命令时必须清空 Redo 栈(主流编辑器行为),否则 Redo 操作会产生歧义。 常见陷阱二:无限历史的内存风险。 使用 collections.deque(maxlen=N) 限制历史深度,或对大操作(如全文替换)进行压缩存储。 设计哲学: 命令模式的核心不是「记录状态」而是「封装意图」。每条命令代表用户的一次有意义操作,Undo 是用户意图的逆转,而不是时间机器。
  • 命令模式通过将操作封装为对象,优雅地解决了撤销/重做这一经典难题。本文构建的完整系统覆盖了以下核心能力: 每种命令(插入、删除、替换、查找替换、缩进)都携带完整的撤销信息;MacroCommand 将多步操作组合为原子单元,整体可撤销;CommandHistory 用双栈模型管理 Undo/Redo,支持批量操作和历史限制;MergeableInsertCommand 实现连续输入的智能合并,贴近真实编辑器体验。 命令模式的应用远不止于文本编辑器——数据库事务、游戏操作回放、GUI 按钮绑定、任务队列调度,处处都是它的身影。一旦你建立起「将操作封装为对象」的思维模式,你会发现很多原本复杂的需求都变得清晰而优雅。 你在项目中实现过撤销功能吗?是用命令模式,还是有其他方案?欢迎在评论区分享你的设计思路,让我们一起探索更优雅的 Python 编程之道。 以上就是基于Python打造支持Undo/Redo的文本编辑器命令系统的详细内容,更多关于Python文本编辑器的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: Python使用Flet打造跨平台文本编辑器 Python ckeditor富文本编辑器代码实例解析 Python如何用wx模块创建文本编辑器 对Python 窗体(tkinter)文本编辑器(Text)详解 Python实现的文本编辑器功能示例 使用Python读写文本文件及编写简单的文本编辑器 python基于Tkinter库实现简单文本编辑器实例 python写的一个文本编辑器
  • 目录
    • 一、引言:你的 Ctrl+Z 背后藏着什么秘密?
    • 二、命令模式基础:四个核心角色
      • 2.1 角色定义
      • 2.2 最小骨架
    • 三、构建文本编辑器核心(接收者)
      • 四、实现完整命令集
        • 4.1 基础文本命令
        • 4.2 高级命令:查找替换
        • 4.3 宏命令:组合多个操作为一个原子单元
      • 五、命令调度中心:历史管理器(Invoker)
        • 六、完整集成:文本编辑器门面类
          • 七、完整演示:从零到成品的编辑过程
            • 八、进阶:命令合并优化(减少 Undo 粒度)
              • 九、最佳实践与常见陷阱
                • 十、总结

                  每天打开代码编辑器,你最常按的快捷键是什么?

                  对大多数开发者来说,Ctrl+Z(撤销)和 Ctrl+Y(重做)可能是仅次于保存的高频操作。写错了一段代码?撤销。误删了一个函数?撤销。一路 Redo 找回刚才的思路?重做。

                  这一切看似魔法的背后,正是软件工程中一个优雅而强大的设计模式:命令模式(Command Pattern)

                  初学者可能会想:撤销不就是「保存历史状态,出错时回滚」吗?用一个列表记录每次操作前的文本不就行了?

                  这种方案在文本量小时可行,但在复杂系统中代价极高——每次操作都复制整个状态,内存消耗随历史深度线性增长,且无法区分「哪些操作可以合并」「哪些操作是原子的」。

                  命令模式的解法更加精妙:将每一个操作封装成独立的对象,对象内同时包含「执行」和「撤销」的逻辑。 撤销不是回滚状态,而是执行「反向命令」——插入的反操作是删除,移动的反操作是移回原位。

                  本文将从零构建一个功能完整的文本编辑器命令系统,覆盖基础命令实现、命令组合、宏命令、历史管理、以及生产级的持久化方案。

                  • Command(抽象命令):定义执行 execute() 和撤销 undo() 的接口。
                  • ConcreteCommand(具体命令):封装一个具体操作及其撤销逻辑,持有接收者的引用。
                  • Receiver(接收者):真正执行业务逻辑的对象(此处为文本编辑器核心)。
                  • Invoker(调用者):持有命令对象,管理命令历史,负责 Undo/Redo 调度。

                  from abc import ABC, abstractmethod
                  
                  class Command(ABC):
                      """抽象命令:所有命令的契约"""
                  
                      @abstractmethod
                      def execute(self) -> None:
                          """执行命令"""
                          pass
                  
                      @abstractmethod
                      def undo(self) -> None:
                          """撤销命令(执行反操作)"""
                          pass
                  
                      @property
                      def description(self) -> str:
                          """命令描述,用于历史记录展示"""
                          return self.__class__.__name__
                  

                  在动手写命令之前,先构建一个功能完备的文本编辑器核心——它是所有命令操作的「接收者」:

                  from dataclasses import dataclass, field
                  from typing import Optional
                  import re
                  
                  @dataclass
                  class TextBuffer:
                      """
                      文本缓冲区:编辑器的核心数据模型
                      支持光标定位、选区操作、文本增删
                      """
                      content: str = ''
                      cursor: int = 0          # 光标位置(字符索引)
                      selection: Optional[tuple[int, int]] = None  # 选区 (start, end)
                      filename: str = 'untitled.txt'
                      modified: bool = False
                  
                      def __post_init__(self):
                          self._validate_cursor()
                  
                      def _validate_cursor(self):
                          self.cursor = max(0, min(self.cursor, len(self.content)))
                  
                      # ===== 基础操作 =====
                      def insert(self, pos: int, text: str) -> None:
                          """在指定位置插入文本"""
                          pos = max(0, min(pos, len(self.content)))
                          self.content = self.content[:pos] + text + self.content[pos:]
                          self.cursor = pos + len(text)
                          self.modified = True
                  
                      def delete(self, start: int, end: int) -> str:
                          """删除 [start, end) 范围的文本,返回被删除内容"""
                          start = max(0, min(start, len(self.content)))
                          end = max(start, min(end, len(self.content)))
                          deleted = self.content[start:end]
                          self.content = self.content[:start] + self.content[end:]
                          self.cursor = start
                          self.modified = True
                          return deleted
                  
                      def replace(self, start: int, end: int, new_text: str) -> str:
                          """替换 [start, end) 范围的文本,返回被替换内容"""
                          old_text = self.delete(start, end)
                          self.insert(start, new_text)
                          return old_text
                  
                      def move_cursor(self, pos: int) -> int:
                          """移动光标,返回旧位置"""
                          old_pos = self.cursor
                          self.cursor = max(0, min(pos, len(self.content)))
                          return old_pos
                  
                      def get_line_at(self, pos: int) -> tuple[int, int, str]:
                          """获取指定位置所在行的 (行首, 行尾, 行内容)"""
                          line_start = self.content.rfind('n', 0, pos) + 1
                          line_end = self.content.find('n', pos)
                          if line_end == -1:
                              line_end = len(self.content)
                          return line_start, line_end, self.content[line_start:line_end]
                  
                      def display(self, show_cursor: bool = True) -> str:
                          """渲染文本(带光标标记)"""
                          if show_cursor:
                              content = (self.content[:self.cursor] + '|' +
                                        self.content[self.cursor:])
                          else:
                              content = self.content
                          lines = content.split('n')
                          result = [f"{'─' * 40}",
                                    f"文件: {self.filename} {'*' if self.modified else ''}",
                                    f"{'─' * 40}"]
                          for i, line in enumerate(lines, 1):
                              result.append(f"{i:3d} │ {line}")
                          result.append(f"{'─' * 40}")
                          result.append(f"光标: {self.cursor} | 字符数: {len(self.content)}")
                          return 'n'.join(result)
                  

                  class InsertCommand(Command):
                      """插入文本命令"""
                  
                      def __init__(self, buffer: TextBuffer, pos: int, text: str):
                          self.buffer = buffer
                          self.pos = pos
                          self.text = text
                          self._old_cursor: int = 0
                  
                      def execute(self) -> None:
                          self._old_cursor = self.buffer.cursor
                          self.buffer.insert(self.pos, self.text)
                  
                      def undo(self) -> None:
                          self.buffer.delete(self.pos, self.pos + len(self.text))
                          self.buffer.cursor = self._old_cursor
                  
                      @property
                      def description(self) -> str:
                          preview = self.text[:20].replace('n', '↵')
                          return f"插入文本: 「{preview}」at pos={self.pos}"
                  
                  
                  class DeleteCommand(Command):
                      """删除文本命令"""
                  
                      def __init__(self, buffer: TextBuffer, start: int, end: int):
                          self.buffer = buffer
                          self.start = start
                          self.end = end
                          self._deleted_text: str = ''
                          self._old_cursor: int = 0
                  
                      def execute(self) -> None:
                          self._old_cursor = self.buffer.cursor
                          self._deleted_text = self.buffer.delete(self.start, self.end)
                  
                      def undo(self) -> None:
                          self.buffer.insert(self.start, self._deleted_text)
                          self.buffer.cursor = self._old_cursor
                  
                      @property
                      def description(self) -> str:
                          preview = self._deleted_text[:20].replace('n', '↵')
                          return f"删除文本: 「{preview}」[{self.start}:{self.end}]"
                  
                  
                  class ReplaceCommand(Command):
                      """替换文本命令(原子操作)"""
                  
                      def __init__(self, buffer: TextBuffer, start: int, end: int, new_text: str):
                          self.buffer = buffer
                          self.start = start
                          self.end = end
                          self.new_text = new_text
                          self._old_text: str = ''
                          self._old_cursor: int = 0
                  
                      def execute(self) -> None:
                          self._old_cursor = self.buffer.cursor
                          self._old_text = self.buffer.replace(self.start, self.end, self.new_text)
                  
                      def undo(self) -> None:
                          self.buffer.replace(self.start,
                                              self.start + len(self.new_text),
                                              self._old_text)
                          self.buffer.cursor = self._old_cursor
                  
                      @property
                      def description(self) -> str:
                          return (f"替换: 「{self._old_text[:15]}」→「{self.new_text[:15]}」"
                                  f"[{self.start}:{self.end}]")
                  
                  
                  class MoveCursorCommand(Command):
                      """移动光标命令"""
                  
                      def __init__(self, buffer: TextBuffer, new_pos: int):
                          self.buffer = buffer
                          self.new_pos = new_pos
                          self._old_pos: int = 0
                  
                      def execute(self) -> None:
                          self._old_pos = self.buffer.move_cursor(self.new_pos)
                  
                      def undo(self) -> None:
                          self.buffer.move_cursor(self._old_pos)
                  
                      @property
                      def description(self) -> str:
                          return f"移动光标: {self._old_pos} → {self.new_pos}"
                  

                  class FindReplaceCommand(Command):
                      """查找并全局替换命令"""
                  
                      def __init__(self, buffer: TextBuffer, pattern: str,
                                   replacement: str, use_regex: bool = False):
                          self.buffer = buffer
                          self.pattern = pattern
                          self.replacement = replacement
                          self.use_regex = use_regex
                          self._original_content: str = ''
                          self._original_cursor: int = 0
                          self._replace_count: int = 0
                  
                      def execute(self) -> None:
                          self._original_content = self.buffer.content
                          self._original_cursor = self.buffer.cursor
                  
                          if self.use_regex:
                              new_content, count = re.subn(
                                  self.pattern, self.replacement, self.buffer.content
                              )
                          else:
                              count = self.buffer.content.count(self.pattern)
                              new_content = self.buffer.content.replace(
                                  self.pattern, self.replacement
                              )
                  
                          self._replace_count = count
                          self.buffer.content = new_content
                          self.buffer.modified = True
                          print(f"  [查找替换] 共替换 {count} 处")
                  
                      def undo(self) -> None:
                          """一键恢复全部替换——这就是命令模式的威力"""
                          self.buffer.content = self._original_content
                          self.buffer.cursor = self._original_cursor
                          self.buffer.modified = True
                  
                      @property
                      def description(self) -> str:
                          mode = "正则" if self.use_regex else "普通"
                          return (f"全局替换({mode}): 「{self.pattern}」→「{self.replacement}」"
                                  f"({self._replace_count}处)")
                  
                  
                  class IndentCommand(Command):
                      """缩进/反缩进命令(对选区内所有行生效)"""
                  
                      def __init__(self, buffer: TextBuffer, start: int, end: int,
                                   indent: bool = True, spaces: int = 4):
                          self.buffer = buffer
                          self.start = start
                          self.end = end
                          self.indent = indent
                          self.spaces = spaces
                          self._original_content: str = ''
                          self._original_cursor: int = 0
                  
                      def execute(self) -> None:
                          self._original_content = self.buffer.content
                          self._original_cursor = self.buffer.cursor
                  
                          lines = self.buffer.content.split('n')
                          # 计算影响的行范围
                          char_count = 0
                          start_line = end_line = 0
                          for i, line in enumerate(lines):
                              if char_count <= self.start:
                                  start_line = i
                              if char_count <= self.end:
                                  end_line = i
                              char_count += len(line) + 1
                  
                          prefix = ' ' * self.spaces
                          for i in range(start_line, end_line + 1):
                              if self.indent:
                                  lines[i] = prefix + lines[i]
                              else:
                                  lines[i] = lines[i].lstrip(' ')[:len(lines[i]) - len(lines[i].lstrip(' '))]
                                  if lines[i].startswith(prefix):
                                      lines[i] = lines[i][self.spaces:]
                                  else:
                                      lines[i] = lines[i].lstrip(' ')
                  
                          self.buffer.content = 'n'.join(lines)
                          self.buffer.modified = True
                  
                      def undo(self) -> None:
                          self.buffer.content = self._original_content
                          self.buffer.cursor = self._original_cursor
                          self.buffer.modified = True
                  
                      @property
                      def description(self) -> str:
                          action = "缩进" if self.indent else "反缩进"
                          return f"{action} {self.spaces}空格 [{self.start}:{self.end}]"
                  

                  class MacroCommand(Command):
                      """
                      宏命令:将多个命令组合成一个可整体撤销的原子操作
                      典型用途:「粘贴」= 先删除选区 + 再插入剪贴板内容
                      """
                  
                      def __init__(self, commands: list[Command], macro_name: str = '宏命令'):
                          self.commands = commands
                          self.macro_name = macro_name
                          self._executed: list[Command] = []
                  
                      def execute(self) -> None:
                          self._executed = []
                          for cmd in self.commands:
                              try:
                                  cmd.execute()
                                  self._executed.append(cmd)
                              except Exception as e:
                                  # 执行失败:回滚已执行的命令
                                  print(f"  ⚠ 宏命令子步骤失败: {e},正在回滚...")
                                  for executed_cmd in reversed(self._executed):
                                      executed_cmd.undo()
                                  self._executed = []
                                  raise
                  
                      def undo(self) -> None:
                          """逆序撤销所有子命令"""
                          for cmd in reversed(self._executed):
                              cmd.undo()
                  
                      @property
                      def description(self) -> str:
                          sub_descs = [f"  · {cmd.description}" for cmd in self.commands]
                          return f"{self.macro_name}({len(self.commands)}步):n" + 'n'.join(sub_descs)
                  

                  from collections import deque
                  import json
                  from datetime import datetime
                  
                  class CommandHistory:
                      """
                      命令历史管理器(Invoker)
                      - 执行命令并记录到 undo 栈
                      - 支持多步 Undo/Redo
                      - 支持历史限制(防止内存溢出)
                      - 支持历史快照导出
                      """
                  
                      def __init__(self, max_history: int = 100):
                          self._undo_stack: deque[Command] = deque(maxlen=max_history)
                          self._redo_stack: deque[Command] = deque(maxlen=max_history)
                          self._max_history = max_history
                          self._execute_log: list[dict] = []
                  
                      def execute(self, command: Command) -> None:
                          """执行命令并推入撤销栈"""
                          command.execute()
                          self._undo_stack.append(command)
                          # 执行新命令后,清空 Redo 栈(与主流编辑器行为一致)
                          self._redo_stack.clear()
                          # 记录执行日志
                          self._execute_log.append({
                              'action': 'execute',
                              'command': command.description,
                              'timestamp': datetime.now().isoformat()
                          })
                          print(f"  ✓ 执行: {command.description}")
                  
                      def undo(self) -> bool:
                          """撤销最近一条命令"""
                          if not self._undo_stack:
                              print("  ⚠ 没有可撤销的操作")
                              return False
                          command = self._undo_stack.pop()
                          command.undo()
                          self._redo_stack.append(command)
                          self._execute_log.append({
                              'action': 'undo',
                              'command': command.description,
                              'timestamp': datetime.now().isoformat()
                          })
                          print(f"  ↩ 撤销: {command.description}")
                          return True
                  
                      def redo(self) -> bool:
                          """重做最近一条被撤销的命令"""
                          if not self._redo_stack:
                              print("  ⚠ 没有可重做的操作")
                              return False
                          command = self._redo_stack.pop()
                          command.execute()
                          self._undo_stack.append(command)
                          self._execute_log.append({
                              'action': 'redo',
                              'command': command.description,
                              'timestamp': datetime.now().isoformat()
                          })
                          print(f"  ↪ 重做: {command.description}")
                          return True
                  
                      def undo_many(self, steps: int) -> int:
                          """批量撤销多步"""
                          count = 0
                          for _ in range(steps):
                              if self.undo():
                                  count += 1
                              else:
                                  break
                          return count
                  
                      def redo_many(self, steps: int) -> int:
                          """批量重做多步"""
                          count = 0
                          for _ in range(steps):
                              if self.redo():
                                  count += 1
                              else:
                                  break
                          return count
                  
                      @property
                      def can_undo(self) -> bool:
                          return len(self._undo_stack) > 0
                  
                      @property
                      def can_redo(self) -> bool:
                          return len(self._redo_stack) > 0
                  
                      @property
                      def undo_count(self) -> int:
                          return len(self._undo_stack)
                  
                      @property
                      def redo_count(self) -> int:
                          return len(self._redo_stack)
                  
                      def get_history_summary(self) -> str:
                          """获取操作历史摘要"""
                          lines = [f"{'─' * 50}",
                                   f"  操作历史 (Undo栈: {self.undo_count} | Redo栈: {self.redo_count})",
                                   f"{'─' * 50}"]
                  
                          lines.append("  [可撤销操作](从新到旧):")
                          for cmd in reversed(list(self._undo_stack)):
                              lines.append(f"    ↩ {cmd.description}")
                  
                          if self._redo_stack:
                              lines.append("  [可重做操作](从新到旧):")
                              for cmd in reversed(list(self._redo_stack)):
                                  lines.append(f"    ↪ {cmd.description}")
                  
                          lines.append(f"{'─' * 50}")
                          return 'n'.join(lines)
                  
                      def export_log(self, filepath: str) -> None:
                          """导出操作日志到 JSON 文件"""
                          with open(filepath, 'w', encoding='utf-8') as f:
                              json.dump(self._execute_log, f, ensure_ascii=False, indent=2)
                          print(f"  操作日志已导出至: {filepath}")
                  

                  class TextEditor:
                      """
                      文本编辑器门面类(Facade)
                      将 TextBuffer(接收者)+ CommandHistory(调用者)整合为统一 API
                      """
                  
                      def __init__(self, filename: str = 'untitled.txt'):
                          self.buffer = TextBuffer(filename=filename)
                          self.history = CommandHistory(max_history=200)
                          self._clipboard: str = ''
                  
                      # ===== 编辑操作(每个操作都封装为命令)=====
                  
                      def type_text(self, text: str) -> None:
                          """在当前光标位置输入文本"""
                          cmd = InsertCommand(self.buffer, self.buffer.cursor, text)
                          self.history.execute(cmd)
                  
                      def insert_at(self, pos: int, text: str) -> None:
                          """在指定位置插入文本"""
                          cmd = InsertCommand(self.buffer, pos, text)
                          self.history.execute(cmd)
                  
                      def delete_range(self, start: int, end: int) -> None:
                          """删除指定范围的文本"""
                          cmd = DeleteCommand(self.buffer, start, end)
                          self.history.execute(cmd)
                  
                      def replace_range(self, start: int, end: int, new_text: str) -> None:
                          """替换指定范围的文本"""
                          cmd = ReplaceCommand(self.buffer, start, end, new_text)
                          self.history.execute(cmd)
                  
                      def find_replace(self, pattern: str, replacement: str,
                                       use_regex: bool = False) -> None:
                          """查找并全局替换"""
                          cmd = FindReplaceCommand(self.buffer, pattern, replacement, use_regex)
                          self.history.execute(cmd)
                  
                      def indent_selection(self, start: int, end: int, indent: bool = True) -> None:
                          """缩进/反缩进选区"""
                          cmd = IndentCommand(self.buffer, start, end, indent)
                          self.history.execute(cmd)
                  
                      def cut(self, start: int, end: int) -> str:
                          """剪切:删除文本并存入剪贴板"""
                          # 剪切 = 复制 + 删除(两步封装为宏命令,整体可撤销)
                          self._clipboard = self.buffer.content[start:end]
                          cmd = DeleteCommand(self.buffer, start, end)
                          self.history.execute(cmd)
                          print(f"  📋 已剪切 {len(self._clipboard)} 个字符到剪贴板")
                          return self._clipboard
                  
                      def paste(self, pos: int = None) -> None:
                          """粘贴:如有选区则先删除再插入(原子宏命令)"""
                          if not self._clipboard:
                              print("  ⚠ 剪贴板为空")
                              return
                          target_pos = pos if pos is not None else self.buffer.cursor
                          cmd = InsertCommand(self.buffer, target_pos, self._clipboard)
                          self.history.execute(cmd)
                          print(f"  📋 粘贴 {len(self._clipboard)} 个字符")
                  
                      def move_cursor(self, pos: int) -> None:
                          """移动光标(不加入 Undo 历史,与主流编辑器一致)"""
                          self.buffer.move_cursor(pos)
                  
                      # ===== Undo/Redo =====
                      def undo(self, steps: int = 1) -> None:
                          self.history.undo_many(steps)
                  
                      def redo(self, steps: int = 1) -> None:
                          self.history.redo_many(steps)
                  
                      # ===== 显示 =====
                      def show(self) -> None:
                          print(self.buffer.display())
                  
                      def show_history(self) -> None:
                          print(self.history.get_history_summary())
                  

                  def demo_text_editor():
                      print("=" * 60)
                      print("    Python 文本编辑器 · 命令模式演示")
                      print("=" * 60)
                  
                      editor = TextEditor('hello_world.py')
                  
                      # ===== 第一阶段:输入初始代码 =====
                      print("n【阶段一:输入代码】")
                      editor.type_text('def hello():n')
                      editor.type_text('    print("Hello, wrold!")n')   # 故意拼错 wrold
                      editor.type_text('nhello()')
                      editor.show()
                  
                      # ===== 第二阶段:发现错误,修复拼写 =====
                      print("n【阶段二:修复拼写错误】")
                      editor.find_replace('wrold', 'world')
                      editor.show()
                  
                      # ===== 第三阶段:撤销修复(看看原来的错误)=====
                      print("n【阶段三:撤销查找替换】")
                      editor.undo()
                      editor.show()
                  
                      # ===== 第四阶段:重做修复 =====
                      print("n【阶段四:重做修复】")
                      editor.redo()
                      editor.show()
                  
                      # ===== 第五阶段:插入注释(宏命令演示)=====
                      print("n【阶段五:在开头插入文件注释(宏命令)】")
                      header = '# -*- coding: utf-8 -*-n# Author: Clauden# Date: 2025-02-25nn'
                      macro = MacroCommand([
                          InsertCommand(editor.buffer, 0, header),
                      ], macro_name='插入文件头注释')
                      editor.history.execute(macro)
                      editor.show()
                  
                      # ===== 第六阶段:缩进操作 =====
                      print("n【阶段六:对函数体进行缩进调整】")
                      content = editor.buffer.content
                      body_start = content.index('    print')
                      body_end = body_start + len('    print("Hello, world!")')
                      editor.indent_selection(body_start, body_end, indent=True)
                  
                      # ===== 第七阶段:剪切粘贴演示 =====
                      print("n【阶段七:剪切并移动代码块】")
                      content = editor.buffer.content
                      call_start = content.rfind('nhello()')
                      call_end = len(content)
                      editor.cut(call_start, call_end)
                      editor.type_text('nnif __name__ == "__main__":')
                      editor.type_text('n    hello()')
                      editor.show()
                  
                      # ===== 第八阶段:多步撤销 =====
                      print("n【阶段八:连续撤销 3 步】")
                      editor.undo(3)
                      editor.show()
                  
                      # ===== 查看历史 =====
                      print("n【操作历史摘要】")
                      editor.show_history()
                  
                      # ===== 统计信息 =====
                      print(f"n【统计信息】")
                      print(f"  当前文本: {len(editor.buffer.content)} 字符")
                      print(f"  可撤销: {editor.history.undo_count} 步")
                      print(f"  可重做: {editor.history.redo_count} 步")
                  
                  demo_text_editor()
                  

                  真实编辑器中,连续输入字符不会每个字符都生成一条 Undo 记录,而是将短时间内的连续输入合并为一条:

                  import time
                  
                  class MergeableInsertCommand(InsertCommand):
                      """可合并的插入命令(连续输入自动合并)"""
                      MERGE_INTERVAL = 0.5  # 500ms 内的连续输入合并为一条
                  
                      def __init__(self, buffer: TextBuffer, pos: int, text: str):
                          super().__init__(buffer, pos, text)
                          self.timestamp = time.monotonic()
                  
                      def try_merge(self, other: 'MergeableInsertCommand') -> bool:
                          """
                          尝试与后续命令合并
                          条件:时间间隔短 + 位置连续 + 不含换行
                          """
                          if not isinstance(other, MergeableInsertCommand):
                              return False
                          time_ok = (other.timestamp - self.timestamp) < self.MERGE_INTERVAL
                          pos_ok = (self.pos + len(self.text) == other.pos)
                          content_ok = 'n' not in other.text
                          if time_ok and pos_ok and content_ok:
                              self.text += other.text  # 合并文本
                              self.timestamp = other.timestamp
                              return True
                          return False
                  
                  
                  class SmartCommandHistory(CommandHistory):
                      """智能历史管理器:支持命令合并"""
                  
                      def execute(self, command: Command) -> None:
                          # 尝试与栈顶命令合并
                          if (self._undo_stack and
                                  isinstance(command, MergeableInsertCommand) and
                                  isinstance(self._undo_stack[-1], MergeableInsertCommand)):
                              top = self._undo_stack[-1]
                              if top.try_merge(command):
                                  # 合并成功:直接执行而不入栈
                                  command.execute()
                                  print(f"  ✓ 合并输入: 「{top.text}」")
                                  return
                          super().execute(command)
                  

                  最佳实践一:命令必须是完全自包含的。 每个命令对象应该包含执行和撤销所需的全部信息,不依赖外部可变状态。

                  最佳实践二:Undo 必须精确还原,而非近似还原。 特别是光标位置、选区状态,都应在执行前保存,在 Undo 时精确恢复。

                  最佳实践三:宏命令的异常回滚。 如示例所示,宏命令中任何子步骤失败都应触发已执行步骤的逆序回滚,保证原子性。

                  常见陷阱一:Redo 栈的清理时机。 执行新命令时必须清空 Redo 栈(主流编辑器行为),否则 Redo 操作会产生歧义。

                  常见陷阱二:无限历史的内存风险。 使用 collections.deque(maxlen=N) 限制历史深度,或对大操作(如全文替换)进行压缩存储。

                  设计哲学: 命令模式的核心不是「记录状态」而是「封装意图」。每条命令代表用户的一次有意义操作,Undo 是用户意图的逆转,而不是时间机器。

                  命令模式通过将操作封装为对象,优雅地解决了撤销/重做这一经典难题。本文构建的完整系统覆盖了以下核心能力:

                  每种命令(插入、删除、替换、查找替换、缩进)都携带完整的撤销信息;MacroCommand 将多步操作组合为原子单元,整体可撤销;CommandHistory 用双栈模型管理 Undo/Redo,支持批量操作和历史限制;MergeableInsertCommand 实现连续输入的智能合并,贴近真实编辑器体验。

                  命令模式的应用远不止于文本编辑器——数据库事务、游戏操作回放、GUI 按钮绑定、任务队列调度,处处都是它的身影。一旦你建立起「将操作封装为对象」的思维模式,你会发现很多原本复杂的需求都变得清晰而优雅。

                  你在项目中实现过撤销功能吗?是用命令模式,还是有其他方案?欢迎在评论区分享你的设计思路,让我们一起探索更优雅的 Python 编程之道。

                  以上就是基于Python打造支持Undo/Redo的文本编辑器命令系统的详细内容,更多关于Python文本编辑器的资料请关注风君子博客其它相关文章!

                  您可能感兴趣的文章:

                  • Python使用Flet打造跨平台文本编辑器
                  • Python ckeditor富文本编辑器代码实例解析
                  • Python如何用wx模块创建文本编辑器
                  • 对Python 窗体(tkinter)文本编辑器(Text)详解
                  • Python实现的文本编辑器功能示例
                  • 使用Python读写文本文件及编写简单的文本编辑器
                  • python基于Tkinter库实现简单文本编辑器实例
                  • python写的一个文本编辑器

                  站内搜索