Editor Extension And Contribution Registries
Purpose
Use this workflow when adding an editor domain or making a tool, command, inspector adapter, asset editor, validator, or importer discoverable as part of an existing domain. assets/editor/extensions.json is the ownership manifest: it says which extension owns each stable contribution ID. It does not instantiate the contribution or replace the native registry that implements its behavior.
The ownership boundary is deliberate. The extension registry owns domain identity, lifecycle state, labels, summaries, and contribution claims. EditorToolRegistry is the runtime gateway for dock-tool discovery, drawing, chrome, status, context requirements, and preview/resource demand; EditorDockTab remains the serialized workspace compatibility key while descriptors are migrated out of the legacy dock catalog. Import routing remains in src/editor/importer_registry.rs; asset-to-workbench routing lives in src/editor/asset_editor_registry.rs; inspector schemas and typed edit routing remain in src/editor/inspector_adapter.rs. Commands and validators likewise remain owned by their implementing systems even when their IDs are claimed by an extension. The manifest schema contains template_catalogs, but the current implementation has no template-catalog registration source, so that group must remain empty.
Extension enablement is executable editor state. Optional domains can be enabled or disabled from Engine Settings → Extensions; overrides persist atomically to aether_editor_extensions.json under the local config root. Disabled tool contributions are removed from dock and detached-window state and excluded from Window discovery, command-palette tool actions, and workspace application. Their inspector adapters, object factories, asset-editor routes, lifecycle-aware importer lookups, and authored-workspace diagnostics providers are also excluded. Validator enablement is captured as a sorted stable-ID selection for each background workspace job and is part of the workspace cache key. A lifecycle toggle immediately invalidates the old diagnostics projection; an already-running job with the previous selection is not allowed to install stale results. Required extensions declare "required": true and cannot be disabled. New contributions should query the registry's typed lifecycle methods rather than copying enabled IDs into subsystem-specific flags.
Steps
Add Or Change A Contribution
-
Choose the owning extension before writing manifest data. Prefer an existing domain such as
aether.core,aether.scene, oraether.runtime. Add a new extension only when the contribution has a distinct ownership and maintenance boundary; every extension requires a non-emptylabelandsummary. -
Choose an ID that is stable, namespaced, and independent of display text. Extension and contribution IDs may contain only lowercase ASCII letters, digits,
.,-, and_. Tool IDs have the stricterEditorToolIdinvariant: they must also contain a dot and may not start or end with one. Treat IDs as compatibility keys for commands, preferences, workspace manifests, and future plugin registrations; change labels rather than renaming IDs for presentation reasons. -
Implement the contribution in its native registry.
- For a dock tool, add or update its
EditorDockTabDescriptor, declare its draw/status providers, chrome policy, context capabilities, resource demand, and placement metadata, then map the compatibility-eraEditorDockTabto itsEditorToolIdincompatibility_tool_id. Dock hosts resolve those behaviors throughEditorToolRegistry; do not dispatch new tools directly fromEditorDockTab. Dock layouts still serialize enum variants, so do not remove that adapter when adding the stable ID. - For an importer, register an
EditorImporterDescriptorwith an ID, label, extensions, optional normalized asset-path prefix, asset kind, hot-reload policy, and priority. Higher priority wins; for equal priority, path-specific importers sort before general importers. For example,prefabs/*.tmxmust resolve toaether.importer.prefabbefore the general TMX importer. - For an asset editor, register an
EditorAssetEditorDescriptorwith a stable ID, code-declared extension owner, label, button label, resource kinds, optional path-prefix or path-contains matchers, priority, and typed handler. Higher priority resolves path-specific editors before kind-wide fallbacks. Project Files and the Asset Inspector must resolve this registry instead of adding anotherProjectAssetKindor path switch. - For an inspector adapter, add a stable adapter ID, object-kind binding, section/property schema, a property snapshot projection, and typed property-edit routes. The selected-object Inspector renders those schemas generically as text, numeric, boolean, enum, resource-reference, and read-only controls. Keep property IDs unique within the adapter, ensure every section refers to a declared property, and keep
read_onlysynchronized withhas_property_edit_route: startup invariants reject editable metadata without executable routing. - For a command or validator, register its implementation in the subsystem that dispatches it. A string in
extensions.jsonis only an ownership claim.
- For a dock tool, add or update its
-
Add the exact contribution ID to one currently backed group on its owning descriptor in
assets/editor/extensions.json:tools,commands,inspector_adapters,object_factories,asset_editors,validators, orimporters. Keep manifestversionat1. A contribution may have only one owner within its contribution kind, and the same descriptor may not repeat an ID. Code-declared command and asset-editor owners must match the manifest owner. Do not populatetemplate_catalogs: ownership validation rejects those claims untilregistered_editor_contributionsgains a template-catalog registration source. -
If the contribution is a tool used by a workspace, add the same tool ID to the applicable
dockregion inassets/editor/workspaces.json. The available regions areroot,scene_tree,inspector,bottom_panel, andnetwork_stack; array order is tab order inside that region. Workspace tool IDs must resolve throughEditorToolRegistry, must be owned by an extension, and may appear in only one region per preset. -
Exercise the runtime load path. The editor first reads
editor/extensions.jsonthrough the configured asset roots. A missing, unreadable, undecodable, unsupported, or invalid disk manifest falls back to the compile-time shipped manifest and recordsEditorExtensionRegistry::load_warning(). The shipped manifest is the last known-good contract; if that embedded copy is invalid, startup panics instead of running with ambiguous ownership. -
Run the focused registry checks, followed by the documentation check:
cargo test -p aether editor::extensions::tests cargo test -p aether editor::tool_registry::tests cargo test -p aether editor::importer_registry::tests cargo test -p aether editor::asset_editor_registry::tests cargo test -p aether editor::inspector_adapter::tests cargo run -p xtask -- docs-check --root .
Verify The Ownership Graph
Confirm that the implemented registry contains the contribution and that the extension manifest names the same ID. The shipped contract test checks every registered tool, command, importer, asset editor, inspector adapter, and validator has an extension owner, and checks every workspace tool has both a registered tool and an owner. It also rejects duplicate extension IDs, duplicate IDs within a contribution group, cross-extension collisions within the same contribution kind, and code/manifest owner disagreement for commands and asset editors.
For a new contribution kind, add its native registrations to registered_editor_contributions before allowing manifest claims, then add focused implementation-to-manifest coverage. A new manifest field alone is not an extension point.
Expected Outputs
- One implemented native contribution with a stable, namespaced ID.
- Exactly one extension owner for that ID and contribution kind in
assets/editor/extensions.json. - A tool-to-dock compatibility route when the contribution is a dock surface.
- Workspace membership only where the tool should be opened by a preset.
- Passing uniqueness, ownership, routing, and docs checks.
- No load warning when the disk manifest is valid; a usable shipped registry plus a diagnostic warning when disk loading fails.
- A reversible enable/disable lifecycle for optional extensions, with required domains protected and local overrides persisted independently of project assets.
- For writable inspector adapters, one snapshot value for every declared property and a typed command route for every non-read-only property. Complex domain workbenches may remain below the generic property grid, but must not duplicate its scalar controls.
- For asset editors, one deterministic highest-priority route whose owning extension is enabled; disabling that extension removes the route without adding UI-specific conditions.
- For validators, a deterministic enabled-ID selection passed to the background authored-workspace build; changing the selection invalidates the cached diagnostics projection even when authored files did not change.
Common Mistakes
- Adding an ID to
extensions.jsonand assuming that it registers behavior. The manifest owns metadata; the native registry owns execution. - Reusing an importer or tool ID under two extensions. Manifest validation rejects cross-domain ownership collisions before registration.
- Renaming an ID when only the label changed. Stable IDs may already be referenced by workspace presets, commands, preferences, or persisted compatibility data.
- Adding a dock descriptor without extending
compatibility_tool_id, or adding a workspace tool that cannot resolve to anEditorDockTab. - Calling a legacy
EditorDockTabdraw, chrome, status, or demand accessor from a runtime host. Resolve the registered tool first so disabled or missing contributions remain recoverable. - Registering a broad importer at a priority that shadows a path-specific importer, especially for shared extensions such as
.tmx. - Adding another
ProjectAssetKind/path match inside Project Files or the Asset Inspector instead of registering an asset editor descriptor. - Treating successful manifest parsing as complete integration for commands, templates, or validators without testing their implementing registry.
- Showing or dispatching a contribution without consulting its owning extension's lifecycle state.
- Marking an inspector property editable without adding its typed route, or rendering another object-kind switch for scalar fields instead of extending the adapter snapshot and property editor.
Recovery Paths
If startup reports that a contribution is owned twice, search all extension groups for the exact ID and leave one owner. If a stable ID error appears, remove uppercase letters, whitespace, slashes, or other unsupported characters; for tools, also restore a dotted namespace.
If a tool is absent from a workspace, verify the chain in order: the dock descriptor exists, compatibility_tool_id returns the manifest ID, the runtime registry resolves the tab, the owning extension lists it, and the workspace preset lists it. If a saved workspace contains a tool whose contribution is unavailable, the editor preserves the layout and shows a recovery surface; restore the owning extension or reset the workspace. If an importer chooses the wrong loader, call or test importer_for_path with the normalized res://-relative path, then compare priority, prefix, and extension matching.
If load_warning() is set, fix the disk asset path or JSON and restart so the process-wide lazy registry reloads. The current process keeps the shipped fallback once initialized. If the embedded manifest itself fails a test, repair assets/editor/extensions.json before rebuilding; bypassing validation would make ownership nondeterministic.