new_editor: build window mutation input from live state

This commit is contained in:
2026-04-22 17:49:38 +08:00
parent 1a3b3577fd
commit 6d97f4fc3a
245 changed files with 322 additions and 1 deletions

View File

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