ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

gpui-kit 历史模块拆分设计:从单一 History 到 History 导航轨迹与 UndoHistory 撤销事务

gpui-kit 历史模块拆分设计:从单一 History 到 History 导航轨迹与 UndoHistory 撤销事务 gpui-kit 历史模块拆分设计从单一 History 到 History 导航轨迹与 UndoHistory 撤销事务【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kitgpui-kit 的gpui-base曾用一个HistoryT同时承担“撤销/重做存储”和“导航轨迹”两种截然不同的职责。本设计文档history-split-design记录了将其拆分为HistoryT浏览器式线性导航轨迹和UndoHistoryT分组撤销事务的完整决策、公开 API、语义边界与验证方案。读完后你将理解两类历史数据的契约差异、各自 API 的精确行为含边界条件以及它们在NavStack、Dock 面板和 Input 中的真实落地方式。为什么必须拆分一个 History 掩盖了契约错配设计文档的 Context 部分指出了问题的根源公开的HistoryI最初是一个 undo/redo 存储它要求每个条目携带版本号并暴露 grouping分组、ignore忽略、undo-stack撤销栈、redo-stack重做栈等概念。拆分前三类消费者对它的用法已经分化为三种契约Input早已不再使用它——输入组件拥有自己私有的、感知事务边界的UndoManagerDock仍把HistoryTileChange当作 undo 存储使用NavStack则把它当导航轨迹使用并把undo翻译成pop、把redo翻译成forward。这两种用法存在本质矛盾撤销要返回“跨越的变更”调用者据此回滚或重放导航要返回“到达的位置”且必须保留根节点。如果只是在同一个类型上重命名方法只会把这种错配隐藏起来。这就是拆分的直接动机。拆分决策两个独立的公开数据结构决策非常明确从gpui-base导出两个相互独立的公开类型并通过 legacy 路径gpui-component::history模块同时可用HistoryT—— 浏览器式的线性轨迹带一个 current entryUndoHistoryT—— 分组变更日志带 undo 与 redo 事务。这是一次破坏性的 API 重设计旧的HistoryItemtrait 和旧HistoryAPI 被直接移除不提供 deprecate 兼容别名。这个决策的意义在于——旧接口要求T实现伴生 trait携带版本号而新设计中版本/分组元数据完全内化为实现细节T可以是任意类型。导出路径在仓库中有两处实证lib.rs 中pub use history::History与pub use undo_history::UndoHistory以及 history.rs 中的一行pub use gpui_base::{History, UndoHistory};即gpui_component::history::{History, UndoHistory}与gpui_base::{History, UndoHistory}双路径均可用。History浏览器式线性导航轨迹数据结构与构造HistoryT内部保存两个栈entries从根到当前条目的 back 栈和forward_entries由back留下的 forward 栈外加max_entries容量上限。在 history.rs 中可以看到结构定义#[derive(Debug)] pub struct HistoryT { entries: VecT, forward_entries: VecT, max_entries: usize, }T: Clone只在返回拥有权条目的操作back/forward上要求其余操作不强制。完整公开 API 如下History::new() History::max_entries(usize) push(T) current() - OptionT replace_current(T) remove_current() - OptionT can_back() - bool can_forward() - bool back() - OptionT forward() - OptionT entries() - DoubleEndedIteratorItem T forward_entries() - DoubleEndedIteratorItem T retain(impl FnMut(T) - bool) clear()几个值得注意的默认值与边界行为均能在源码中直接验证new()的max_entries默认1000max_entries(n)是消费式 buildermut self - Self降低上限会立即驱逐最旧的条目enforce_max_entries从entries头部drain零条目上限是 no-op 而非 panicmax_entries(0)时push直接返回测试zero_max_entries_retains_nothing验证了此时current()为None、can_back/can_forward均为 falseback()永不越过根节点entries.len() 1时返回Noneforward()在max_entries 0时返回None恢复后同样受容量约束。导航语义设计文档为每个行为规定了精确语义与 history.rs 的实现一一对应方法语义push(entry)令entry成为当前条目并丢弃整个 forward 分支对应浏览器打开新页面后前进历史失效back()保留根节点返回移动后的新当前条目forward()返回被恢复的条目本身replace_current(entry)原地替换当前条目轨迹为空时退化为 pushremove_current()只移除当前条目若有前一条则使其显现forward 分支保持完好entries()从根到当前条目迭代因此.rev()即从当前到根forward_entries()从最近的 forward 条目到最远的迭代retain(keep)同时过滤 back 侧与 forward 侧不改变顺序clear()清空当前、back、forward 全部条目一个关键的设计取舍History不对相等条目去重。导航轨迹必须保留A - B - A这样的往返——去重MRU 行为既不是导航语义也不是撤销语义调用者如需抑制未变化的 current 条目应自行处理。测试repeated_entries_preserve_every_navigation_step明确验证了 push 三次[1, 2, 1]后每个导航步骤都完整保留。典型用法与 history 使用文档 中的示例一致let mut history History::new(); history.push(A); history.push(B); history.push(C); assert_eq!(history.back(), Some(B)); assert_eq!(history.current(), Some(B));测试覆盖history.rs 内的单测覆盖了设计文档 Verification 一节列出的全部场景导航根边界navigation_moves_between_entries_without_backing_past_the_root、分支截断pushing_after_back_truncates_the_forward_branch、重复条目、容量驱逐max_entries_evicts_the_oldest_entry、降低上限的即时截断lowering_max_entries_truncates_populated_entries_and_caps_forward_restores、零容量、原地替换、移除当前、retain 双向过滤、clear。UndoHistory分组撤销与重做事务事务模型UndoHistoryT拥有 undo 与 redo 两个事务栈。版本/分组元数据是实现细节因此T无需实现任何伴生 trait。在 undo_history.rs 中核心状态为pub struct UndoHistoryT { undos: VecVecT, redos: VecVecT, last_changed_at: OptionInstant, max_undos: usize, group_interval: OptionDuration, grouping: bool, ignoring: bool, }公开 APIUndoHistory::new() UndoHistory::max_undos(usize) UndoHistory::group_interval(Duration) push(T) undo() - OptionVecT redo() - OptionVecT can_undo() - bool can_redo() - bool start_grouping() end_grouping() is_ignoring() - bool set_ignoring(bool) clear()语义规则与设计文档逐条对应每次未分组的 push 就是一个独立事务计时分组group_interval或显式分组start_grouping/end_grouping让 push 追加进当前事务而不是新开事务undo()返回事务内变更按“最新在前”排序调用者据此反向撤销redo()按“最旧在前”返回调用者按原顺序重放。测试explicit_grouping_undoes_newest_first_and_redoes_oldest_first直接断言了分组 push[1, 2, 3]后undo() [3, 2, 1]、redo() [1, 2, 3]undo 之后 push 会清空 redo 分支a_new_push_clears_redoignore 模式下push为 no-opignoring_drops_pushes——重放变更期间用set_ignoring(true)防止重放本身被记录零撤销上限不保留任何事务zero_max_undos_retains_no_transactionsmax_undos默认同样是 1000。一个容易踩坑的时序细节group_interval的计时分组窗口在一次成功的 undo 或 redo 之后终止undo/redo都会把last_changed_at置为None所以下一次 push 必然开启新事务而显式分组独立于计时窗口即使 undo 之后处于start_grouping状态push 仍会追加到当前事务——测试undo_breaks_timed_grouping_across_the_branch_boundary与explicit_grouping_still_appends_after_undo分别锁定了这两个行为。let mut history UndoHistory::new(); history.start_grouping(); history.push(move from x0 to x10); history.push(move from x10 to x20); history.end_grouping(); assert_eq!( history.undo(), Some(vec![move from x10 to x20, move from x0 to x10]), ); assert_eq!( history.redo(), Some(vec![move from x0 to x10, move from x10 to x20]), );另一个被明确删除的特性是旧版的unique选项把相等条目重新排序属于 MRU 列表行为不是导航或撤销行为且在仓库中没有生产消费者。测试覆盖undo_history.rs 的单测覆盖单事务、计时分组与显式分组、undo/redo 顺序、redo 截断、ignore 模式、容量驱逐与下调上限的即时性lowering_max_undos_evicts_oldest_populated_transactions_immediately、零上限下 redo 仍保留事务可用性redo_at_zero_max_undos_keeps_the_transaction_available。消费者迁移NavStack、Dock 与 Input设计文档为每类消费者规定了迁移路径仓库源码中均已落地NavStack ——HistoryNavEntry。nav_stack.rs 中NavStackState持有history: HistoryNavEntry其pop调用back、forward调用forwardviews()与forward_views()直接使用entries()/forward_entries()迭代器 API。源码注释还点明了行为对标pop保留根页面同 QtStackView与 UIKit 导航控制器push 后丢弃 forward 分支同 WinUI 的BackStack/ForwardStack深度大于 1 时显示返回按钮。Dock ——UndoHistoryTileChange。tiles_state.rs 中TilesState持有history: UndoHistoryTileChange并设置了group_interval(Duration::from_millis(100))——把 100ms 内的连续拖拽/调整大小变更合并为一个可撤销手势push_completed_gesture将一次完成的手势推入 undo 栈。其undo()遍历返回的变更最新在前读取old_bounds()回滚redo()读取new_bounds()重放两者都通过TilesEvent::BoundsChanged事件通知视图刷新——这正是“undo 返回最新在前、redo 返回最旧在前”契约在真实组件中的消费方式。Input —— 移除陈旧实现。输入组件中过时的HistoryItem for Change实现和版本号字段被删除其私有的、感知事务输入、删除、选区、IME 组合的UndoManager保持不变。这印证了设计文档的判断当分组的语义比“时间窗口或显式边界”更丰富时应当使用领域专用管理器而不是通用历史结构。兼容性验证由于是破坏性重设计验证策略分三层全部可在仓库中复核单元测试如上两节所述History与UndoHistory各自的行为测试覆盖设计文档 Verification 一节列出的全部场景现有消费者测试Dock 与 NavStack 的既有测试在迁移后必须通过兼容导出测试base_compat.rs 中的legacy_history_path_reexports_the_base_type断言两个类型都能经由gpui-component重新导出fn legacy_history_path_reexports_the_base_type() { let _: gpui_component::history::Historyu8 gpui_base::History::new(); let _: gpui_component::history::UndoHistoryu8 gpui_base::UndoHistory::new(); }完成标准是格式检查、针对性的gpui-base与兼容性测试、以及整个 workspace 的 check 全部通过。选型指南按状态语义而不是按按钮名称最后把设计文档隐含的选型原则显式化——选哪个类型取决于状态的语义而不是操作它的 UI 命令叫什么名字判断依据选择每个条目是一个“位置”后退/前进要返回到达的位置且根必须始终可用HistoryT每个条目是一个“可逆变更”undo/redo 要一次性返回一个用户事务内的全部变更UndoHistoryT分组依赖时间或显式边界之外的更丰富语义如输入、选区、IME领域专用管理器参考 Input 的私有UndoManager值得注意的是NavStack的pop按钮和 Dock 的undo按钮在 UI 上都叫“后退/撤销”但底层契约完全不同前者操作位置轨迹后者操作变更事务。这正是这次拆分要消除的歧义——当语义不同就让类型不同。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表