# The code_editor architecture tour

`example/code_editor/` is a Flutter capstone: a modular code editor where every UI element beyond the text area and tab bar is contributed by plugins. The shell knows zero specific plugins. It emits collection events, reads the descriptors plugins append, resolves widget factories from the registry, and renders.

This is the longest-running example in the repo and the most architecturally complete. If [the villain_lair examples](https://plugin-kit.saad-ardati.dev/examples/villain-lair/) teach the runtime concept-by-concept, code_editor teaches what those concepts look like assembled into a working app.

## Run it

Try the live web build at [plugin-kit.saad-ardati.dev/code-editor](https://plugin-kit.saad-ardati.dev/code-editor), or run it locally:

```bash
cd example/code_editor
flutter pub get
flutter run
```

The app opens with a SQL document, a left panel for git, a bottom panel for the terminal and runner, and an AI assist panel on the right. Every one of those is a separate plugin.

## The architecture in one sentence

The shell is a `StatefulWidget` that holds a `PluginRuntime`, listens for session-bus events, and renders the descriptors plugins append to mutable collection events. Plugins contribute *what* should appear (descriptors) and *how to render it* (widget factories), separately. The shell stitches both together at build time.

```
   shell                   plugins
   ─────                   ───────
   emits CollectPanels  ─► append PanelDescriptor(id, title, position)
                         ─►  ...

   resolves PanelWidgetFactory in 'panel' namespace
                         ◄─ registers PanelWidgetFactory implementation
                         ◄─  ...

   renders panel chrome around factory.build(context)
```

The two halves are deliberately split. Descriptors are pure Dart (just data). Widget factories are Flutter (`PanelWidgetFactory.build` returns a `Widget`). A plugin that wants to live in a non-Flutter package can declare descriptors there and register the factory from a Flutter-side adapter.

## The shell

Two files do most of the architectural work.

**`lib/app/contributions.dart`** declares the pure Dart side: namespaces (`ServiceSlots.panel`), descriptors (`PanelDescriptor`, `ToolbarActionDescriptor`, etc.), and the collection events the shell emits (`CollectPanels`, `CollectToolbarActions`, etc.). No Flutter imports.

**`lib/app/factories.dart`** declares the Flutter side: abstract widget factories (`PanelWidgetFactory`, `ToolbarActionWidgetFactory`). Plugins implement these and register them in the registry under the matching namespace.

**`lib/app/editor_app.dart`** is the shell `StatefulWidget`. It boots the runtime, opens a session, subscribes to session-bus events for refresh, and on every `setState` re-emits the collection events to discover what plugins want to contribute now.

## How a plugin contributes UI

Take the terminal plugin (`lib/app/plugins/terminal_plugin.dart`):

1. **Append a descriptor** when the shell asks. The plugin subscribes to `CollectPanels` in its `attach` and pushes a `PanelDescriptor(id: 'terminal', title: 'Terminal', position: PanelPosition.bottom)`.
2. **Register a factory** in `register`, namespaced under `panel`:
   ```dart
   registry.registerSingleton<PanelWidgetFactory>(
     ServiceSlots.panel('terminal'),     // ServiceId('panel.terminal')
     TerminalPanelFactory(),
   );
   ```
3. **The factory implements `PanelWidgetFactory.build(BuildContext)`** and returns the actual terminal widget.

When the shell renders, it walks the descriptors, looks up `PanelWidgetFactory` for each by `(panel, descriptor.id)`, and lets the factory build the body. The shell paints the chrome (collapse/expand, title bar) around it. The plugin owns everything else inside.

Disable the terminal plugin via `RuntimeSettings`, the shell re-emits `CollectPanels` on the next reconciliation, the terminal descriptor is gone, and the panel disappears. The shell never knew which plugin was contributing.

## Shell plugins

The five shell plugins all follow the descriptor-plus-factory pattern. They live in `lib/app/plugins/`:

| Plugin | Contributes |
|---|---|
| `ai_assist_plugin` | A right-side AI panel with a fake chat surface. |
| `git_plugin` | A left-side panel showing branch state and changed files. |
| `minimap_plugin` | A right-edge mini-map of the document. |
| `runner_plugin` | A toolbar action that runs the active document. |
| `terminal_plugin` | A bottom panel with a terminal interface. |

Each one is a small, self-contained example of the contribution pattern. Read any single file to see how a plugin participates in the shell without the shell knowing it exists.

## Domain plugins (the formatter pipeline)

The shell plugins handle UI. The domain plugins in `lib/plugins/` do real work: language support, debugging, formatting, linting.

The most architecturally interesting one is `formatter_pipeline`. It is split into a generic base and two language-specific subclasses, demonstrating priority cascades and plugin dependencies in one feature.

**Base pipeline** (`formatter_pipeline.dart`) registers low-level hooks on `FormatDocumentEvent` at priority 0 (whitespace trimming, indent normalization). It is the foundation everything else builds on.

**Language-specific plugins** (`dart_language`, `sql_language`) declare `formatter_pipeline` as a `dependencies` entry. They register additional hooks at higher priorities (10, 20, 50) for their language's idiomatic formatting. The runtime guarantees that if `formatter_pipeline` is disabled, both language plugins disable too, transitively.

Toggle the SQL plugin off in the dialog and:

1. The shell's `updateSessionSettings` runs.
2. The runtime detects that `sql_language` is now disabled.
3. `sql_language.detach` runs, cancelling its formatter hooks.
4. The shell re-renders. The SQL panel still works (text is still there), but invoking format leaves the SQL-specific transforms unapplied.

Toggle `formatter_pipeline` itself off and dependency cascade kicks in: both `dart_language` and `sql_language` get disabled too. The shell's chip list reflects all three becoming inactive.

## Settings dialog wired in

The shell mounts [Plugin Kit Dialog](https://plugin-kit.saad-ardati.dev/guides/plugin-kit-dialog/) as the customization UI. The user opens it, toggles a plugin, and saves. The shell's `onSave` callback calls `runtime.updateSettings(...)` against the dialog's returned `RuntimeSettings`. The runtime reconciles. The shell re-emits collection events. The UI converges on the new state without a restart.

The example uses Plugin Kit on itself: every primitive the docs describe is visible in this one app, working end to end.

## What to take from it

Three patterns that scale to non-editor apps:

1. **Descriptors plus factories.** Splitting "what to render" from "how to render" lets your plugin layer stay portable. The descriptors are pure Dart; the factories are framework-specific.
2. **Collection events for contributions.** A mutable collection event the shell emits is the simplest way to let plugins contribute zero-or-more items without registering each individually.
3. **Priority cascades for layered features.** A base plugin registers low-priority hooks; language- or feature-specific plugins layer higher-priority hooks on top through the registry. Dependencies make the cascade safe to disable.

## Where to next

[Flutter Integration](https://plugin-kit.saad-ardati.dev/guides/flutter-integration/)
  [Migrating a Flutter App](https://plugin-kit.saad-ardati.dev/guides/migrating-flutter-app/)
  [Plugin Kit Dialog](https://plugin-kit.saad-ardati.dev/guides/plugin-kit-dialog/)
  [Try the live demo](https://plugin-kit.saad-ardati.dev/code-editor)
  [Read the runnable code](https://github.com/SaadArdati/plugin_kit/tree/main/example/code_editor)