Skip to content

Latest commit

 

History

History
86 lines (69 loc) · 32.4 KB

File metadata and controls

86 lines (69 loc) · 32.4 KB

Extensions/

Часть архитектуры 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 по пути файла. Неочевидное правило разрешения: приоритет filenamesfilenamePatterns (мини-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: мета → совместимая версия (enginesDIODE_VERSION/VSCODE_SHIM_VERSION) → артефакт → sha256 → установка. Источник за швом IExtensionRegistrySource: каталог (FileExtensionRegistrySource) или сеть (HttpExtensionRegistrySource); выбирает createRegistrySource по значению --registry, без флага — публичный реестр. Формат и политика наполнения — docs/TODO/Marketplace.md.

Extensions/Host/ — extension host (real subprocess)

Изоляция кода расширений от ядра. 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.provideFoldingRanges RPC). 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)TextEditorEdit insert/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.provideDefinition RPC, таймаут 5000 мс — холодный language server). Core-seam — EditorService.definitionSource (тип editor/common/languages/iDefinitionSource.ts); UI — contrib gotoDefinition (DefinitionService + F12 editor.action.revealDefinition), кросс-файловая навигация паттерном Problems reveal (openUri + goToPosition). Результат провайдера Location | Location[] | LocationLink[] нормализуется субпроцессом (у LocationLink прицел — targetSelectionRange ?? targetRange).
  • Hover-провайдер seam (LSP): languages.registerHoverProvider — калька с definition (languages.provideHover RPC, таймаут 5000 мс). Субпроцесс обходит ВСЕ совпавшие по селектору провайдеры и конкатенирует непустые ответы в порядке регистрации (WireHover {contents, range?}, contents — блоки сырого markdown; MarkedString {language, value} сериализуется fenced-блоком). Core-seam — EditorService.hoverSource (тип editor/common/languages/iHoverSource.ts); UI — contrib hover (пара HoverService/HoverComponent по образцу suggest: overlay-сессия у каретки без захвата фокуса, stripMarkdown в плоский текст, закрытие по Escape/правке/каретке/фокусу; ключ editorHoverVisible).
  • References-провайдер seam (LSP): languages.registerReferenceProvider — та же калька (languages.provideReferences RPC, таймаут 5000 мс; в параметрах LSP-контекст includeDeclaration). Субпроцесс обходит ВСЕ совпавшие провайдеры и конкатенирует ответы в порядке регистрации (WireReference {uri, range}, сериализатор общий с definition). Core-seam — EditorService.referenceSource (тип editor/common/languages/iReferenceSource.ts); UI — contrib references (вьюлет сайдбара REFERENCES: ReferencesService спрашивает провайдеров и добирает текст строк, ReferencesComponent рисует список; ключи referencesViewletVisible / hasReferenceResult). Особенность против definition/hover: ответ несёт только координаты, поэтому строку кода панель читает сама — из открытой модели, иначе через IFileSystemProviderRegistry.
  • Signature-help seam (LSP): languages.registerSignatureHelpProvider — та же калька (languages.provideSignatureHelp RPC, таймаут 5000 мс), но с двумя отличиями. Первое: провайдеров обходим по очереди и возвращаем ПЕРВЫЙ непустой ответ (vscode API предписывает именно это — склеивать две разные activeParameter нечем). Второе: в параметрах едет LSP-контекст (triggerKind, triggerCharacter, isRetrigger, эхо показанной подсказки activeSignatureHelp — по нему сервер удерживает выбранную пользователем перегрузку). Триггер- и ретриггер-символы объявляет сервер: они приезжают в ядро через languages.updateSubscriptions рядом с completionTriggerCharacters. Core-seam — EditorService.signatureHelpSource (тип editor/common/languages/iSignatureHelpSource.ts); UI — contrib parameterHints (ParameterHintsService ловит набор триггер-символа и перезапрашивает подсказку на каждую правку, ParameterHintsComponent держит overlay-сессию НАД строкой каретки; ключи parameterHintsVisible / parameterHintsMultipleSignatures). Стаб vscodeTypes пришлось дополнить value-классами SignatureHelp/SignatureInformation/ParameterInformation и enum'ом SignatureHelpTriggerKind — их конструирует и читает конвертер стокового клиента.
  • Completion-провайдер seam (LSP): languages.registerCompletionItemProvider — RPC languages.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), а следит за деревом ядро: notify workspace.watcher.create {id, base, pattern, ignore*Events} → порт IExtensionFileWatcher (в DI — FileWatcherAdapter поверх ITreeFileWatcher + excludes из files.watcherExclude) → обратный notify workspace.watcher.events. Матчинг шаблона — на хосте: субпроцессу уезжают только подошедшие события, а не весь поток по воркспейсу. Рекурсивность выводится из шаблона (* — только прямые дети, **// — поддерево) — на этом держится дешёвый watcher .git у встроенного git.
  • Сток диагностик (LSP): languages.createDiagnosticCollection (наивная: хранит оригинальные Diagnostic, честные get/has/forEach/итератор) → notify diagnostics.publish → опция ExtensionHost.diagnosticsSink → мост в MarkerService.changeOne (module); потребители (squiggle, панель Problems) слушают onDidChangeMarkers и правок не требуют.
  • Прогресс (LSP): настоящий window.withProgress — notify window.progress.{start,report,end} → опция ExtensionHost.progressSinkProgressStatusBarAdapter (запись статус-бара с анимированным спиннером; report обновляет message/проценты, end снимает; на смерть subprocess'а host гасит живые handle'ы). Отмена не поддержана — токен не стреляет.
  • Output-каналы (LSP): настоящий window.createOutputChannel — notify output.append/output.show → опция ExtensionHost.outputSinkExtensionOutputAdapter (ленивая регистрация канала 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) и является дефолтным кандидатом резолва: настройка → workspace node_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 — ранний branch runAsNode.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 после wrapWithRegion fire-and-forget зовёт отсутствующую editor.action.formatDocument), логируется в stderr и не уносит с собой host со всеми остальными расширениями.
  • contributes.keybindings (#194): декларативный contributor extensionKeybindingContributor.ts (не через host) регистрирует манифестные биндинги в KeybindingRegistry ПОСЛЕ builtin (расширение может переопределить встроенный аккорд); key/mac/linux/win, -command — снятие.
  • IExtensionRegistration несёт configDefaults, commandTitles и activationEvents. commandTitles нужны, чтобы рантайм-registerCommand расширения показался в палитре (host заводит прокси в CommandRegistry с этим title; иначе команда исполнима, но невидима). activationEvents — см. «Активация» ниже.
  • Lifecycle: ExtensionHost.dispose() — graceful host.shutdownSIGTERMSIGKILL; disposeNow() — то же для путей, где event loop дальше не крутится (перезагрузка окна: сразу за ней идёт spawnSync нового процесса), поэтому субпроцесс снимается сигналом синхронно, иначе он остался бы сиротой. В DI — ExtensionHostDIToken. main.ts содержит ранний branch на env-флаг: subprocess уходит в runExtensionHostSubprocess(), обычный запуск — в runEditor().

Активация (activationEvents)

Расширения активируются лениво по manifest.activationEvents — до триггера код расширения не грузится и subprocess под него не поднимается. Механика целиком на родительской стороне ExtensionHost; ядро про activation-events не знает.

  • Регистрация ≠ активация. registerExtension(reg) — синхронный bookkeeping: кладёт reg в pending, регистрирует commandTitles (палитра видит команды до активации), возвращает disposable. Subprocess не поднимается.
  • activateByEvent(event) — идемпотентно активирует pending-расширения, чьи activationEvents содержат событие: ensureSubprocess()host.activateExtension RPC → перенос в реестр активных. Ошибки spawn/parseActivateParams (в т.ч. конфликт source/mainPath) всплывают здесь, а не на регистрации.
  • Дефолт. Пустой/отсутствующий activationEvents нормализуется в ["*"] (eager) — расширения без описанных событий (напр. builtin git) ведут себя как раньше.
  • Поддержанные события: * и onStartupFinished (фаерит main.ts после регистрации, когда файлы открыты), onLanguage:<id>. Стартовый onLanguage:* для уже открытого редактора фаерит main.ts; последующие (переключение/открытие вкладок) — seam EditorService.onActiveEditorChangedactivateByEvent("onLanguage:"+langId) в ExtensionHostModule.ts (тот же паттерн, что completionSource/saveParticipant). onCommand:* — пока не реализован (Phase 7).
  • Тест-хелпер: registerAndActivate(host, reg) (TestUtils/ExtensionTestHarness.ts) = registerExtension + activateByEvent("*"); харнесс фаерит activateEvents (дефолт ["*"]) после регистрации.

Пример: builtin diode-settings (автодополнение настроек)

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 за схемой расширение не ходит.

Правило роста vscode.d.ts (важно)

Extensions/Api/vscode.d.ts — стадийная копия upstream microsoft/vscode:src/vscode-dts/vscode.d.ts, всё line-commented кроме активной поверхности. Это дословная копия реального API, а не стаб под реализацию. Инвариант: файл меняется ТОЛЬКО снятием // .

Структура файла:

  1. Шапка — провенанс (upstream tag + commit SHA + permalink) и ссылка сюда.
  2. Активный declare module "vscode" — дословно раскомментированные строки upstream. Единственный human-owned блок. Framing-строки (сам declare module "vscode" {, его }, глобальный Thenable) — единственное не-upstream в этой части.
  3. Строка-сентинел //@diode:begin-upstream-verbatim ….
  4. Дормант — вся 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 цвета.