Часть архитектуры Diode — обзорная карта в ../ARCHITECTURE.md.
История миграции Controllers → Workbench (задача завершена, слой Controllers растворён) —
в git (docs/TODO/WorkbenchRefactoring.md до удаления).
Прикладной слой приложения. Здесь живут сервисы (логика приложения) и компоненты
(UI-сборка поверх контролов TUIDom) — как в VS Code (services + Part/ViewPane), а также
встроенные экшены (Actions/) и DI-модули с профилями (Modules/).
- Service — где живёт логика приложения: состояние, I/O, индексы, подписки на нижние слои. Сервис ничего не знает про конкретные компоненты.
- Component — принимает сервисы в конструктор и общается с ними (вызовы, подписки); владеет корневым контролом и раздаёт данные/стили вниз.
Правило-инвариант: есть view → Component; нет view → Service.
Async-инициализация живёт в сервисах: интерфейс IActivatable (src/vs/workbench/browser/iActivatable.ts):
export interface IActivatable {
activate(): Promise<void>;
}У компонентов отдельных mount()/activate() нет — вся сборка происходит в конструкторе.
Единственное исключение — корневой WorkbenchComponent: у корня есть реальная
bootstrap-последовательность приложения (mount → activate → open/restore файлов),
которую ведёт main.ts.
export abstract class Component extends Disposable {
public abstract readonly view: TUIElement;
}
export abstract class ThemedComponent extends Component {
protected constructor(protected readonly themeService: ThemeService);
protected get theme(): WorkbenchTheme; // активная тема из themeService
protected initStyles(): void; // подписка на onThemeChange → updateStyles()
protected abstract updateStyles(): void; // пуш стилей во владеемые контролы
}- Компонент владеет корневым контролом (
view), но в жизненный цикл контролов не встраивается — только размещает их (как DOM-узлы) и не наследуетTUIElement. - Наследник
ThemedComponentвызываетinitStyles()последней строкой конструктора (из базового конструктора нельзя — поля наследника ещё не инициализированы).ThemeService.onThemeChangeфайрит листенер немедленно с текущей темой, поэтому начальная покраска происходит ровно один раз — внутриinitStyles(); явный вызовupdateStyles()не нужен. Подписка снимается приdispose().
Компонент вешает view.id на свой корневой контрол — это DOM-идентичность для тестов и
Inspector'а (поиск по дереву, скриншот-демо). Контролы своих id не придумывают.
Контролы TUIDom про темы не знают. У контрола — плоский интерфейс packed-цветов и дефолты рядом с ним:
export interface IButtonStyles { readonly fg: number; readonly bg: number; /* … */ }
export const unthemedButtonStyles: IButtonStyles = { /* историческая палитра */ };
class ButtonElement {
constructor(label: string, options?: { styles?: IButtonStyles });
setStyles(styles: IButtonStyles): void; // единственный канал обновления, вызывает markDirty()
}Мост тема → стили — src/vs/platform/theme/browser/defaultStyles.ts: по функции
getXxxStyles(theme) на контрол; единственная точка знания «ключ темы → поле стиля».
Раздача — пуш-моделью: компонент подписан на смену темы (ThemedComponent.updateStyles())
и заново вызывает control.setStyles(getXxxStyles(this.theme)). Никаких applyTheme(theme)
у контролов и никаких литералов цвета вне темы (см. Theme.md).
- component → control: вызовы методов контрола +
setStyles(...). - control → component: колбэки
onX(контрол не знает получателя). - component ↔ service: конструкторная инъекция + подписки на события сервиса.
- component ↔ component: напрямую запрещено — только через общий сервис.
Исторически — чек-лист миграции view-контроллера (миграция завершена, слой Controllers растворён); остаётся конвенцией для нового кода:
- Логика — в
Workbench/Services/<Area>/, UI-сборка — вWorkbench/Components/<Area>/(компонент наследуетComponent/ThemedComponent). - Стили —
updateStyles()+getXxxStyles(theme)изWorkbench/Styles/defaultStyles.ts(никакихapplyTheme(...)у контролов и ручных подписок на тему). - Wiring — в конструктор компонента, async-часть — в сервис (
IActivatable). - DI-токен компонента —
*ComponentDIToken, рядом с компонентом; биндинг — вWorkbench/Modules/. view.id— на корневой контрол компонента; тесты живут рядом с кодом.
Фич-проводка (подписки на события сервисов, публикация UI, регистрация
программных команд) НЕ живёт в конструкторе корневого WorkbenchComponent —
она вынесена в самодостаточные contribution'ы (аналог IWorkbenchContribution
в VS Code). Так корень остаётся тонким: сборка view + слоты + lifecycle, а не
перечисление фич.
Контракт IWorkbenchContribution — маркер (extends IDisposable, пустой):
вся работа делается в конструкторе (подписка/публикация/регистрация), dispose()
её сматывает. Отдельного метода активации нет — инстанцирование класса И ЕСТЬ
активация. Прообраз — EditorStatusContribution.
Реестр + фазы. WorkbenchContributionsRegistry.instantiateByPhase(phase)
инстанцирует по фазе через DI (accessor.get(token) — авто-инжект
static dependencies + кэш-синглтон) и забирает владение (register), поэтому
dispose реестра сматывает все contribution'ы. Две фазы:
restored— синхронно вWorkbenchComponent.mount()(view построена, лёгкие сервисы готовы; между конструктором и mount ни один редактор не открывается → перенос проводки из конструктора эквивалентен);eventually— idle после первого кадра (изmain.ts,setImmediate(() => workbench.runEventuallyPhase()); из mount фаза сработала бы до кадра, т.к.app.run()красит отложенно) — для тяжёлой/отложенной работы.
Регистрация — явный массив WORKBENCH_CONTRIBUTIONS (workbenchContributions.ts,
зеркало builtinActions, без import-side-effect самрегистрации): пара
{ token, phase }. Новую фич-проводку добавляем сюда + биндим класс в
Modules/WorkbenchModule.ts, а не строкой в конструктор корня.
Правило под наш DI (ленив по токену, без Delayed-прокси): тяжёлые сервисы НЕ
класть в static dependencies — иначе они сконструируются в момент прогона фазы.
Тяжёлое резолвить лениво через ServiceAccessor внутри колбэков.
Contribution vs Action. Порция кейбиндов и команды с ленивым run(accessor) —
это декларативные CommandAction в Actions/ (данные в реестры, сервис
резолвится при вызове), а не contribution'ы. Contribution нужен для eager-проводки
на событиях. Пограничный случай — программная команда без title (workbench.openFile):
не Action (тот форсит title → палитра), а contribution, регистрирующая команду.
Текущие: EditorStatusContribution, TerminalEnvStatusContribution (сегменты
статус-бара), AutoRevealContribution (explorer.autoReveal),
ThemeConfigContribution (live-reload workbench.colorTheme),
OpenFileCommandContribution (команда workbench.openFile),
PanelFocusContribution (возврат фокуса в редактор, когда содержимое нижней панели
уходит со сцены), HistoryService (история навигации — сервис, он же contribution:
владение подписками отдаёт реестр). Все — restored.
Схемы настроек фич — узлы IConfigurationNode для ConfigurationRegistry
(механизм — слой Configuration, см. Configuration.md):
по файлу на секцию (workbenchConfiguration.ts, editorConfiguration.ts,
explorerConfiguration.ts, filesConfiguration.ts, terminalConfiguration.ts)
плюс явный массив CONFIGURATION_CONTRIBUTIONS (configurationContributions.ts).
Узлы — чистые данные (их бандлит генератор схемы автодополнения); новая
настройка = свойство в узле своей секции. Реестр собирает main.ts (defaults-слой
ConfigurationService) и биндит configurationModule
(ConfigurationRegistryDIToken — известные ключи валидации settings.json в
DiagnosticsService).
Пункты меню собираются декларативно из реестра (аналог MenuRegistry+MenuId в
VS Code), а не хардкодятся списками в местах открытия. Так when-видимость и
группировку пунктов задаёт данные, а сборщик (контекст-меню редактора/Explorer,
меню-бар) лишь запрашивает готовый список.
Точки. MenuId — расширяемый класс с id-уникальностью (как в vscode):
встроенные точки — статические инстансы (EditorContext, ExplorerContext,
EditorTitleContext, MenubarMainMenu + MenubarFileMenu/MenubarEditMenu/…),
новая точка —
new MenuId("my.menu"); сравнение — по идентичности инстанса.
Записи. Два вида contributions (MenuContribution):
IMenuContribution { menuId; command; title?; when?; visible?; group?; order?; icon?; args?; shortcut? }— пункт-команда;ISubmenuContribution { menuId; submenu; title; mnemonic?; when?; group?; order? }— пункт, открывающий вложенную точку (аналогISubmenuItem); меню-бар = submenu-записи вMenubarMainMenu.
Co-location placement (аналог registerAction2). Placement живёт на самой
команде: CommandAction.menus: CommandMenuPlacement[] (+ shortTitle — короткий
label для меню: «File: Copy» → «Copy»; label-цепочка «title размещения →
shortTitle → title» фиксируется при деривации). Явный полный массив
MENU_CONTRIBUTIONS = структура меню-бара (MENUBAR_SUBMENUS) + деривация
menuItemsOfAction(action) по builtinActions — конвенция явных массивов
сохраняется, но источник каждого placement'а — файл его экшена.
Реестр (данные). MenuRegistry.getMenuItems(menuId, context?): фильтр по
when (ContextKeyService.evaluate) и по visible(context), сортировка групп —
navigation первой (спец-группа vscode), дальше по строке; внутри — order;
авто-разделители между непустыми группами (скрытый пункт схлопывает группу
без лишнего сепаратора), маппинг в MenuEntry:
- label —
titleпункта, иначеCommandRegistry.getTitle(command), иначе id; - shortcut —
false→ нет; строка → литерал; иначе резолв изKeybindingRegistry.getKeybindingForCommand+formatKeybinding; - onSelect —
CommandRegistry.execute(command, ...args(context))(args резолвятся при открытии).
getSubmenus(menuId) — submenu-записи (тот же when-фильтр и сортировка, без
разделителей). appendMenuItem (динамика) + onDidChangeMenu (событие смены
состава). Реестр — чистая функция реестров команд/кейбиндов/контекст-ключей;
состояние открытия (буфер обмена файлов, путь узла) приходит параметром
context, не через DI. Конвенция контекста: EditorContext, меню-бар →
undefined; ExplorerContext → { path, canPaste }; ScmGraphContext → { sha, shortSha, subject } — коммит под меню в графе;
EditorTitleContext → { groupId, index, path, tabCount, hasTabsToTheRight, hasSavedTabs } — вкладка под правым кликом (хелперы — Menus/menuContexts.ts).
EditorTitleContext — меню вкладки (VS Code editor/title/context).
Открывает EditorGroupComponent по EditorTabStripElement.onTabContextMenu;
цель — вкладка под курсором, а не активная (правый клик активную не меняет,
как в VS Code). Поэтому команды пунктов адресуются парой (groupId, index) из
editorTabTargetArg (резолв — Actions/editorTabTarget.ts, фолбэк на активную
вкладку, если аргументов нет: палитра и клавиатура), а видимость решается
императивно через visible(context): глобальные when-ключи вкладку под курсором
не различают, а enablement тут не поможет — он гасит пункт, а не адресует его.
Пункт закрытия ходит через
closeTabsWithConfirm (Actions/editorCloseHelpers.ts) — общая серия закрытий с
confirm-диалогом по несохранённым и прерыванием на Cancel.
MenuService (потребление). MenuService.createMenu(menuId) → IMenu (аналог
IMenuService/IMenu): живое меню одной точки — getEntries(context?)/
getSubmenus() резолвят на момент вызова, onDidChange переэмитит смену
состава реестра. Консюмеры не ходят в реестр напрямую: контекстные меню — через
ContextMenuService (platform/contextview, делегаты с menuId; редактор —
editor/contrib/contextmenu), меню-бар (MenuBarComponent:
top-уровень — getSubmenus(MenubarMainMenu), entries каждого меню — ленивый
геттер, резолв при открытии попапа — шорткаты и динамические пункты всегда
актуальны, порядок резолва относительно user keybindings не важен). Попапы
по-прежнему резолвятся при открытии, а постоянный UI (inline-кнопки заголовков
view) пере-резолвится по ContextKeyService.onDidChange — событие коалесцировано
в тик и не стреляет на запись того же значения (иначе оно бы срабатывало перед
каждым кейбиндом, см. WorkbenchContextKeys.update).
enablement — доступность против видимости. У CommandAction,
CommandMenuPlacement и IMenuContribution рядом с when есть enablement
(аналог precondition/enablement VS Code): when пункт прячет, enablement
гасит — пункт остаётся на месте, но приглушён и не исполняется. Placement
наследует значение экшена, своё — сужает (AND). Принуждение живёт на самой
команде (обёртка в registerAction), поэтому накрывает все пути запуска: пункт
меню, inline-кнопку, кейбинд, палитру и программный execute; enablement
дополнительно складывается в when биндинга, иначе диспетчер проглотил бы
клавишу впустую. Резолвнутый пункт несёт enabled (IResolvedMenuItemEntry) —
его читает заголовок view и рисует кнопку токеном disabledForeground. Серые
пункты попапа «⋯» пока невозможны: у MenuItemEntry в @tuidom/elements нет
поля disabled — это задача в репозиторий tuidom, см.
TODO.
Границы vs contribution/Action. Menu-placement — где показывать команду
(данные, co-located на экшене). Command+keybinding — Actions/ (поведение).
Event-проводка — Contributions/.
Осознанно не перенесено из vscode: alt-пункты (альтернатива по Alt) и user
hide-toggle (isHiddenByDefault). См.
TODO.
Группы и вложенность. MenuRegistry.getMenuItemGroups отдаёт пункты
разложенными по группам (склейка сепараторами — joinMenuGroups): это нужно
потребителям, которые рисуют группы по-разному — заголовок view показывает
navigation inline-кнопками, а остальное уводит в «⋯». Вложенные submenu в
попапах поддержаны: MenuService.getEntries/getEntryGroups резолвят их
рекурсивно (seen рвёт циклы MenuId), PopupMenuElement рисует
type: "submenu" дочерним попапом.
Component.ts— базаComponent/ThemedComponent.IActivatable.ts— контракт async-инициализации сервисов.Styles/— мост тема → стили контролов (defaultStyles.ts).Modules/— DI-модули и профили (ProductionProfile/TestProfile,WorkbenchModuleсо всеми парами Service ↔ Component и интерфейсными швами,ExtensionHostModuleи др.) — см. ../DI.md.Services/— переехавшие из Controllers сервисы: система команд (CommandRegistry,KeybindingRegistry,ContextKeyService,ContextKeys),KeybindingDispatcher(клавиатурный диспатч: резолв keydown противKeybindingRegistry+ContextKeyService, chord-режим с таймаутами и swallow продолжения, chord-хинт/«is not a command» черезStatusBarService, hold-сессии черезModifierReleaseArmory, runtime-детект CSI-u, применение user keybindings.json; view не знает — владелец корневого дерева (WorkbenchComponent) вешает его capture/bubble-листенеры и подключает хук-шовhasKeyboardCapturingOverlay; второй хук —updateContextKeys— замыкает на себяWorkbenchContextKeys),StateKeys,ModifierReleaseArmory,ChokidarFileWatcher+IFileWatcherDIToken,FileSearchService,QuickOpenParsing,collectWordCompletions,CoreTokens, каталогиWorkspace/(undo/redo +TrashService/WorkspaceEditService/fileClipboardFs.ts— чистые ФС-операции copy/cut/paste),TerminalEnvironment/,Terminal/(EmbeddedTerminalSession, фабрика, загрузчик node-pty,TerminalService),Diagnostics/(валидатор settings.json,ProblemsTreeDataProvider,DiagnosticsService),history/(HistoryService— стек Go Back / Go Forward, см. ниже).- История навигации (
services/history/browser/historyService.ts, аналогIHistoryService+EditorNavigationStackу VS Code). Стек хранит место (ресурс + позиция каретки + группа), а не вкладку: MRU вкладок (Ctrl+Tab) помнит, ЧТО было открыто, история — ГДЕ ты был. Ведётся двумя путями: неявно — подпиской на смену активного редактора и на каретку (entries[index]всегда зеркалит живую позицию, движение ближе 10 строк перезаписывает запись, а не растит стек), и явно — обёрткойIJumpRecorder.jump()вокруг перехода. Обёртку обязан звать каждый сайт прыжка (Go to Definition, Problems, результаты поиска, Go to Line/File): междуopenUriиgoToPositionистория иначе увидит промежуточную позицию «начало целевого файла», и первый Back приведёт туда. Собственные Back/Forward гасятся флагомsuspended— без него восстановление тут же записалось бы новой записью и отсекло forward-хвост. Чистка стека (удалённый с диска файл, закрытыйuntitled:) — ленивая, только на шаг пользователя:onDidChangeEditorsприлетает на КАЖДОЕ нажатие клавиши (группа слушаетonDidChangeContentвкладки), и вешать тудаexistsSyncпо полусотне записей нельзя. - Explorer-кластер (этап 7) — дерево файлов сайдбара и файловые операции:
Services/FileTreeDataProvider.ts— данные дерева (ленивая загрузка по уровням, chokidar-watch раскрытых каталогов, статус-декорации/иконки).Services/ExplorerService.ts— логика Explorer'а (аналогIExplorerService): корень воркспейса + владение провайдером (setRootPathпересоздаёт провайдер и файритonDidChangeRoot),revealPath(построение цепочки предков),autoRevealActiveFile(настройкаexplorer.autoReveal; активный файл передаётWorkbenchComponent), выбор (getSelectedPaths/getPasteTargetDir),setFileDecorations(мост декораций extension-host'а: адаптерFileDecorationsServiceAdapterтипизирован минимальным интерфейсомIFileDecorationsTarget, сервис соответствует структурно), подсветка «вырезанных» поIFileClipboard.onDidChangeи лог ошибок file-watcher'а (filetree.watcher, подсказка про inotify-лимит). Дерево приходит через шовIExplorerView(refresh/reveal/focus/selection/cut-keys):TreeViewElement<FileTreeNode>соответствует структурно, регистрирует его компонент черезattachView.Components/Explorer/ExplorerComponent.ts—ThemedComponent; поonDidChangeRootстроитTreeViewElementповерх провайдера сервиса (обёрнутScrollBarDecorator+TitledPanelElement«EXPLORER»,view.id = "explorer"; стили —getFileTreeStyles/getScrollBarStyles), вяжет события дерева (expand → watch каталога, активация файла → командаworkbench.openFile) и открывает контекст-меню дерева черезContextMenuService— делегат сMenuId.ExplorerContext, пункты исполняют командыexplorer.*/fileOperations.*; правый клик и Shift+F10 — одно событиеcontextmenuдвижка, один путь.Services/FileOperationsService.ts— файловые операции поверхWorkspaceEditService/DialogService/UndoRedoService/IFileClipboard:runCreate/runRename(промпт имени через узкий шовIExplorerInputPrompt— срезQuickInputService.input, в DI замкнут наQuickInputServiceDIToken; интерфейс оставлен ради фейков в тестах),requestDeleteFile(корзина/ безвозвратно + подтверждения),copySelected/cutSelected/paste(+buildPasteEdits), workspace-undo/redo,resolveInputPath(~, корень воркспейса).Services/InputWidgetService.ts— целевой сервис input-команд: держит активныйInputElement(ставитWorkbenchContextKeys.update()) и исполняет курсор/правки/выделение/клипборд для него (читают экшеныWorkbench/Actions/InputActions.tsподwhen: inputWidgetFocus).
- QuickInput-кластер (этап 8) — квик-инпут/квик-опен поверх ОДНОГО общего
виджета:
Components/QuickInput/QuickInputComponent.ts—ThemedComponent; владеет единственным переиспользуемымQuickPickElement(view.id = "quickInput"; внутри —InputElementстроки запроса) и его overlay-сессией (restoreFocus,closeOnEscape,pointerPolicy: "close-on-outside"). Overlay-хост — late-init шовattachHost(BodyElement)(как у DialogService). API для сервисов-клиентов:show()(позиция: центр, ~10% от верха + open + focus),hide(),isOpen(), канал закрытияonDidClose(Escape / клик мимо / программное — один путь). Стили: пушunthemedQuickPickStyles— пикер пока на исторической unthemed-палитре, маппинг на ключи темы — отдельная задача.Services/QuickInputService.ts— VS Code-style QuickInput:input(opts)(InputBox: title/prompt/placeholder/value/validateInput; Enter блокируется hard-ошибкой) иquickPick(opts)(фильтруемый список,activeIndex,onDidChangeActive— шов live-preview). Промисы резолвятся значением/ выбранным айтемом илиundefinedпри отмене; новый вызов отменяет предыдущий. На каждый показ полностью ре-инициализирует состояние и колбэки общего виджета.Services/QuickAccess/— contribution point провайдеров Quick Open (аналогIQuickAccessRegistryvscode,vs/platform/quickinput/common/quickAccess.ts):QuickAccessRegistryвыбирает провайдера по самому длинному префиксу запроса; список — явный массивQUICK_ACCESS_PROVIDERS(quickAccessProviders.ts, DI-токены, резолв ленивый). Провайдер (IQuickAccessProvider) отдаёт пункты (getItems, запрос целиком — префикс срезает сам; свой префикс объявляет статикойPREFIX, как vscode-провайдеры) и плейсхолдер, живёт по хукамonShow(refresh)/onHide; пункт —QuickAccessItemс колбэкомaccept(пункт безaccept— информационный хинт, пикер не закрывает). Встроенные провайдеры:FilesQuickAccessProvider(""— дефолтный: фоновый индексFileSearchService,debounceQuery, live-refresh поonIndexChangedс сохранением курсора,file:line[:col]-суффикс черезQuickOpenParsing),CommandsQuickAccessProvider(>:CommandRegistry.listCommands+ шорткаты изKeybindingRegistry/ContextKeyService),GotoLineQuickAccessProvider(:; активный редактор — шовIGotoLineEditorSource→EditorServiceструктурно, биндинг вModules/WorkbenchModule.ts).Services/QuickOpenService.ts— контроллер показа Quick Open (аналогQuickAccessController):show(prefix)занимает общий виджет, дальше ведёт запрос через реестр (смена префикса на лету переключает провайдера), к дорогим провайдерам применяет leading+trailing debounce 16мс. О конкретных префиксах не знает. UI — тот жеQuickInputComponent; сервис-клиент, занявший виджет позже, закрывает предыдущий показ (его промис отменяется черезonDidClose).
Actions/— экшены Workbench (CommandAction/registerAction— описание команды + кейбинды; переехали из Controllers):FileTreeActions.ts(delete/rename/refresh/undo/redo + Shift+F10-меню Explorer'а),FileTreeClipboardActions.ts(copy/cut/paste, copyPath/copyRelativePath),FileTreeCreateActions.ts(explorer.newFile/explorer.newFolder); с этапа 8 — тонкие экшены-пикеры с реальнымиrun(accessor):QuickOpenActions.ts(Ctrl+P / Show Commands / goto-line →QuickOpenService.show(prefix)с префиксами из статикPREFIXпровайдеров),ThemeActions.ts(selectColorThemeповерхQuickInputService.quickPick+ThemeRegistry/ThemeService, live-preview черезonDidChangeActive, персист вworkbench.colorTheme; здесь жеthemeTypeLabel),FileActions.ts(Open File / Open Folder: InputBox-промпт пути +FileOperationsService.resolveInputPath; открытие — командаworkbench.openFile, смена воркспейса — шовIWorkspaceFolderOpener→WorkbenchComponentструктурно, биндинг вModules/WorkbenchModule.ts). Регистрирует ихWorkbenchComponentв общем циклеbuiltinActions. С этапа 9b здесь же экшены активного редактора поверхEditorService:EncodingActions.ts(двухуровневый пикер Reopen/Save with Encoding),EolActions.ts(convert/toggle/пикер EOL),ContextMenuActions.ts(Shift+F10-меню редактора). С этапа 10 —FindActions.ts(Ctrl+F/Enter/F3/ Escape →FindService) иSuggestActions.ts(Ctrl+Space triggerSuggest + навигация/accept/hide попапа →CompletionService); экшены подfindWidgetVisible/suggestWidgetVisibleидут ХВОСТОМbuiltinActions, чтобы победить editor-команды (резолвер берёт последний зарегистрированный с проходящимwhen). С этапа 11Controllers/Actions/растворён целиком: сюда переехали Editor*/Input*/Clipboard*/Folding*/List*/Tab*/Whitespace*/App*/ Preferences*-экшены (Preferences и save/saveAs/newUntitled — с реальнымиrun(accessor), About — экшен поверх DialogService; у quitrunперекрываетWorkbenchComponentconfirm-save-флоу), добавилисьLayoutActions.ts/TerminalActions.ts, а сам упорядоченный список —builtinActions.ts(регистрирует владелец приложения одним циклом).- Диалоги (этап 5b) —
browser/parts/dialogs/: базаDialogComponent(владеетFitContentElement-view; наследник собирает в нём дерево примитивов конструкторами один раз — компонент компонует контролы, не наследуяTUIElement; общий каркас окна —buildFrame(рамка + отступы + стек, цвета контента раздаёт каскад); общее поведение: ряд кнопок, стрелки, Escape →onDismiss; цвета — токеныDIALOG_STYLES:editorWidget.*,descriptionForeground,textLink.foreground,editorWarning.foreground— резолвит каскад, пере-пуш при смене темы не нужен) и наследникиConfirmDialog,ConfirmSaveDialog,AboutDialog. Оркестрация —Services/DialogService.ts(аналогIDialogService): владеет компонентами и их overlay-сессиями (pointerPolicy: "modal", центрирование по экрану), API —showConfirmDialog/showConfirmSaveDialog(+ promise-обёрткаconfirmSave)/showAboutDialog,getOpen*для тестов/оркестрации. OverlayLayer приходит через late-init шовattachHost(BodyElement)— его зовёт владелец корневой view (WorkbenchComponent) после её постройки. - Жизненный цикл (этап 5c) —
Services/LifecycleService.ts:requestShutdown(onProceed)последовательно спрашивает про «грязные» элементы участников черезDialogService.confirmSave(Cancel прерывает прощание; чистый выход — синхронно, до первого await). Шов — интерфейсIShutdownParticipant(collectDirty(): IShutdownDirtyItem[]— имя +isStillDirty()+save()с overwrite): Workbench объявляет,EditorServiceреализует структурно, регистрирует егоWorkbenchComponent; что произойдёт после прощания — колбэк вызывающего. Сценариев два, протокол один:- выход (
quitAction→QuitHandlerDIToken→WorkbenchComponent): teardown TUI +process.exit; - перезагрузка окна (
reloadWindowAction→WindowReloadHandlerDIToken,services/lifecycle/common/windowReload.ts): владелец приложения (main.ts) отпускает терминал, сокет инспектора, extension host и состояние сессии, а затем заменяет процесс новым с теми же аргументами (base/node/restartProcess.ts). Первый reload превращает текущий процесс в супервизор (spawnSyncсоstdio: "inherit"), дальнейшие делает уже он — так терминал не отбирается у нового окна и процессы не копятся. Нужна перезагрузка потому, что вклады расширений сканируются один раз на старте: установленное из UI начинает работать именно после неё.
- выход (
- Статус-бар — эталонная пара Service ↔ Component (пилот, этап 4):
Services/StatusBarService.ts— реестр записей статус-бара (аналогIStatusbarServiceVS Code):addEntry(IStatusBarEntry) → IStatusBarEntryHandle(update/dispose),onDidChangeEntries,entries()(left, затем right; внутри стороны — по убываниюpriority, выше — левее, как в VS Code). Про поставщиков и контролы не знает. Он же владеет видимостью записей:allEntries()отдаёт полный список (источник пунктов меню),entries()— только видимые (то, что рисует компонент),isHidden/setHiddenпереключают с write-through персиста вSTATUS_BAR_HIDDEN_STATE(scope: "global"— состав полосы это вкус пользователя, а не свойство проекта, так же в VS Code). Скрыть можно только запись сname: транзиентные сегменты (chord-хинт, прогресс расширения) имени не несут, в меню не показываются и всегда видимы.Components/StatusBar/StatusBarComponent.ts—ThemedComponent; композиционный корень из примитивов tuidom (view=HFlexElement,view.id = "statusBar": краевыеFillerElement-паддинги, лейблы сегментовTextLabelElement, fill-филлер в середине). Отдельных разделителей нет: каждый сегмент несёт по пробелу с краёв, поэтому соседей разделяют ровно две клетки, а блок подсветки накрывает сегмент с воздухом — как элемент статус-бара в VS Code. Дерево строится один раз и мутируется поonDidChangeEntries: при неизменном числе сегментов — толькоsetText(путь курсораLn X, Col Y), пересборкаreplaceChildren— лишь при смене состава; лейблы живут в пулах, клик резолвит запись по (стороне, индексу) в момент клика. Красит бар из темы вupdateStyles()— дети наследуют цвета каскадом.- Наведение и меню видимости. Кликабельный сегмент (запись с
onClick) несётwhen-стильstatusBarItem.hoverBackground/hoverForeground— hover движок ставит сам, лейблы настоящие цели хит-теста. Инертные сегменты подсветки не получают: она обещала бы клик, которого нет (в VS Code ровно так — hover-стиль у<a>-элементов с командой). Команду запускает только ЛЕВАЯ кнопка. Правый клик по полосе открывает черезContextMenuServiceпереключатель видимости: галочки (CHECKED_ICON) по всем именованным записям включая скрытые — иначе их нечем вернуть — иHide '<name>'для сегмента под курсором (цель резолвится изevent.target). Клик мимо сегментов даёт меню безHide— это путь назад, когда скрыто всё. Id лейбла едет за записью (#statusBarItem-status-scm-branch), не за индексом в пуле — селекторы инспектора/e2e стабильны. - Сегменты публикуют workbench-contribution'ы (инстанцируются реестром в фазе
restored, см. «Workbench-contributions»):Services/EditorStatusContribution.ts(правые, порядок VS Code:Ln X, Col Y· Indentation (Spaces: N/Tab Size: N) · Encoding · EOL · Language; Encoding/EOL кликабельны — командыchangeEncoding/changeEOLчерезCommandRegistry; Indentation инертен — команд смены отступов пока нет) иServices/TerminalEnvironment/TerminalEnvStatusContribution.ts(tier + моды). Активный редактор приходит через интерфейсный шов: Workbench объявляетIActiveEditorStatusSource/IActiveEditorStatus(минимальный срез:onActiveEditorChanged, курсор/отступы/encoding/EOL/язык),EditorServiceсоответствует ему структурно; связывание — биндингActiveEditorStatusSourceDITokenвModules/WorkbenchModule.ts. Chord-хинт публикуетKeybindingDispatcherкак обычную запись сервиса.
- Panel-кластер (этап 6) — нижняя панель и её вкладки:
Services/PanelService.ts— таб-строка нижней Panel: набор вкладок, активная вкладка и видимость панели. Вкладки заводит толькоViewsService(контейнер сlocation: "panel"= вкладка), фичи в этот реестр не ходят. События:onDidChangeViews,onDidChangeActiveView,onDidActivateView(пользовательская активация — клик по табу; программныйsetActiveViewего не порождает — на нём висят ленивые фичи),onDidChangeVisibility(с этапа 11 за ней следуетLayoutService: двигаетWorkbenchLayoutElementи контекст-ключpanelVisible).Components/Panel/PanelComponent.ts—ThemedComponent; владеетPanelContainerElement(view.id = "panel", стили —getPanelContainerStyles), отражает реестр сервиса (вкладки/контент/актив) и возвращает клик по табу вPanelService.activateView.Components/Panel/ProblemsComponent.ts—ThemedComponent; дерево «файл → маркеры» (TreeViewElementповерхProblemsTreeDataProvider,view=ScrollBarDecorator,view.id = "problemsView"; стили —getProblemsTreeStyles+getScrollBarStyles). Регистрирует контейнер и view PROBLEMS (PROBLEMS_VIEW_ID,location: "panel"); пока маркеров нет — тело viewnullи секция рисует placeholder. Reveal маркера — через интерфейсный шовIMarkerRevealTarget(openUri+getActiveEditorсgoToPosition/revealRange);EditorServiceсоответствует структурно, биндингMarkerRevealTargetDIToken— вModules/WorkbenchModule.ts.Services/Terminal/TerminalService.ts— headless-оркестратор терминала: инстансы (id/title/session), lazy spawn черезTerminalSessionFactory, регистрация контейнера/view TERMINAL (TERMINAL_VIEW_ID,location: "panel")- подписка на её активацию, чистка PTY при выходе шелла/dispose. События:
onDidOpenInstance/onDidCloseInstance/onDidChangeActiveInstance/onDidRequestFocus.
- подписка на её активацию, чистка PTY при выходе шелла/dispose. События:
Components/Panel/TerminalPanelComponent.ts— view-владелец терминала: строитTerminalViewElementпо каждому инстансу, вкидывает виджет активного в TERMINAL-вкладку (черезViewsService.setViewBody), красит виджеты (getTerminalViewStyles). Не наследникComponent: корневого контрола нет — его UI это несколько виджетов. ВАЖНО: уTUIElementнет unmount-хуков, поэтому компонент обязан сам dispose'ить виджеты — при закрытии инстанса и при своёмdispose().Services/Diagnostics/DiagnosticsService.ts— headless-проводник диагностик поверхMarkerService: поставщик — валидатор активного settings.json, потребитель — editor squiggles (Problems — второй потребитель того же реестра). Редакторы приходят через шовIDiagnosticsEditorSource/IDiagnosticsEditor(EditorService/EditorPaneструктурно; биндингDiagnosticsEditorSourceDIToken— в WorkbenchModule).
- Editor-кластер (этапы 9a/9b) — редактор целиком в Workbench:
Services/TextFile/TextFileModel.ts— per-file модель без view (аналогITextFileEditorModel): владеетTextDocument, dirty-статусом (isModified= versionId + EOL-ось), осями encoding/EOL/language, записью на диск (save/saveAs/saveWithEncoding+ save-участник с клампом правок), перечиткой (revertToDisk/reopenWithEncoding→ событиеonDidReloadDocumentс reasondisk/owned) и слежением за файлом на диске черезIFileWatcher(авто-перечитка чистого буфера /hasDiskConflictу «грязного»). Владеет движком undo:UndoManager(класс — вsrc/vs/editor) — один на документ, пересоздаётся с ним; модель сама роутит шаги вUndoRedoService(undoContext), аundo/redo(view)принимают «действующую вью», которой восстанавливается снимок выделений. Не singleton-сервис: экземпляр на файл, но один при любом числе вкладок — реестрTextFileModelRegistry(acquire(uri)→ ref-count-ссылка, вкладка владеет ссылкой, модель умирает с последней; untitled/synthetic — мимо реестра). Правки, которые модель применяет сама (участник,setEol,applyExternalEdits), идут через шовITextFileEditTarget— прикрепляет каждый парный компонент (целей может быть несколько — сплит-вью; действующую передаёт вызывающий,markDirtyвещается всем).Components/Editor/EditorComponent.ts—ThemedComponent; владеетEditorElement+ view-state + токен-кешем (view=ScrollBarDecorator), принимает модель в конструктор (модель может делиться несколькими компонентами — по вью на группу): поonDidReloadDocumentпересобирает view-state/EditorElement(перенося стили/контекст-меню; приreason === "disk"— ещё каретку и скролл), поonDidChangeLanguageиTokenizationRegistry.onDidChangeпересаживает токенизатор, по контенту пересчитывает folding-регионы (микротаск-коалесинг). Чужие правки документа (другая вью, undo, владелец буфера) строчно ремапят выделения/фолды/скролл каждой вью —EditorViewState.remapForDocumentChangeпоonDidChangeContent(свои мутаторы гейтятся и пересчитывают себя точно). Здесь же view-API: курсор/reveal/goToPosition, декорации (search/markers/ gutter change-bars), folding-команды, контекст-меню редактора,updateStyles()→getEditorStyles+editor.style={fg,bg}+getScrollBarStyles.Components/Editor/EditorPane.ts— пара «модель + view-компонент» одного открытого редактора (аналог editor input + pane): владеет временем жизниTextFileModel+EditorComponentи делегирует единый API по принадлежности. Это поверхность «активного редактора» для потребителей (экшены, Find/Completion, host-адаптеры, швыIActiveEditorStatus/IDiagnosticsEditor/IMarkerRevealEditor/IGotoLineEditor— выполняются структурно делегатами в модель/компонент).Services/EditorService.ts(этап 9b; со сплитов — фасад активной группы + менеджер полосы, аналог IEditorService+IEditorGroupsService) — логика без view. Пер-группная модель извлечена вeditorGroupModel.ts(EditorGroup: вкладки, активная, MRU-серии Ctrl+Tab,insertPane/detachPaneбез dispose, пер-группный дедупfindPaneIndex; контракт порядка событийonDidChangeEditors→ фокус →onDidChangeActivePane). Сервис владеет полосой (groups/activeGroup/viewColumnOf/groupOf), операциями сплитов (splitActiveGroup/newGroup/focusGroup/moveActiveEditorToGroup/copyActiveEditorToGroup/joinTwoGroups/joinAllGroups/moveActiveGroup; отказ по месту —canAddGroupHook+ лог), схлопыванием опустевших групп, реестром моделей (TextFileModelRegistry: однаTextFileModelна ресурс, вкладка владеет ref-count-ссылкой),openFile/openUri({group:"beside"}— Open to the Side),newUntitled,displayName/suggestedSaveName, применениеeditor.*-настроек, группа-уровневые швы host'а (saveParticipant,completionSource),IShutdownParticipant(collectDirty— дедуп по документу). События:onActiveEditorChanged(смена вкладки активной группы ЛИБО активной группы),onEditorSaved,onDidChangeEditors(агрегат групп),onDidActiveGroupChange,onDidGroupsChange({kind: added|removed|moved}).parts/editor/editorPartComponent.ts— часть «область редактора» (аналогEditorPart): владеетtuidom/ui/editorpart/EditorPartElement(полоса N вью + N−1 сашей, нормированные веса, min-клампы 20×5, максимизация,canFit) и поEditorGroupComponentна группу; политика долей (сплит делит долю источника пополам); хуки сервису (canAddGroupHook,focusGroupContentHook) и персисту (onDidChangeGroupLayout,IEditorGroupsLayoutViewдляWorkbenchStateService).Components/Editor/EditorGroupComponent.ts— контрол ОДНОЙ группы; композиционный корень из примитивов tuidom:view=OverlayHostElement(локальный OverlayLayer для find-виджета группы;view.id = "editorGroup-<groupId>") поверхVFlexElement[EditorTabStripElement(1 ряд), контент-слот (остаток; пустой — фон-филлерeditor.background, focusable у пустой группы)]: поgroup.onDidChangeEditorsвставляет view активной вкладки и перерисовывает табы (метки с минимальной разводкой тёзок пер-стрип, иконки, маркер изменённости —getTabStripStyles); клики по табам возвращает в группу (activateTab/closeTab, закрытие «грязной» вкладки —EditorService.onRequestConfirmClose(group, index)); любой фокус в поддереве капчурится →notifyGroupFocused(клик мышью делает группу активной).Parts/Editor/DiffEditorPane2.ts— живая дифф-вкладка (DiffEditable): композиция двух настоящих редакторов — стороны этоTextFileModel+EditorComponentвTextEditorPane(file-сторона — общая модель из реестра, untitled — своя, git/clipboard/диск — снимок read-only); контейнерDiffPaneElement(строка подписейHEAD │ a.ts+ колонки 50/50 + разделитель). Выравнивание — view zones, свёртка unchanged — обычный фолдинг с парным синком, подсветка —IExternalDecorations; раскладку считаетeditor/common/diff/diffV2Layout. Живой пересчёт — поonDidChangeContentмоделей сторон (debounce 200) с переносом свёрнутости и якорем скролла;getActiveEditor()/getActiveTabEditor()отдают активную сторону (команды, статус-бар, find, Ctrl+S работают по ней); revert-чанка —revertHunkAtCaret()(команда «Diff: Revert Hunk»); dirty-контракт —EditorService.needsCloseConfirm/collectDirtyсчитают стороны диффов. Снимочные стороны автоосвежаетcontrib/diff/DiffSnapshotRefreshContributionпоonDidChangeFile(US-31). Узкая панель (порог 100 колонок элемента, гистерезис) или тумблер «Diff: Toggle Inline View» (персистDIFF_VIEW_MODE_STATE) переводят пару в inline: original скрыт, modified на всю ширину, удалённые строки — зоны-призраки (computeInlineLayout).
- Extensions-кластер — магазин расширений в UI (план и сценарии —
../TODO/ExtensionsView.md):
contrib/extensions/common/extensionsWorkbench.ts— контрактIExtensionsWorkbenchService+ DI-токен + карточкаIExtensionListEntry. Контракт вcommon/, реализация вnode/— так вьюлет изbrowser/не импортируетnode/(иначе понадобилась бы запись в списке исключенийscripts/check-layers.mjs).contrib/extensions/node/extensionsWorkbenchService.ts— каталог реестра (черезcreateRegistrySource, кэш в памяти на сессию) ∪ установленное на диске (listInstalledExtensions); состояние карточки считается поlatest.enginesи версиям. Сетевой сбой не бросается:getCatalogError().contrib/extensions/browser/extensionsComponent.ts— вьюлетEXTENSIONS(view.id = "extensionsView"):InputElement+ListViewElementс двумя группами (MARKETPLACE / INSTALLED), фильтр локальный; активация строки открывает страницу через шовIExtensionsEditorTarget(EditorServiceструктурно, биндинг — вdiode/modules/extensionsModule.ts).contrib/extensions/browser/extensionEditorPane.ts— вкладка расширения:IEditorPaneс ресурсомextension:<id>(идентичность вкладки —EditorService.openPane), тело —ExtensionPageElementповерх строкbuildExtensionPageLines. Перенос по словам свой (wrapText) и считается в раскладке: wrap-элемента в tuidom нет, а откладывать пересборку в микротаск значило бы показать пустую страницу первым кадром.browser/parts/views/headerBodyViewElement.ts— общая раскладка «шапка натуральной высоты + тело на остаток» (Search и Extensions).
- Find/Suggest-кластер (этап 10) — поиск по файлу и автодополнение поверх
активного редактора (
EditorService):Components/Editor/FindComponent.ts—ThemedComponent; композиционный корень, собранный из примитивов (view=SizedBoxElement(preferredWidth×3) →BoxContainerElement→HFlexElementсо строкой запросаInputElement, счётчиком совпадений и кнопками ↑ ↓ ✕ButtonElement;view.id = "findWidget"). Ручного рендера нет — рамку/фон/раскладку дают примитивы, цвета из темы черезgetFindWidgetStyles(editorWidget.*+ счётчикdescriptionForeground+ «No results»editorError.foreground; кнопки изgetDialogButtonStyles). Дерево строится ОДИН раз в конструкторе и мутируется на месте (setCounter/setQueryменяют только текст/цвет счётчика и его зазор) — строка запроса никогда не переподключается к дереву и не теряет фокус между нажатиями. Публичная поверхность дляFindService— на самом компоненте (getQuery/setQuery/setCounter/focus+ колбэкиonQueryChange/onNext/onPrev/onClose), а не наview. Overlay-сессия — в ЛОКАЛЬНОМ слое группы редакторов (pointerPolicy: "passthrough"— док-виджет, клики мимо уходят в редактор); хост (OverlayHostElementгруппы) приходит через late-init шовattachHost(зовётWorkbenchComponentпосле постройки дерева).show()позиционирует виджет (правый край группы с 1-колоночным отступом, под tab strip) и фокусирует input.Services/FindService.ts— состояние поиска query → matches → current index:open(сеет запрос из однострочного выделения),close(курсор остаётся на текущем совпадении, подсветка снимается),next/prev(циклично), recompute поonQueryChange(стартовый индекс — первое совпадение от курсора); подсветка/reveal —setSearchDecorations/revealRangeактивногоEditorPane. Смена активного редактора закрывает виджет (подписка наonActiveEditorChanged— find оперирует только активным редактором).Components/Editor/SuggestComponent.ts— компонент suggest-попапа; владеетCompletionListElement(view.id = "suggestWidget"; НЕThemedComponent— контрол живёт на unthemed-палитреunthemedCompletionListStyles, маппинг на тему — отдельная задача) и overlay-сессией в глобальном body-слое (attachHost(BodyElement);capturesKeyboard: false— редактор сохраняет фокус, команды идут поsuggestWidgetVisible;close-on-outside).openAt(anchor)/setAnchor— позиционирование у каретки (EditorPane.getCaretAnchor).Services/CompletionService.ts— логика автодополнения (WP8):trigger()(провайдеры расширений черезEditorService.completionSource+ word-based fallbackcollectWordCompletionsиз всех открытых редакторов), сессия попапа (живойprefixRange, re-filter по мере набора, авто-suggest по эвристике «вставлен 1 word-символ» с задержкойautoSuggestDelayMs), accept (замена префикса/провайдерского range с догоном каретки;item.commandисполняется напрямую черезCommandRegistry.executeв микротаске), делегаторы select*/accept/hide для команд,onFocusChanged(зовётWorkbenchContextKeys.handleFocusChangeпри смене фокуса — клавиатурный уход с редактора закрывает попап).
- Shell-кластер (этапы 11–12) — корневой компонент, меню, layout, персист
сессии и контекст-ключи:
-
Components/Shell/WorkbenchComponent.ts— корневой компонент приложения (финал этапа 12; бывшийAppController): владеет корневой view (BodyElement,view.id = "workbench", +WorkbenchLayoutElementс сэшами), вставляет в неё view компонентов (EditorGroupComponentв центр,PanelComponentвниз,ExplorerComponentв сайдбар приsetWorkspaceFolder,StatusBarComponent,MenuBarComponent— ПОСЛЕ применения user keybindings), прикрепляет late-init швы (DialogService/ExplorerComponent/QuickInputComponent/SuggestComponentattachHost(BodyElement),FindComponent.attachHost(OverlayHostElement),LayoutService.attachLayout,WorkbenchContextKeys.attachView), вешает листенерыKeybindingDispatcherи фокус-хуки, регистрирует списокbuiltinActionsодним циклом. Фич-проводка (autoReveal, live-reload темы, контекст-меню редактора, командаworkbench.openFile, статус-бар) вынесена в workbench-contribution'ы — корень лишь прогоняет их по фазам через реестр (restored— вmount(),eventually— изmain.tsчерезrunEventuallyPhase(); см. «Workbench-contributions»). Выход (quitAction) делегируется корню через шовQuitHandlerDIToken→requestQuit: confirm-save черезLifecycleService, затем teardown TUI +process.exit; перезагрузка окна (reloadWindowAction) идёт тем же протоколом прощания, но заканчивается не выходом, а перезапуском процесса (шовWindowReloadHandlerDIToken). НаследникThemedComponent:updateStyles()красит корень (fg/bg body) и hover-цвет сэшей. Единственный компонент с lifecycle за пределами конструктора — bootstrap ведётmain.ts:setWorkspaceFolder→mount()(contribution'ы фазыrestored+ листенеры + restore layout до первого кадра) →run()→activate()(контекст-ключи, probe терминала, активация редакторов/ Explorer'а) →openFile/restoreOpenEditors→focusEditor→runEventuallyPhase(). -
Components/Shell/MenuBarComponent.ts—ThemedComponent; владеетMenuBarElement(view.id = "menuBar"; стили —getMenuStyles), строит top-уровень из submenu-записейMenuId.MenubarMainMenu, а entries каждого меню резолвит лениво при открытии попапа через живыеIMenu(см. раздел «MenuRegistry»);viewвставляет владелец корневой view (BodyElement.setMenuBar). -
Services/LayoutService.ts— логика workbench-layout'а: сайдбар (видимость/toggleSidebar/nudgeSidebarWidth/resetSidebarWidth) и нижняя панель (isPanelVisible/setPanelVisible; истина видимости — вPanelService, layout и контекст-ключpanelVisibleследуют заonDidChangeVisibility). Персист layout'а поверхIStateService(StateKeys.ts):restoreLayout()до первого кадра (+ синхронизация истины в PanelService), write-throughcaptureLayout()поWorkbenchLayoutElement.onDidChangeLayout(drag сэша и команды; во время restore глушится re-entrancy-guard'ом). СамWorkbenchLayoutElementостаётся контролом у владельца корневой view (WorkbenchComponent) и приходит через late-init шовattachLayout. -
browser/parts/sidebar/sidebarService.ts— реестр вьюлетов сайдбара и переключатель (activity bar'а нет, роль играют командыworkbench.view.*; показ вьюлета — подмена контента сайдбара черезLayoutService). Своих вьюлетов ему больше никто не приносит: единственный регистратор —ViewsService(см. ниже). -
browser/parts/views/— общая модель container↔view (аналог ViewContainer/PaneView/ViewsService VS Code). Ей подчиняются ВСЕ панели с содержимым: Explorer, Search, Source Control в сайдбаре и Problems, Output, Terminal в нижней панели.Дескрипторы (
viewsService.ts). Контейнер — «активити»:{id, title, location: "sidebar" | "panel", order?}. View — секция внутри него:{id, containerId, title, order, body, placeholder?, focus, minBodyHeight?, canToggleVisibility?}.containerId— реестровая связь, а не свойство контрола: перенос view между контейнерами ляжет сменой поля.body === nullрисуетplaceholder(аналогviewsWelcome); тело и виджет заголовка меняются на месте (setViewBody/setViewTitleWidget), поэтому место держит одну и ту же ссылку на корень контейнера всю жизнь.Merged выводится, а не объявляется. Контейнер с ровно одной ВИДИМОЙ секцией сливает заголовки (как VS Code): в сайдбаре заголовка контейнера нет вовсе, а единственная секция несёт его название и не сворачивается; в панели у секции нет и своей строки заголовка — им служит таб (
IPaneOptions.headerVisible: false). Скрыли предпоследнюю секцию — контейнер сам стал merged, вернули — заголовки разъехались.Видимость секций —
setViewVisible/isViewVisible/getContainerViews; последнюю видимую скрыть нельзя. Персист (workbench.views.state, write-through по действию пользователя, restore строго послеopenWorkspace) хранит свёрнутость, веса и скрытость.Раскрытость — опора ленивых view.
isViewExpanded(viewId)отвечает, видит ли пользователь тело секции (контейнер собран, секция не скрыта и не свёрнута; доattachContainer—false),onDidChangeViewExpandedшлёт переходы. Считает и диффит их самViewsService, а неPaneViewElement: тот пересоздаёт панели вrebuildPanesи молча игнорирует свёртку несворачиваемой (merged) секции, так что пер-панельное событие теряло бы переходы. Поэтому после каждого пути изменения (rebuildPanes— он же покрываетattachContainer, позднююregisterViewиsetViewVisible;restoreViewsState; пользовательский toggle черезonDidChangeState) состояние пересчитывается целиком и сравнивается с прежним. Первый потребитель — GRAPH контейнера Source Control: пока секция не раскрыта, она не строит строк и черезScmGraphService.setActiveпросит git-расширение не запускатьgit logвовсе.Прогресс — общий такт, а не таймер в элементе. Занятость секции рисуется спиннером сразу после названия (
CHANGES ⠹): полосы прогресса, как в VS Code, у нас быть не может — заголовок ровно одна строка, а лишняя дёргала бы layout секции. Кадры гонитProgressService(platform/progress) — один тикер на приложение, живой только пока есть что крутить; элемент сам ничего не анимирует. Мост —viewProgressContribution.ts→ViewsService.setViewSpinner. Кадр живёт в записи view и переприменяется вrefreshContainerTitleActions, иначе операция, начатая до пересборки секций, теряла бы индикацию; сам тик идёт коротким путём (applySpinner) и меню не резолвит — 10 Гц этого не стоят. Место выбрано так, чтобы ряд кнопок не дёргался: лишние колонки съедает филлер, а зоныhitZoneсчитаются по разложенным ширинам.Отрисовка заголовка — одна на всех:
viewTitleRowElement.ts(название + шеврон? + виджет + inline-кнопки + «⋯», плюс арифметика зон — хит-тест детей отключён, иначе не работает pointer capture у drag границы). На нём собраныpaneHeaderElement.ts(заголовок секции: toggle, drag границы, Shift+F10) иviewContainerHeaderElement.ts(заголовок активити; в панели он же — полоса контролов в таб-строке, без названия).paneViewElement.ts— стопка секций: развёрнутые тела делят высоту по весам, граница таскается за заголовок нижней секции (паттернSashElement).Меню. Одна точка на view —
MenuId.ViewTitle: группаnavigationс иконкой рисуется inline-кнопкой, остальное уходит в попап «⋯» и там не дублируется. У контейнера своя точкаMenuId.ViewContainerTitle. Обе фильтруются императивно (viewMenuVisible/containerMenuVisibleпоmenuContext) — глобальный when-ключ не годится: в сайдбаре видимы несколько секций сразу. Состав попапов собираетViewsService:Заголовок «⋯» секции (2+ видимых) overflow этой view контейнера (2+ видимых) его команды + подменю Views с чекбоксами видимости секций merged (одна видимая) overflow view + подменю с названием контейнера (его команды + Views) Пункты «Views» динамические, поэтому идут не контрибуцией, а собственным списком
MenuEntryчерезgetEntriesделегата контекст-меню.Потребители: Explorer (одна секция), Search (одна), Source Control (две — CHANGES с контролами коммита
ScmInputComponentв теле view и GRAPH), Problems / Output / Terminal (location: "panel", по одной секции; переключатель каналов Output —setViewTitleWidget, он и уезжает в таб-строку). Редактор Output садится на фон панели черезTextEditorPane.backgroundToken(EditorComponent.backgroundToken— имя токена темы, по умолчаниюeditor.background); свой фон заодно снимает тематическийeditorGutter.background, иначе гуттер остался бы полосой цвета редакторской группы. -
Services/WorkbenchStateService.ts— персист открытых редакторов (headless):openWorkspace(per-project стор),captureOpenEditors(write-through — собственная подписка наEditorService.onActiveEditorChanged),getOpenEditorsToRestore/restoreOpenEditors(реплей выживших путей + активная вкладка). См. State.md. -
Services/WorkbenchContextKeys.ts— выставляет контекст-ключи (ContextKeys.ts) из фокуса и сервисов:update()читает активный элемент из FocusManager корневой view (шовattachView; ключиtextInputFocus/inputWidgetFocus/listFocus/terminalFocus+ передача активногоInputElementвInputWidgetService), состояние сервисов (editorGroupHasEditors/editorTabsMultiple/panelVisible/findWidgetVisible/suggestWidgetVisible/terminalIsOpen) и терминальное окружение (tier/os/cap_/mode_; динамическиеmode_<name>регистрирует в конструкторе + подписка наonDidChange). Замыкает на себя хукKeybindingDispatcher.updateContextKeys;handleFocusChange(capture focus/blur листенеры вешает владелец дерева) сбрасывает незавершённый чорд и закрывает suggest-попап при уходе фокуса с редактора. -
Экшены:
LayoutActions.ts(toggle sidebar Ctrl+B, show explorer Ctrl+Shift+E, reveal active file, width-команды, toggle panel Ctrl+J, Problems Ctrl+Shift+M) иTerminalActions.ts(toggle Ctrl+/ new Ctrl+Shift+на tier kitty/csi-u) — поверх LayoutService/PanelService/ TerminalService/WorkbenchContextKeys.
-
Components/— UI-компоненты:StatusBar/(пилот),Dialogs/,Panel/,Explorer/,QuickInput/,Editor/иShell/(корневойWorkbenchComponent+ меню-бар).
- ID команд, отражающих VS Code Workbench/Editor, именуются в стиле VS Code
(
workbench.action.closeActiveEditor). - Доступность кейбиндингов — через typed when-контексты из
Workbench/Services/ContextKeys.ts(ContextKeyService); фокус/UI-состояния обновляетWorkbenchContextKeys.update(). - Кейбинды адаптируются к терминалу по трём осям — capability / tier
(
legacy < csi-u < kitty) / mode (local/ssh/tmux) — доступны в when-клаузах (tier == 'kitty',cap_osc52,mode_ssh,os == 'mac'). Default-бинды задают tier-зависимые fallback'и через per-bindingwhen; пользовательские — черезkeybindings.json(VS Code-семантика-commandдля unbind). - Tier определяется по env, но под мультиплексором env-флаги хост-терминала
(
KITTY_WINDOW_IDи родня) не считаются доказательством: расширенные клавиши доходят, только если их пропускает сам tmux (extended-keys on). Внутри tmux ждём подтверждения — probeCSI ? uили реально увиденный CSI-u ввод (noteExtendedKeysObserved). Иначе tier завышался, Ctrl+Shift+F приезжал неотличимым от Ctrl+F, а legacy-фоллбэки были выключены — то есть терялись и комбинация, и запасной путь. - Вторая часть аккорда — с модификатором, если команда переключает вкладку.
Парный
keypressзакреплён за целью своегоkeydown(TuiApplication.pinnedKeypressTarget), и когда команда меняет активную панель, эта цель уезжает из дерева: глобальный capture-обработчикKeybindingDispatcherдо неё уже не достаёт и проглотить клавишу не может — голый символ печатается в документ, который команда только что покинула. ОтсюдаCtrl+K Ctrl+BуnavigateBack, а неCtrl+K -. Баг движка — в трекере (docs/TODO/README.md). - Экшены объявляются
CommandAction/registerActionвWorkbench/Actions/; упорядоченный список —builtinActions.ts, регистрируетWorkbenchComponentодним циклом. Порядок важен: резолвер берёт последний зарегистрированный биндинг с проходящимwhen.
Виджет со сколько-нибудь сложным поведением строится из трёх частей, а не из «толстого» элемента:
- Service/Component (слой Workbench) — логика, I/O, подписки, оркестрация;
- Element (слой TUIDom) — тонкий: только render + локальные input-события;
- опц. State-класс — выделенное изменяемое состояние виджета.
Связь двунаправленная и без обратной зависимости TUIDom → Workbench:
element.onX = … (element → component) и component.update(view)
(component → element). Эталоны: EditorGroupComponent ↔ EditorTabStripElement,
InputWidgetService ↔ InputElement + InputState. «Контроллеры под видом
элемента» (напр. MenuBarElement, ContextMenuLayer) сводим к этому паттерну —
см. ../TODO/Inspector.md.
Где живёт Element. Элемент общего назначения (его публичный API не упоминает
понятий Diode) — в @tuidom/elements. Diode-специфичному элементу в tuidom не
место: либо он вовсе не существует — компонент является композиционным корнем
и собирает view из примитивов (FindComponent, StatusBarComponent,
EditorGroupComponent, диалоги), либо, если посимвольная раскладка оправдывает
ручной render (как у EditorElement), живёт рядом со своим компонентом в
parts/*.
Смешанный случай — QuickPickElement (parts/quickinput/): сам он собран из
примитивов (InputElement, ListViewElement, флексы, PaddingContainerElement)
и своего render'а не имеет, а ручной остался ровно на хроме, который композицией
не выражается, — рамка с врезанным в неё заголовком и сепаратором
(QuickPickFrameElement). Виджет приехал из движка, где по этому же критерию
лежать не должен был; остальные кандидаты на возврат —
../TODO/EngineWidgetRepatriation.md.
Зависимости слоя: Workbench → { Editor, TUIDom, Theme, Configuration, Common,
интерфейс Backend }. Workbench — верхний слой ядра приложения; выше него только
Extensions (host-адаптеры) и App (main.ts).