Часть архитектуры Diode — обзорная карта в ../ARCHITECTURE.md.
Загрузка VS Code-совместимых расширений. Сейчас поддержаны contributes.languages и contributes.grammars для builtin (extensions/ — 48 языковых паков из microsoft/vscode, импорт verbatim скриптом с пином тега) и user-расширений (<userData>/extensions/<publisher>.<name>-<version>/). Остальные contribution points — отдельные фазы (см. ../TODO/Extensions.md).
Контракты/швы:
IExtension—{ id, manifest, location, isBuiltin }.ExtensionScannerпарситpackage.jsonчерезIAssetAccess;mergeExtensionsразруливает конфликты id (builtin побеждает user).LanguageRegistry implements ILanguageService— собираетcontributes.languages, резолвит language по пути файла. Неочевидное правило разрешения: приоритетfilenames→filenamePatterns(мини-glob) →extensions; при конфликте одного.extпобеждает зарегистрированный позже (user поверх builtin, как в VS Code). Seed'ит core-языкplaintextкак fallback.ExtensionTokenizationContributor— регистрирует грамматики из всехcontributes.grammarsвTokenizationRegistry.apply()синхронный и без I/O: кладёт ленивые фабрики (registerLazy), а.tmLanguage.jsonпарсится при первомTokenizationRegistry.load(languageId)— его дёргаетEditorComponent.ensureTokenizerForLanguage, когда язык реально понадобился документу (наш аналогonLanguage; к activation-events extension host'а отношения не имеет — языковые паки декларативные, безmain). Пока грамматика едет, документ работает наPlainTextTokenizer, а подъехавший support пересаживается черезonDidChange. Мотив: 77 builtin-грамматик — 6.6 MB JSON, ~420 мс на полную загрузку; раньше всё это лежало на пути к первому кадру с содержимым файла.
Две вещи прикрывают ленивость, чтобы пользователь не видел неподсвеченный текст:
- Стартовые файлы —
main.tsчерезpreloadGrammarsForFilesждёт грамматики именно тех файлов, что сейчас откроются (обычно один язык, ~2 мс), доopenFile. Ждать после поздно:awaitотдаёт event loop, и отложенный рендер успевает нарисовать кадр на fallback'е. Пути берутся из CLI либо изWorkbenchComponent.getOpenEditorsToRestore()— тот же список, что откроетrestoreOpenEditors(), чтобы знание о восстанавливаемой сессии не расползалось по бутстрапу. - Остальные грамматики — фоновый
preloadAll()(setImmediateпосле первого кадра), чтобы переключение вкладки на другой язык не ждало парсинга. ExtensionInstaller— установка/удаление/список из.vsix(CLI--install-extensionи т.п., до подъёма TUI): распаковка с защитой от zip-slip + атомарныйrenameв каталог, который ждётExtensionScanner.- Установка по id из реестра (
platform/extensionManagement/) —installFromRegistryповерх того жеinstallVsix: мета → совместимая версия (engines↔DIODE_VERSION/VSCODE_SHIM_VERSION) → артефакт →sha256→ установка. Источник за швомIExtensionRegistrySource: каталог (FileExtensionRegistrySource) или сеть (HttpExtensionRegistrySource); выбираетcreateRegistrySourceпо значению--registry, без флага — публичный реестр. Формат и политика наполнения — docs/TODO/Marketplace.md.
Изоляция кода расширений от ядра. Host форкает один subprocess (тот же бинарь / main.ts с env DIODE_EXTENSION_HOST=1) и общается с ним через RPC поверх Node IPC. Швы:
IMessageChannel— транспортно-агностичный двунаправленный канал. Реализации:IpcMessageChannel(Node IPC) иcreateInProcessChannelPair()(unit-тесты).RpcEndpoint— request/response/notification поверх канала, симметричный (запросы шлют обе стороны).- vscode-стаб: subprocess патчит
Module._cache["vscode"]+Module._resolveFilename(installVscodeStub), поэтомуrequire("vscode")расширения отдаёт нашу поверхность. Сама поверхность собрана вExtensions/Host/Vscode/— тонкий ассемблерbuildVscodeNamespace(rpc)над общим контекстом (window/workspace/languages/commands+ value-типыPosition/Range/Uri/enum'ы). Документы держат стабильную идентичность (DocumentRegistry: один объект наfileName, обновления мутируют его на месте — нужно дляactiveTextEditor.document === doc). - Save-participant / completion seams:
TextFileModel.save()асинхронный — при заданномsaveParticipantонawait-ится до записи (правки клампятся к границам, уходят одним undoable-батчем). Инъекция seam'ов — вWorkbench/Modules/ExtensionHostModule.ts; ядро (TextFileModel/EditorService) про extension-слой не знает. - Folding-провайдер seam (#194):
languages.registerFoldingRangeProvider— путь host↔subprocess по образцу completion (languages.provideFoldingRangesRPC). Core-seam —EditorService.foldingRangeSource(типeditor/common/languages/iFoldingSource.ts), инъекция вExtensionHostModule;EditorComponentпри пересчёте фолдов запрашивает провайдерские области и мержит их поверх indentation-фолдов (union: provider ∪ indentation, provider выигрывает по общейstartLine), переносяisCollapsedпоstartLine. Провайдер, появившийся после открытия файла, пере-триггерит пересчёт черезExtensionHost.onFoldingProvidersChanged. Стоковыйmaptz.regionfolderзаводится этим путём (тестextensionHost.maptzRegionfolder.test.ts). - Editor-write seam (#194):
TextEditor.edit(cb)(сTextEditorEditinsert/replace/delete),editor.selection(s)геттер/сеттер,window.visibleTextEditors, value-типSelection. Правки/выделения субпроцесса едут хосту (editor.applyEdit/editor.setSelection) и применяются к активному редактору портаIEditorOptionsService(правки — одним undoable-батчем черезapplyExternalEdits, с клампом к границам; guard по совпадению uri). - Document sync seam (LSP): наивный full-text push host → subprocess —
editor.didOpen/editor.didChange(IWireDocumentSyncSnapshot: uri, languageId,version=versionIdмодели, text). Продюсер —bindDocumentSync(api/browser/documentSyncAdapter.ts, подписки в module/харнессе); субпроцесс кладёт текст вDocumentRegistryи фаеритworkspace.onDidOpen/onDidChangeTextDocument(change — одна full-range правка, валидно для Full и Incremental sync LSP-сервера). Гейт по подписке (workspace.updateSubscriptions.documentSync, переход 0→1 доталкивает активный документ), didChange коалесируется в тик, лимит 8 МБ; активный документ пушится наhost.readyДО активации (стоковыйvscode-languageclientчитаетworkspace.textDocumentsнаstart()). Люфты (нет didClose, только активный редактор) — docs/TODO/LSP.md. - Definition-провайдер seam (LSP):
languages.registerDefinitionProvider— калька с completion (languages.provideDefinitionRPC, таймаут 5000 мс — холодный language server). Core-seam —EditorService.definitionSource(типeditor/common/languages/iDefinitionSource.ts); UI — contribgotoDefinition(DefinitionService+ F12editor.action.revealDefinition), кросс-файловая навигация паттерном Problems reveal (openUri+goToPosition). Результат провайдераLocation | Location[] | LocationLink[]нормализуется субпроцессом (у LocationLink прицел —targetSelectionRange ?? targetRange). - Hover-провайдер seam (LSP):
languages.registerHoverProvider— калька с definition (languages.provideHoverRPC, таймаут 5000 мс). Субпроцесс обходит ВСЕ совпавшие по селектору провайдеры и конкатенирует непустые ответы в порядке регистрации (WireHover {contents, range?}, contents — блоки сырого markdown;MarkedString {language, value}сериализуется fenced-блоком). Core-seam —EditorService.hoverSource(типeditor/common/languages/iHoverSource.ts); UI — contribhover(параHoverService/HoverComponentпо образцу suggest: overlay-сессия у каретки без захвата фокуса,stripMarkdownв плоский текст, закрытие по Escape/правке/каретке/фокусу; ключeditorHoverVisible). - References-провайдер seam (LSP):
languages.registerReferenceProvider— та же калька (languages.provideReferencesRPC, таймаут 5000 мс; в параметрах LSP-контекстincludeDeclaration). Субпроцесс обходит ВСЕ совпавшие провайдеры и конкатенирует ответы в порядке регистрации (WireReference {uri, range}, сериализатор общий с definition). Core-seam —EditorService.referenceSource(типeditor/common/languages/iReferenceSource.ts); UI — contribreferences(вьюлет сайдбара REFERENCES:ReferencesServiceспрашивает провайдеров и добирает текст строк,ReferencesComponentрисует список; ключиreferencesViewletVisible/hasReferenceResult). Особенность против definition/hover: ответ несёт только координаты, поэтому строку кода панель читает сама — из открытой модели, иначе черезIFileSystemProviderRegistry. - Signature-help seam (LSP):
languages.registerSignatureHelpProvider— та же калька (languages.provideSignatureHelpRPC, таймаут 5000 мс), но с двумя отличиями. Первое: провайдеров обходим по очереди и возвращаем ПЕРВЫЙ непустой ответ (vscode API предписывает именно это — склеивать две разныеactiveParameterнечем). Второе: в параметрах едет LSP-контекст (triggerKind,triggerCharacter,isRetrigger, эхо показанной подсказкиactiveSignatureHelp— по нему сервер удерживает выбранную пользователем перегрузку). Триггер- и ретриггер-символы объявляет сервер: они приезжают в ядро черезlanguages.updateSubscriptionsрядом сcompletionTriggerCharacters. Core-seam —EditorService.signatureHelpSource(типeditor/common/languages/iSignatureHelpSource.ts); UI — contribparameterHints(ParameterHintsServiceловит набор триггер-символа и перезапрашивает подсказку на каждую правку,ParameterHintsComponentдержит overlay-сессию НАД строкой каретки; ключиparameterHintsVisible/parameterHintsMultipleSignatures). СтабvscodeTypesпришлось дополнить value-классамиSignatureHelp/SignatureInformation/ParameterInformationи enum'омSignatureHelpTriggerKind— их конструирует и читает конвертер стокового клиента. - Completion-провайдер seam (LSP):
languages.registerCompletionItemProvider— RPClanguages.provideCompletionItems(ответWireCompletionResult {items, isIncomplete}) иlanguages.resolveCompletionItem({id}→ detail/documentation/additionalEdits). Core-seam'ы:EditorService.completionSource,.completionResolver,.completionTriggerCharacters(типы —editor/common/languages/iCompletionSource.ts), инъекция вExtensionHostModule/ExtensionTestHarness. Резолв идёт по id"<cacheId>.<index>"в кэш последних 2 ответов субпроцесса: клиенту нужно вернуть тот же самый объект пункта (ProtocolCompletionItemс приватнымdata), а не его копию. Триггер-символы сервера приезжают вlanguages.updateSubscriptions(completionTriggerCharacters) и меняются на лету — host фаеритonCompletionTriggerCharactersChanged. Грабли стека (падающий конвертер безCompletionList/SnippetString, dot-accessor-пункты) — docs/TODO/LSP.md. - Файловые watcher'ы (
workspace.createFileSystemWatcher):RelativePattern+FileSystemWatcher— настоящие. Субпроцесс держит только id и эмиттеры (fileWatcherNamespace.ts), а следит за деревом ядро: notifyworkspace.watcher.create {id, base, pattern, ignore*Events}→ портIExtensionFileWatcher(в DI —FileWatcherAdapterповерхITreeFileWatcher+ excludes изfiles.watcherExclude) → обратный notifyworkspace.watcher.events. Матчинг шаблона — на хосте: субпроцессу уезжают только подошедшие события, а не весь поток по воркспейсу. Рекурсивность выводится из шаблона (*— только прямые дети,**//— поддерево) — на этом держится дешёвый watcher.gitу встроенного git. - Сток диагностик (LSP):
languages.createDiagnosticCollection(наивная: хранит оригинальныеDiagnostic, честные get/has/forEach/итератор) → notifydiagnostics.publish→ опцияExtensionHost.diagnosticsSink→ мост вMarkerService.changeOne(module); потребители (squiggle, панель Problems) слушаютonDidChangeMarkersи правок не требуют. - Прогресс (LSP): настоящий
window.withProgress— notifywindow.progress.{start,report,end}→ опцияExtensionHost.progressSink→ProgressStatusBarAdapter(запись статус-бара с анимированным спиннером; report обновляет message/проценты, end снимает; на смерть subprocess'а host гасит живые handle'ы). Отмена не поддержана — токен не стреляет. - Output-каналы (LSP): настоящий
window.createOutputChannel— notifyoutput.append/output.show→ опцияExtensionHost.outputSink→ExtensionOutputAdapter(ленивая регистрация каналаextensions.<slug(name)>вOutputChannelRegistry, строки — логгеромILogService,show— командаworkbench.action.output.show.<id>).appendбуферизуется в subprocess до\n(панель строчная). - Builtin LSP-клиент:
extensions/diode-lsp-typescript— стоковыйvscode-languageclient@10в esbuild-бандле, декларативная таблица серверов{ languageIds, resolveCandidates }(lib/resolveServer.ts), ленивая активацияonLanguage:*. Сервер вшит в поставку (ts-server.bundle→ XDG-кэш,loadTsServer.ts) и является дефолтным кандидатом резолва: настройка → workspacenode_modules/.bin→ bundled → PATH; пути и режим рантайма инжектирует host синтетическимиconfigDefaults(builtinConfigInjectionвmain.ts). Рантайм «как VS Code»: JS-энтрипоинты запускаютсяprocess.execPathсубпроцесса (dev/self-extract — настоящий node; SEA — diode-бинарь в node-режимеDIODE_RUN_AS_NODE=1— ранний branchrunAsNode.ts, калькаELECTRON_RUN_AS_NODE; env сервера всегда снимаетDIODE_EXTENSION_HOST). Наивные стабы поверхности languageclient (no-op провайдеры, naive-события, env,versionв лок-степе сextensions/VSCODE_VERSION) — статусы и шаги закрытия в docs/TODO/LSP.md. - Проекция выделения host → subprocess (#194): первичное выделение едет в
IActiveEditorMetaна смене активного редактора, а каждое движение каретки — отдельной нотификациейeditor.selectionChanged(IActiveEditorSelections: uri + все выделения). Продюсер —EditorService.onDidChangeActiveEditorSelection(подписка живёт на группе и сама переезжает на новый активный редактор) →IEditorOptionsService.onActiveEditorSelectionChanged(коалесинг в пределах тика + эхо-гард: выделение, поставленное самим субпроцессом черезeditor.setSelection, назад не уезжает). Отдельное сообщение, а не повторныйactiveEditorChanged, потому что последний дёргаетonDidChangeActiveTextEditor— встроенный git пересчитывал бы статус на каждое нажатие стрелки. Без этой проекции любая команда расширения, читающаяactiveTextEditor.selection, видит состояние момента открытия файла (обычно(0,0)) и молча ничего не делает — так ломались все командыmaptz.regionfolder. - Изоляция сбоев субпроцесса:
runExtensionHostSubprocessставитprocess.on("unhandledRejection")— расширение, не поймавшее свой промис (стоковый maptz послеwrapWithRegionfire-and-forget зовёт отсутствующуюeditor.action.formatDocument), логируется в stderr и не уносит с собой host со всеми остальными расширениями. contributes.keybindings(#194): декларативный contributorextensionKeybindingContributor.ts(не через host) регистрирует манифестные биндинги вKeybindingRegistryПОСЛЕ builtin (расширение может переопределить встроенный аккорд);key/mac/linux/win,-command— снятие.IExtensionRegistrationнесётconfigDefaults,commandTitlesиactivationEvents.commandTitlesнужны, чтобы рантайм-registerCommandрасширения показался в палитре (host заводит прокси вCommandRegistryс этим title; иначе команда исполнима, но невидима).activationEvents— см. «Активация» ниже.- Lifecycle:
ExtensionHost.dispose()— gracefulhost.shutdown→SIGTERM→SIGKILL;disposeNow()— то же для путей, где event loop дальше не крутится (перезагрузка окна: сразу за ней идётspawnSyncнового процесса), поэтому субпроцесс снимается сигналом синхронно, иначе он остался бы сиротой. В DI —ExtensionHostDIToken.main.tsсодержит ранний branch на env-флаг: subprocess уходит вrunExtensionHostSubprocess(), обычный запуск — вrunEditor().
Расширения активируются лениво по manifest.activationEvents — до триггера код расширения не грузится и subprocess под него не поднимается. Механика целиком на родительской стороне ExtensionHost; ядро про activation-events не знает.
- Регистрация ≠ активация.
registerExtension(reg)— синхронный bookkeeping: кладёт reg вpending, регистрируетcommandTitles(палитра видит команды до активации), возвращает disposable. Subprocess не поднимается. activateByEvent(event)— идемпотентно активируетpending-расширения, чьиactivationEventsсодержат событие:ensureSubprocess()→host.activateExtensionRPC → перенос в реестр активных. Ошибки spawn/parseActivateParams(в т.ч. конфликтsource/mainPath) всплывают здесь, а не на регистрации.- Дефолт. Пустой/отсутствующий
activationEventsнормализуется в["*"](eager) — расширения без описанных событий (напр. builtingit) ведут себя как раньше. - Поддержанные события:
*иonStartupFinished(фаеритmain.tsпосле регистрации, когда файлы открыты),onLanguage:<id>. СтартовыйonLanguage:*для уже открытого редактора фаеритmain.ts; последующие (переключение/открытие вкладок) — seamEditorService.onActiveEditorChanged→activateByEvent("onLanguage:"+langId)вExtensionHostModule.ts(тот же паттерн, чтоcompletionSource/saveParticipant).onCommand:*— пока не реализован (Phase 7). - Тест-хелпер:
registerAndActivate(host, reg)(TestUtils/ExtensionTestHarness.ts) =registerExtension+activateByEvent("*"); харнесс фаеритactivateEvents(дефолт["*"]) после регистрации.
Extensions/builtin/diode-settings/ — code-расширение, активируется только по onLanguage:json/onLanguage:jsonc (доказательство лениости: пока не открыт JSON — не грузится). В activate() регистрирует registerCompletionItemProvider с селектором pattern:"**/settings.json" → в settings.json подсказывает известные ключи настроек. Каталог ключей вшит на этапе сборки: scripts/generate-settings-schema.mjs (запускается из build-extensions.mjs перед esbuild) собирает ключи из app-дефолтов (Configuration/defaults.ts) + contributes.configuration всех builtin и пишет settings-schema.generated.ts, который бандлится в out/extension.cjs. Никакого рантайм-API за схемой расширение не ходит.
Extensions/Api/vscode.d.ts — стадийная копия upstream microsoft/vscode:src/vscode-dts/vscode.d.ts, всё line-commented кроме активной поверхности. Это дословная копия реального API, а не стаб под реализацию. Инвариант: файл меняется ТОЛЬКО снятием // .
Структура файла:
- Шапка — провенанс (upstream tag + commit SHA + permalink) и ссылка сюда.
- Активный
declare module "vscode"— дословно раскомментированные строки upstream. Единственный human-owned блок. Framing-строки (самdeclare module "vscode" {, его}, глобальныйThenable) — единственное не-upstream в этой части. - Строка-сентинел
//@diode:begin-upstream-verbatim …. - Дормант — вся upstream-копия, каждая строка с
//. Генерируется, вручную не редактируется.
Пиннинг. Тег зафиксирован в шапке и согласован с extensions/VSCODE_VERSION (сейчас 1.127.0) — держи их в лок-степе. Пин нужен, чтобы обновление upstream шло ручным трёхсторонним merge: base = vscode.d.ts запинненной версии, theirs = новый upstream, ours = наш файл с раскомментированными блоками.
Как добавить API. Найди нужный блок в дормантной части и подними дословно (сняв // ) в активный модуль — не сужать / не переписывать / не переоформлять (комментарии тоже upstream). Если блок тянет ещё не раскомментированный тип (dependency closure) — раскомментируй и его. Runtime-значение может опережать типовую декларацию (namespace отдаётся через as unknown as typeof vscode).
Bounded member-level uncommenting. Для «тяжёлого по closure» блока (namespace/интерфейс/класс, чьё полное upstream-тело тянет непрактичное дерево зависимостей) можно раскомментировать подмножество членов, оставив прочие в дормантной части. Каждая раскомментированная строка обязана быть байт-в-байт равна upstream. Так сделаны, например, window/workspace/languages (только реализованные функции), TextEditor (document/selection/selections/options/edit), ExtensionContext (subscriptions), TextDocument/FileStat/CompletionItem (подмножество полей).
Инструмент. scripts/import-vscode-dts.mjs:
- (без флагов) — регенерировать дормант из запинненного тега + обновить провенанс в шапке (активный модуль не трогает);
--check— сверить, что дормант байт-в-байт равен upstream тега (drift guard, нужна сеть);--verify-active— offline-проверка инварианта: каждая кодовая строка активного модуля дословно присутствует в дормантной копии. Прогоняй после ручного раскомментирования.
Семантические отклонения Diode (тип совпадает с upstream, отличается только смысл/JSDoc-намерение):
| Символ | Отклонение |
|---|---|
version |
Возвращает версию Diode, а не VS Code (upstream JSDoc говорит «editor»). |
Event<T> |
Слушатель (e) => any (upstream); хост оборачивает подписки через EventEmitterImpl в Vscode/VscodeTypes.ts. |
TextEditorOptions.indentSize |
Хост алиасит его к tabSize (Diode пока не различает); editorconfig шлёт indent_size так. |
| Namespaces / value-типы | Рантайм может опережать/отставать от типов; поверхность собирается в Vscode/* и отдаётся как as unknown as typeof vscode.*. |
Кодировки (#106). workspace.openTextDocument(…, { encoding }) реально декодирует не-utf8 файлы осью encoding ядра (src/vs/editor/common/model/encoding.ts): explicit-кодировка побеждает BOM-сниф, неизвестный id молча откатывается к дефолту (контракт vscode.d.ts); эфемерный документ детектит и encoding, и eol. ExtHostTextDocument.encoding/.eol — живые: обновляются метой editor.activeEditorChanged и снапшотом will-save (IWireWillSaveParams.encoding). Дормантные workspace.decode/encode не раскомментированы (не понадобились).
Зависимости: Extensions → Editor (через ILanguageService, TextMateGrammarLoader, TokenizationRegistry), Common. Подмодуль Extensions/Host дополнительно → Workbench (адаптеры над EditorService; мост файловых декораций типизирован портом IFileDecorationsTarget, в DI его реализует ExplorerService) и → Theme (ThemeColorResolverAdapter над ThemeService) — единственное место, где Extensions поднимается выше Editor.
Мост декораций (vscode.window.createTextEditorDecorationType / registerFileDecorationProvider). Value-типы (ThemeColor/FileDecoration/OverviewRulerLane/DecorationRangeBehavior) живут в Vscode/VscodeTypes.ts; WindowNamespace держит тип декорации локально (монотонный числовой key) и шлёт хосту RPC-нотификации, сериализуя ThemeColor как { $themeColor: id } (см. WireTypes.ts). Хост держит реестр key → { overviewRulerColorId?, isWholeLine } (gutter-тип = есть overviewRulerColor), резолвит ThemeColor через IThemeColorResolver и проталкивает: gutter change-bar'ы — в EditorComponent.setGutterChangeDecorations (по совпадению пути, образец — DiagnosticsService), файловые декорации — в ExplorerService.setFileDecorations. На theme.onDidChange все держимые декорации пере-резолвятся и пере-push'атся. Ядро про источник декораций (git/SCM) не знает — адаптеры отдают уже резолвнутые packed-RGB цвета.