首页
/ VS Code 会话布局控制器(手机布局):MobileLayoutController 规则 M1–M2 规格与源码解析

VS Code 会话布局控制器(手机布局):MobileLayoutController 规则 M1–M2 规格与源码解析

2026-09-07 11:54:55作者:郁楠烈Hubert

本文以仓库中会话布局子系统的「手机布局控制器」规格文档 mobileSessionLayoutController.md 为主体,结合其基类规范、注册入口源码与单元测试,解析 VS Code 在 web phone(网页手机)布局下如何管理每个会话(session)的界面状态。读完后你将掌握:布局控制器家族的 B/D/M 规则体系如何组织、手机控制器为什么"刻意不管"侧栏(auxiliary bar)、以及 M1/M2 两条规则分别如何被代码与测试落实。

一、定位:三份规格、三套控制器

VS Code 的会话布局(Sessions Layout)体系按使用形态拆成三类控制器,每一类对应一份 Spec 文档,规则带稳定引用标签,供源码与测试引用:

控制器 规则前缀 覆盖布局 规格文档
BaseLayoutController(抽象基类) [B*] 所有布局共享的逐会话布局状态 baseSessionLayoutController.md
LayoutController(桌面控制器) [D*] 桌面端与 web desktop 布局 desktopSessionLayoutController.md
MobileLayoutController(手机控制器) [M*] web phone 布局 mobileSessionLayoutController.md

其中手机控制器是一个"裁剪版"控制器:它继承 BaseLayoutController(从而拿到 B1–B6 全部共享行为),但刻意省略了辅助栏(auxiliary bar,即侧栏/secondary side bar)的可见性管理。理由很直白——窄视口上如果侧栏被自动展开,会对用户造成破坏性干扰。

如果对整个规则家族感兴趣,可先读文件级总纲 LAYOUT_CONTROLLER.md(基类规范在其开头自述为该文件的"file-level companion")。

阅读约定:规格文档中的 Rules 描述用户可见行为、按场景分组,每个规则带稳定标签([M*] / [B*] / [D*]),编号不代表优先级或执行顺序;Implementation notes 则描述规则的实现方式,只有改代码时才需要读。此外每份规格文档都带一个 Specification change gate 提示:恢复既有规则行为的 bug 修复应落在回归测试里,仅当预期的控制器行为发生改变时才更新本文档。

二、注册:只在 web phone 布局下生效

手机控制器并非所有平台都会装载。查看注册贡献 sessions.layout.contribution.ts 的构造逻辑,可以清楚看到选择分支:

if (layoutService.isSinglePaneLayoutEnabled) {
    this._register(instantiationService.createInstance(SinglePaneLayoutController));
    return;
}

if (isWeb && isMobile) {
    this._register(instantiationService.createInstance(MobileLayoutController));
    return;
}

this._register(instantiationService.createInstance(LayoutController));

也就是说,在满足 isWeb && isMobile(web 平台 + 移动设备,即手机浏览器)时实例化 MobileLayoutController;其余所有布局(桌面端、web desktop,以及未开启 single-pane 的形态)走桌面控制器 LayoutController;若启用了 single-pane 详情节流板布局,则优先交给 SinglePaneLayoutController

这一点在手机控制器的 实现注解 中也有对应描述,并明确补充两点:

  • 该贡献由 sessions.web.main.ts 导入——它在注释里写明 "The web bundle serves both the web desktop and the web phone layouts; the layout contribution registers the correct one at runtime.",即同一份 web 构建同时服务两种布局,控制器在运行时按环境选择;
  • 注意规格文档把贡献阶段标注为 WorkbenchPhase.AfterRestored,而当前源码(sessions.layout.contribution.ts 第 44 行)实际以 WorkbenchPhase.BlockRestore 注册该 workbench contribution——判断时以实际代码为准。

三、场景一「手机上的布局」与规则 M1:与其他 surface 完全相同的逐会话布局

规格文档把 M1 归纳为一句话:会话仍然像其它任何 surface 一样记住自己的布局。手机只是没有侧栏的容身之处,但布局记忆能力不被削弱。M1 的具体含义需要回落到基类规范 baseSessionLayoutController.md 中被它逐条引用的 B 规则:

  • B1 — 面板可见性(Panel visibility):底部面板(bottom panel)的显示/隐藏按会话记忆,默认隐藏;切换会话时恢复该会话上次的状态。开启 single-pane 时会禁用这种"逐会话"作用域(面板改由 workbench 级别管理),但手机布局保持基类默认的逐会话管理。
  • B2 — 打开的编辑器(Open editors):每个会话在激活时恢复自己的一组打开的编辑器;切换会话会先保存离开的会话、再套用目标会话。新会话/未命名会话以及无保存记录的会话,绝不会强制撑开或清空编辑器区。workbench.editor.useModal'all' 时浏览器编辑器仍停靠共享的 grid editor part,因此该模式下编辑器标签页依旧需要逐会话捕获/恢复。
  • B3 — 启动时恢复:应用启动时恢复每个会话保存的布局;损坏数据被防御性忽略,并一次性从旧版 sessions.workingSets 键迁移。
  • B4 — 关闭时保存:关闭或重载应用时,通过 IStorageService.onWillSaveState 钩子把每个会话当前布局写入 sessions.layoutStateStorageTarget.MACHINE)。
  • B5 — 多会话回退默认:同一时刻可见会话多于一个时,暂停逐会话的面板恢复,并丢弃这些可见会话记忆的面板/辅助栏状态,从而折叠回单会话时显示默认布局;但打开的编辑器工作集仍被保留。

从源码上看,这些行为全部集中在基类 baseSessionLayoutController.ts 中,以 ResourceMap<...> 按会话资源(URI)存储:_panelVisibilityBySession_panelViewBySession_workingSets_editorPartHiddenBySession_viewStateBySession 等,并统一序列化进工作区级存储键 sessions.layoutState。手机控制器不触碰这套机制,因此 M1 天然成立——这也是"手机布局复用共享的逐会话布局状态"的直接体现。

四、规则 M2:侧栏(辅助栏)永不自动化

M2 是手机控制器区别于桌面控制器的关键规则。规格文档将其表述为:

The auxiliary bar (side pane) is never auto-opened, auto-closed or remembered on phones — not when switching sessions, submitting a new session, or when changes arrive.

也就是说,无论以下哪种时刻发生,手机端都不会自动开/关侧栏,也不会把侧栏状态记入某个会话:

  • 切换会话时;
  • 提交(submit)一个新会话时;
  • 会话产生新的文件变更(changes)时。

这样做的目的,是为了避免在手机窄视口上破坏性地自动展开 secondary side bar。反观桌面控制器 desktopSessionLayoutController.md,其 D1–D11 规则全部围绕辅助栏的记忆、恢复、默认视图(Files/Changes)、空栏隐藏与响应式折叠展开,两者形成鲜明对照:桌面控制器负责"管好侧栏",手机控制器负责"完全不碰侧栏"。

五、实现注解:刻意不覆盖 _registerViewStateManagement

规格文档的 Implementation notes 部分揭示了 M2 的代码级实现手法,且 mobileSessionLayoutController.ts 的完整源码只有一句话的分量:

export class MobileLayoutController extends BaseLayoutController {

    static readonly ID = 'workbench.contrib.sessionsMobileLayoutController';

    // [M2] Intentionally does not override `_registerViewStateManagement`, so the
    // auxiliary bar is never auto-shown / hidden / captured on phone viewports.
}

理解这段"空实现"的意义,需要回到基类的钩子设计。基类构造函数在 baseSessionLayoutController.ts 末尾调用三个可被子类覆写的钩子:

  1. _registerViewStateManagement() —— 用于平台特定的辅助栏接线;基类为空实现,桌面控制器覆写它来挂上 D1/D2/D3/D5/D7 逻辑,而手机控制器刻意不覆写
  2. _captureActiveSessionViewState(resource) —— 供 B4 保存时调用的快照钩子,基类为空实现;
  3. _registerAuxiliaryControllers() / _onSidePaneToggled() 等 —— 同样留空或默认无操作。

手机控制器的"不覆写"正是 M2 的实现:基类构造函数确实定义了"逐会话 view state(辅助栏可见性与激活容器)"的存储映射 _viewStateBySession 和相关的 autorun/监听逻辑,但在 _isViewStatePerSession 等开关全为基类默认值的前提下,没有桌面控制器去解释 ISessionViewState(桌面端将其视为不透明持久化数据并覆写钩子消费它),这些状态就永远不会被写入或用于驱动辅助栏的显隐。因此没有任何一份桌面端辅助栏逻辑会在手机布局上运行

六、测试验证:M1/M2 的守门人

规格的行为不是靠文档自证,而是由测试兜底。见 mobileSessionLayoutController.test.ts,它显式用测试标题标注规则标签:

  • [M2] does not manage the auxiliary bar for untitled or existing sessions:分别构造未命名会话(桌面控制器此时会打开 Files 视图)和已存在会话(桌面控制器此时会隐藏辅助栏),断言 openedViewsopenedViewContainers 始终为空、setPartHiddenCalls 中没有任何针对 Parts.AUXILIARYBAR_PART 的调用——即既不开视图、也不开容器、更不切换辅助栏可见性
  • [M2] ignores auxiliary bar maximize events:即使触发编辑器最大化事件,也断言 openedViews 保持为空——桌面端 D5"最大化时侧栏固定显示 Changes"的逻辑在手机上绝不生效;
  • [M1] still hides the panel by default (base behaviour):切换会话时,面板被默认隐藏(Parts.PANEL_PART + hidden === true),验证从基类继承的 B1 默认值依然工作。

这些测试通过 createTestHarness(见同目录 layoutControllerTestUtils.ts)模拟会话、activeSession 可观察对象与部件可见性调用,从而在无 UI 环境下验证控制器行为。开发者在为手机控制器做任何改动时,都应保证这些标了 [M*] 的用例继续通过——这正对应规格文档开头的 "Specification change gate"。

七、如何判断该看哪一份文档

在会话布局子系统中继续深入时,可按下表定位:

八、小结

MobileLayoutController 是"少即是多"的典型实现:它不新增任何属于自己的行为代码,仅通过继承基类的全部逐会话布局能力(M1 → B1/B2/B3/B4/B5)拒绝覆写辅助栏接线钩子(M2),就在网页手机布局上同时保证了会话布局的跨会话、跨重启记忆,以及窄视口下侧栏的绝对安静。理解它,等于同时理解了会话布局控制器家族"基类共享 + 平台子类增量"的整体架构哲学——这也正是三份配套 Spec 文档(B*/D*/M*)想向阅读者传达的设计骨架。

登录后查看全文
热门项目推荐
相关项目推荐