第 11 章:Workspace、Action 与 Settings——产品外壳怎样保持可组合
Workspace 是一个窗口中的产品级所有权根:一个 Project、中心 PaneGroup、三个 Dock、状态栏、 item registry、导航历史与持久化。Action 负责意图路由,Settings 负责分层配置合并。
1. Workspace 不等于 Project
Project 表示工程语义,Workspace 表示窗口布局与交互状态。同一个 Project 可以被不同窗口打开, 一个 Workspace 也可能包含多个 worktree。
Workspace 源码注释定义了典型组成:Project、central pane group、3 docks、status bar。 源码:crates/workspace/src/workspace.rs:1366-1372。
2. Item:中心区域的共同协议
Editor、ProjectSearch、Settings、Markdown Preview、Git Diff、Terminal(center mode)都可成为 Item。 Item trait 组合:
- Focusable;
- EventEmitter;
- Render;
- tab title/icon/dirty state;
- save/reload/navigation hooks;
- clone/split capability;
- optional serialization。
源码入口:crates/workspace/src/item.rs:170。
ItemHandle 做类型擦除,让 Pane 能持有异构 item,同时保留 downcast 到具体 Entity 的能力。
3. Pane 与 PaneGroup
Pane 管一组 tabs:
- active item;
- preview item;
- pinned/dirty tabs;
- activation history;
- drag/drop;
- close/save prompt;
- zoom state。
PaneGroup 是 split tree:叶子是 Pane,内部节点是水平/垂直 axis 和 flex。拆分编辑器不是创建 特殊 window,而是修改这棵布局树。
4. Dock 与 Panel
左、右、下 Dock 持有实现 Panel trait 的实体,例如 ProjectPanel、AgentPanel、GitPanel、 TerminalPanel。Panel 提供:
- position/size/default width;
- activation/focus;
- icon/tooltip;
- zoom 和 persistence id。
源码:crates/workspace/src/dock.rs:36-105。
Item 和 Panel 分开,避免“工具面板”和“主文档 tab”共享一堆不自然的行为。
5. 打开路径时谁决定用哪种 Item
Workspace 不硬编码所有文件类型。ProjectItemRegistry 保存 open handler,按注册逆序让每种 Item 尝试处理目标:图片、CSV、Markdown preview 或普通 Editor。
源码注释:crates/workspace/src/workspace.rs:961-963。
扩展一个新 viewer 的关键不是改 Workspace::open_path 巨型 match,而是注册新的 ProjectItem。
6. Action 从窗口全局到焦点局部
Action dispatch 沿焦点树冒泡:
Keymap 匹配 Action
→ 当前 focused element handler
→ Editor/Panel handler
→ Pane handler
→ Workspace handler
→ App global handler于是 cancel 在 completion menu、Editor、modal、Workspace 中可以有不同含义;最靠近焦点且能处理 的对象优先。
Context-sensitive keymap
元素在 prepaint 建立 key context,例如 Editor && vim_mode == normal。Keymap 不把产品状态写进 按键字符串,而是针对当前 focus path 的 context predicate 选择 binding。
7. 导航历史为何属于 Workspace
Definition、search result、outline、Git hunk 都可能跨 Item/Pane 跳转。单个 Editor 只知道自己的 selection,Workspace 才知道:
- 从哪个 item/location 离开;
- 目标在哪个 pane 打开;
- back/forward 时是否要重建 item;
- 最近激活 item 的顺序。
所以导航历史放在窗口所有权根,而不是 Buffer。
8. Settings 的多层来源
SettingsStore 合并大致优先级:
default
< server/managed
< user
< profile
< extension contribution
< project/worktree local settings
< language-specific override具体字段通过 SettingsContent::merge 合并;每个强类型 Settings 实现 from_settings 投影自己关心 的部分。
9. 强类型 Settings 与动态 JSON 的桥
用户编辑 JSONC,但业务代码读取:
let settings = EditorSettings::get_global(cx);
let local = EditorSettings::get(Some(SettingsLocation { ... }), cx);SettingsStore 为每种注册类型保存 SettingValue<T>:global value 加多个 (WorktreeId, RelPath, T) local value。源码: crates/settings/src/settings_store.rs:252-271。
这让热路径无需反复查询 JSON Value,同时保留按路径覆盖。
10. Settings 更新为何串行
文件 watcher、UI 编辑、profile 切换、project settings 可能同时更新。SettingsStore 使用 channel 顺序处理 setting file update,解析、合并、更新强类型值后再 notify Global observer。
源码:crates/settings/src/settings_store.rs:287-335。
串行化比给每种 Settings 单独 watch 更容易保证“所有类型看到同一版 merged settings”。
11. Workspace 持久化不是内存快照
可恢复状态只保存稳定描述:
- local/remote workspace location;
- worktree roots;
- pane split tree 和 active tab;
- serializable item kind + item-specific payload;
- dock/panel state;
- recent navigation/scroll/selection(由 item 决定)。
SerializableItem trait 要求 kind、serialize、deserialize、should_serialize。源码: crates/workspace/src/item.rs:408-473。
恢复时 registry 按 kind 找反序列化器;单个 item 失败不会阻止整个 Workspace 恢复。
12. Dirty close 是编排而不是一个 confirm
关闭窗口可能涉及多个 Pane 和 Item:
- 收集所有 dirty、可保存 item;
- 合并同一 Buffer 的重复视图;
- 根据 CloseIntent/Autosave 决定保存、丢弃或提示;
- await save task;
- 让 terminal/debug/remote connection 执行 shutdown;
- 写最终 Workspace state;
- 允许 Window 关闭。
这也是为什么 close 逻辑属于 Workspace,而不是每个 tab 自行弹窗。
13. 可迁移设计
- 业务文档用 Item protocol,工具面板用 Panel protocol;
- path opener 使用 registry,不把所有类型写进中心 switch;
- action 通过 focus path 路由,keymap 只表达意图;
- JSON config 先合并,再投影成强类型快照;
- 持久化稳定描述符,不序列化运行时对象图。