10 KiB
NewEditor Window Workspace Single-Source Refactor Plan
Date: 2026-04-22 Status: In Progress
Progress
Completed on 2026-04-22:
EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController()no longer reads mutation input fromEditorWindowWorkspaceStoremirror- mutation input is now built from live
EditorWindowcontrollers at call time - Debug build of
XCUIEditorApppassed - manual verification passed for:
- detach
- re-dock
- open / close baseline behavior
- detached title baseline behavior
Next:
- split detached title refresh from frame-time projection
- then remove the remaining per-frame mirror commit path
1. Objective
这份计划只处理 new_editor 当前最严重的一处结构性冗余:
- 每个
EditorWindow已经各自持有一份 liveUIEditorWorkspaceController EditorWindowWorkspaceStore又额外持有一份完整的UIEditorWindowWorkspaceSet- 宿主层每帧把前者整份投影回后者
- 跨窗口操作再从后者反向重建 controller 灌回窗口
本计划的目标不是“看起来更抽象”,而是把窗口 workspace 的单一事实源收回到 live window,自根源消除双写状态同步环,同时不破坏以下现有行为:
- 分离窗口打开与关闭
- Tab 跨窗口拖拽
- 面板 detach / re-dock
- detached tool window 标题与尺寸策略
- 现有
UIEditorWindowWorkspaceController上的跨窗口变换语义
2. Confirmed Problem
当前重复链路已经形成完整闭环:
- live state:
EditorWindowRuntimeController::m_workspaceController
- mirrored state:
EditorWindowWorkspaceStore::m_windowSet
- frame-time projection:
EditorWindowHostRuntime::RenderAllWindows(...)EditorWindowWorkspaceCoordinator::CommitWindowProjection(...)
- mutation-time reverse application:
EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController()EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet(...)
这不是普通 cache,而是双向同步的第二套真相源。
3. Root Cause
根因不在某一个函数,而在宿主层把“跨窗口协调”实现成了“跨窗口状态镜像”。
当前设计默认:
- window 内的 live controller 负责真实交互与更新
- store 内的 window set 负责跨窗口 mutation 的输入
- 二者通过每帧投影保持近似一致
这导致:
- 同一语义的 workspace/session 状态有两份 owner
- 每帧发生整份复制和整套验证
- mutation 逻辑被迫依赖 mirror,而不是直接面向 live windows
- 标题刷新、窗口集合描述、关闭路径,也被顺带绑在 mirror 上
4. Why This Is The Most Serious Redundancy
这处问题同时命中四个严重特征:
- 同一职责重复实现
- live window controller 和 mirrored window set 都在表达同一份窗口 workspace 状态
- 同一帧重复计算
- 每帧
CommitWindowProjection(...)都会复制m_windowSet、覆写某个窗口状态、再验证整套 window set
- 每帧
- 结构扩散
- 打开窗口、关闭窗口、跨窗口拖拽、标题刷新都被迫围绕 mirror 组织
- 没有独立 owner
EditorWindowWorkspaceStore并没有独立业务语义,只是在宿主层替 live state 保存一份副本
5. Red Lines
这次重构明确禁止以下做法:
- 只删除
CommitWindowProjection(...),但继续让 mutation 从旧 store 读数据 - 先删 store,再临时把跨窗口逻辑散落回
EditorWindowWorkspaceCoordinator - 改写
UIEditorWindowWorkspaceController的跨窗口变换语义 - 修改 dock / transfer / extract 的业务规则,只为了配合结构调整
- 以“后面再补验证”为理由跳过 detached window / global tab drag 的回归验证
其中第 1 条最危险。它一定会造成功能回归,因为当前 detached 窗口标题刷新和 mutation 输入都还绑在 projection 路径上。
6. Target End State
目标终态必须满足:
- live
EditorWindow持有的UIEditorWorkspaceController成为唯一可变真相源 - 跨窗口 mutation 需要的
UIEditorWindowWorkspaceSet改为按需从 live windows 现采样构建 EditorWindowWorkspaceStore不再保存整份workspace/session- detached 窗口标题刷新从 projection 逻辑中独立出来
- primary / active / existing window 集合信息只保留真正被消费的最小元数据
允许两种落地形式:
- 保留
EditorWindowWorkspaceStore,但只保留窗口元数据和注册信息,不再保存 workspace/session - 完全删除
EditorWindowWorkspaceStore,由 coordinator 直接从 host runtime 构建 snapshot
7. Refactor Strategy
Phase A. Freeze semantics and add live snapshot builder
先新增从 live windows 构建 UIEditorWindowWorkspaceSet 的单一路径,例如:
BuildLiveWindowWorkspaceSnapshot()- 或
BuildWorkspaceMutationControllerFromLiveWindows()
要求:
- snapshot 必须完整覆盖所有活跃窗口
- 每个窗口的 workspace/session 直接来自 live controller
- primary window id 必须来自 live host state
- active window id 只保留当前真实需要的最小语义
这一步先增加,不删旧路径。
Phase B. Make mutation read from live snapshot instead of store mirror
把以下入口改为依赖 live snapshot:
BuildWorkspaceMutationController()TryStartGlobalTabDrag(...)TryProcessDetachRequest(...)TryProcessOpenDetachedPanelRequest(...)- cross-window drop commit path
要求:
- mutation 前只从 live windows 取状态
- mutation 后仍复用
SynchronizeWindowsFromWindowSet(...)回灌窗口 - 在这一阶段仍允许 store 暂时存在,但不能再是 mutation 输入源
Phase C. Split title refresh from projection
当前 RefreshWindowTitle(...) 被挂在 CommitWindowProjection(...) 后面,这是一种错误耦合。
必须单独拆出标题同步路径:
- detached 窗口标题直接基于 live controller 刷新
- 标题刷新继续允许 frame-time 执行
- 但标题刷新不能再隐含 workspace mirror commit
完成后,删除“为了标题而保留每帧 projection”的借口。
Phase D. Remove mirrored workspace/session from store
在 mutation 输入已经切换到 live snapshot 后:
- 删除
EditorWindowWorkspaceStore中的workspace/session镜像 - 删除
BuildWindowState(...)这类整份复制逻辑 - 删除每帧
CommitWindowProjection(...) - 删除围绕 projection 的整套验证与回退路径
如果此时 store 还存在,它只能保存:
- 已注册窗口 id
- primary window id
- 可能仍有意义的轻量元数据
8. Functional Risk Analysis
这次重构不是零风险,但风险集中且可控。
8.1 Real risk: title synchronization
如果直接删除每帧 projection,而不拆标题刷新:
- detached 窗口标题可能停留在旧 active panel
- 同一窗口内切 tab 后标题可能不再更新
因此标题刷新必须先和 projection 解耦,再删 projection。
8.2 Real risk: mutation input staleness
如果 mutation 仍从 store 读状态,而 store 不再每帧同步:
- detach 可能从旧 workspace 提取 panel
- cross-window drop 可能基于过期 session 判断 panel owner
- open detached panel 可能错误判断某 panel 已经打开或未打开
因此必须先引入 live snapshot builder,再切 mutation 输入。
8.3 Medium risk: lifecycle and close path assumptions
当前 IsPrimaryWindowId(...)、DescribeWindowSet()、RemoveWindowProjection(...) 都依赖 store。
需要逐条确认:
- 哪些只用于日志
- 哪些只用于关闭路径
- 哪些真影响行为
如果不区分,容易把“日志依赖”误当成“结构依赖”。
8.4 Low risk: cross-window mutation algorithm itself
UIEditorWindowWorkspaceController 和 UIEditorWorkspaceTransfer 负责的是跨窗口 panel 变换语义。
本计划不改:
- extract 规则
- insert 规则
- dock 规则
- detached workspace 生成规则
因此只要 mutation 输入快照正确,这一层不是主要风险源。
9. Why This Refactor Is Actually Simplifying
它不是表面简化,而是实质上减少状态 owner。
完成后会得到:
- 窗口 workspace 只在 live window 内持有一份
- mutation 输入不再依赖长期镜像,而是按需快照
- frame path 不再做整份 workspace/session 复制与整套 validate
- coordinator 的职责变成:
- 从 live windows 取快照
- 调用 mutation controller
- 把 mutation 结果同步回 live windows
- title refresh 变成一个独立的小职责,而不是夹带在 projection 里
也就是说,复杂度不是被藏起来,而是真的减少了一层。
10. Why This Refactor Is Root-Cause-Oriented
它之所以是从根源出发,是因为它处理的是“谁拥有窗口 workspace 真相”。
如果不解决 owner 问题,只做局部删减,最终都会回到:
- 某处还需要一份 mirror
- 某帧还得再同步一次
- 某条宿主功能又悄悄绑回 projection
而这份计划直接把真相源收回 live window,把跨窗口 controller 降级为临时 snapshot consumer,这才是对根因动刀。
11. Validation Requirements
没有以下验证,不允许落地删除 projection:
- detached window 内切换 active panel 后,窗口标题实时更新
- primary window detach panel 正常创建新窗口
- detached window 再次拖拽 panel 发起 global tab drag 正常
- 跨窗口 drop 到 tab stack 和 relative dock 都正常
- 已打开 tool window 的 panel 重复打开请求仍然正确复用
- 关闭 detached window 后,其余窗口状态保持正确
- 关闭 primary window 时,其他窗口生命周期不异常
XCUIEditorAppDebug 构建通过
12. Completion Criteria
只有满足以下条件,这次重构才算真正完成:
EditorWindowWorkspaceStore不再持有整份workspace/session镜像EditorWindowHostRuntime::RenderAllWindows(...)不再每帧调用 workspace projection commit- 所有跨窗口 mutation 都从 live snapshot 构建输入
- detached 标题刷新与 projection 彻底解耦
- 现有 detach / drag / re-dock / close 行为无回归
13. Final Statement
这次重构不能靠“删几行同步代码”完成。
真正安全、真正彻底的路径只有一条:
- 先建立 live snapshot 输入
- 再把 mutation 输入切过去
- 再拆标题刷新
- 最后删除 mirror
只有按这个顺序,才能既不破坏功能,又从根上消灭这套双写宿主残片。