Files
XCEngine/docs/plan/NewEditor_WindowWorkspaceSingleSourceRefactorPlan_2026-04-22.md

10 KiB
Raw Blame History

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

UIEditorWindowWorkspaceControllerUIEditorWorkspaceTransfer 负责的是跨窗口 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

只有按这个顺序,才能既不破坏功能,又从根上消灭这套双写宿主残片。