Skip to main content

lattice_host/
editor_boot.rs

1//! Phase 5.7.B.1: `Editor::boot` -- the renderer-agnostic
2//! editor boot routine.
3//!
4//! Moved out of `lattice-ui-tui::app::boot::App::new` so the
5//! TUI peer and the future GPUI peer can produce a fully-
6//! constructed [`Editor`] from a [`Document`] through the same
7//! entry point. Each peer's `App::new` wrapper then:
8//!
9//! 1. calls [`Editor::boot`] to build the renderer-neutral state,
10//! 2. wraps the result alongside its renderer-specific caches
11//!    (`theme`, `pane_render_registry`, ...),
12//! 3. runs the post-boot derived-cache + activation helpers
13//!    (theme mirror, option cache, major mode activation,
14//!    `Event::DocumentOpened` publish, eager subsystem buffer
15//!    seeding).
16//!
17//! Three module-private helpers (`build_lsp_subsystem`,
18//! `built_in_picker_registry`, `register_mode_toggle_commands`)
19//! came along with the body and live here too -- they're called
20//! only from `Editor::boot`.
21
22use std::collections::HashMap;
23use std::sync::Arc;
24
25use arc_swap::ArcSwap;
26use lattice_completion::CompletionRegistry;
27use lattice_config::ConfigRegistry;
28use lattice_core::{BufferKind, Document};
29use lattice_grammar::CommandRegistry;
30use lattice_grammar::builtins::populate as grammar_builtins_populate;
31use lattice_lsp::{
32    ApplyEditBus, ConfigurationBus, DiagnosticsLayer, InboundApplyEdit, InboundShowMessageRequest,
33    LspLogger, LspSupervisor, LspSupervisorHandle, ShowDocumentBus, ShowMessageRequestBus,
34};
35use lattice_mode::{ModeRegistry, ServiceRegistry, SubsystemBoot};
36use lattice_picker::PickerRegistry;
37use lattice_protocol::position::Position;
38use lattice_runtime::{EventBus, MessagesRing, spawn_document};
39use lattice_snippet::SnippetRegistry;
40use lattice_syntax::{Lang, LangRegistry, Syntax, SyntaxHandle};
41
42use crate::boot_context::BootContext;
43use crate::buffer_registry::{BufferData, BufferEntry, BufferRegistry, DocumentEntry};
44use crate::buffers::{BufferFlags, BufferId};
45use crate::editor::Editor;
46use crate::pane::{PaneId, PaneState, PaneTree};
47
48/// Build a fresh LSP subsystem. Returns the supervisor handle +
49/// cloned handles to the diagnostics layer + logger so the
50/// renderer's per-frame reads can skip the supervisor lock,
51/// plus the four server-initiated channel rx ends.
52///
53/// `event_bus` is wired in pre-spawn so the supervisor task is
54/// born already knowing about it; subsequent actor spawns get
55/// their per-actor edit fan-in (via `lattice_lsp::fan_in`) for
56/// free. The explicit `runtime_handle` removes the silent-fail
57/// footgun of `Handle::try_current()` -- `Editor::boot` runs
58/// before any main loop has entered a tokio context.
59fn build_lsp_subsystem(
60    event_bus: Arc<EventBus>,
61    runtime_handle: &tokio::runtime::Handle,
62    // BC.8b/BC.8c: the configuration + show-document buses are now the generic
63    // `InboundBus` (wired via `boot.inbound` in Phase A so their `send` wakes
64    // the editor + their drain runs the mode-owned handler). Passed in
65    // pre-spawn so the supervisor fans them out to its (lazily-spawned) actors.
66    // `logger` is likewise created in Phase A (so the show-document handler can
67    // capture a clone — LspLogger is Arc-backed, clones share rings) and passed
68    // in. BC.8d/e: apply-edit + show-message-request are the host-drained
69    // `InboundBus` (wake-baked sender + host-owned receiver via
70    // `boot.inbound_raw`) — all four server-initiated buses now ride the generic
71    // primitive; no bespoke bus remains.
72    logger: LspLogger,
73    configuration_bus: ConfigurationBus,
74    show_document_bus: ShowDocumentBus,
75    apply_edit_bus: ApplyEditBus,
76    show_message_request_bus: ShowMessageRequestBus,
77) -> (LspSupervisorHandle, DiagnosticsLayer) {
78    let mut sup = LspSupervisor::new(logger.clone());
79    sup.set_configs(lattice_lsp::builtin_servers());
80    let diagnostics = sup.diagnostics().clone();
81    // BC.8d: the apply-edit bus is the generic (host-drained) `InboundBus`
82    // passed in (no bespoke `ApplyEditBus::new()`); the host owns the matching
83    // receiver, seated on the Editor + drained in `run_tick_pending`.
84    sup.set_apply_edit_bus(apply_edit_bus);
85    // BC.8b: the configuration bus is the generic `InboundBus` passed in (no
86    // bespoke `ConfigurationBus::new()` / host-drained `rx`).
87    sup.set_configuration_bus(configuration_bus);
88    // BC.8c: the show-document bus is the generic `InboundBus` passed in (no
89    // bespoke `ShowDocumentBus::new()` / host-drained `rx`).
90    sup.set_show_document_bus(show_document_bus);
91    // BC.8e: the show-message-request bus is the generic (host-drained)
92    // `InboundBus` passed in (no bespoke `ShowMessageRequestBus::new()`); the
93    // host owns the matching receiver, seated on the Editor + drained in
94    // `run_tick_pending` (the picker routing is irreducibly `&mut Editor`).
95    sup.set_show_message_request_bus(show_message_request_bus);
96    sup.set_event_bus(event_bus.clone());
97    let handle = sup.spawn(runtime_handle);
98    // M-async.5: LSP attach driver is gone; modes drive
99    // `open_buffer` directly via the supervisor handle pulled
100    // from `ctx.service::<...>()`. `event_bus` stays bound to
101    // the supervisor for the per-actor edit fan-in; the keep
102    // is intentional -- no other consumer in this function.
103    let _ = &event_bus;
104    (handle, diagnostics)
105}
106
107/// Boot-time registration of the first-party picker sources
108/// the `:picker <source>` ex-command dispatches to. Each
109/// source is registered with its `PickerSourceGenerator` impl
110/// so dispatch resolves through `gen.init()` / `gen.accept()`.
111/// Feature-crate sources (snippet today; LSP / DAP / ... later)
112/// register through their own entry points called from inside
113/// this helper -- the host wires the dependency direction so
114/// `lattice-picker` itself never has to know about feature
115/// crates.
116fn built_in_picker_registry(
117    command_registry: lattice_grammar::CommandRegistryHandle,
118    config: Arc<ConfigRegistry>,
119    keybinding_reverse: Arc<dyn lattice_completion::KeymapReverseLookup>,
120    grep_highlighter: Option<Arc<dyn lattice_picker::picker_sources::GrepPreviewHighlighter>>,
121    snippet_registry: Arc<ArcSwap<SnippetRegistry>>,
122    theme_registry: lattice_theme::ThemeRegistryHandle,
123    magit_repo: lattice_magit::picker_sources::RepoLens,
124) -> PickerRegistry {
125    let mut reg = PickerRegistry::new();
126    // MG.54: magit's revision picker reads `magit.revision-preview` at
127    // preview time, so it needs its own handle on the registry.
128    let magit_config = Arc::clone(&config);
129    for generator in lattice_picker::picker_sources::first_party_generators(
130        command_registry,
131        config,
132        keybinding_reverse,
133        grep_highlighter,
134    ) {
135        reg.register_generator(generator);
136    }
137    lattice_snippet::picker_sources::register(&mut reg, snippet_registry);
138    lattice_magit::picker_sources::register(&mut reg, Some(magit_config), magit_repo);
139    // T.12a: the live-preview theme picker (`:colorscheme` no-arg).
140    // Holds a clone of the host's `ThemeRegistryHandle` so it can
141    // enumerate registered theme names + drive live preview.
142    reg.register_generator(Arc::new(crate::host_generators::ThemePickerSource::new(
143        theme_registry,
144    )));
145    reg
146}
147
148/// M.5.1: register a `:<mode-name>` toggle ex-command for every
149/// mode in `mode_registry`. The command id is the mode name (no
150/// `ex:` prefix; the ex-command resolver tries direct registry-
151/// name lookup before alias expansion, so `:lsp-mode` resolves
152/// directly).
153///
154/// Toggle apply-fn returns
155/// [`lattice_grammar::Effect::ToggleMode { mode_name }`]; each
156/// renderer's effect dispatcher routes that to the App-level
157/// `toggle_mode_by_name`.
158fn register_mode_toggle_commands(cmd_registry: &mut CommandRegistry, mode_registry: &ModeRegistry) {
159    let mut names: Vec<String> = mode_registry
160        .iter_meta()
161        .map(|(id, _kind)| id.to_string())
162        .collect();
163    // Sort for deterministic registration order (HashMap iteration
164    // is hash-randomized; deterministic boot keeps `:describe-*`
165    // and tests stable).
166    names.sort();
167    for name in names {
168        // The toggle spec is shared with the plugin modes-seam drain (loader
169        // `drain_mode`) so native + plugin modes get an IDENTICAL `:<mode>`
170        // toggle command. Native modes register under Builtin provenance here;
171        // plugin modes register under `Plugin(id)` so unload reverses them.
172        cmd_registry.register_ex_command(
173            &name,
174            lattice_grammar::registry::MODE_TOGGLE_COMMAND_DOC,
175            lattice_grammar::registry::mode_toggle_ex_command_spec(&name),
176        );
177    }
178}
179
180impl Editor {
181    /// Build a fully-wired renderer-neutral [`Editor`] from an
182    /// initial [`Document`]. Phase 5.7.B.1 extraction of the
183    /// boot body from `lattice-ui-tui::app::boot::App::new`.
184    ///
185    /// What happens here: event bus + LSP subsystem (with all
186    /// four server-initiated channels) + every per-feature LSP
187    /// rx subscription; grammar registry populated with builtins +
188    /// ex-commands + auto-generated `:<mode-name>` toggles;
189    /// mode registry with foundation / syntax / lsp-log /
190    /// lsp-completion / oil / file-tree / snippet / buffer-kind
191    /// modes; completion registry with builtins + the seven
192    /// host-side completion generators; config registry +
193    /// linkme init + LSP-logger seeding; tree-sitter
194    /// `LangRegistry` + initial seeded `SyntaxHandle`;
195    /// `spawn_document`; buffer registry seeded with typed
196    /// buffer-locals.
197    ///
198    /// What does NOT happen here (each renderer's `App::new`
199    /// runs them afterwards): renderer-specific theme cache
200    /// rebuild; `rebuild_option_cache` /
201    /// `sync_host_theme_from_config` (host fns but their
202    /// renderer-signal fan-out is per-renderer);
203    /// `activate_major_for_buffer_kind` (returns signals);
204    /// `publish_document_opened_for_active` /
205    /// `ensure_named_synthetic_document` /
206    /// `ensure_messages_buffer` (App-side helpers).
207    pub fn boot(document: Document) -> Self {
208        // §5.10 event bus. Built before `build_lsp_subsystem`
209        // because the supervisor wires its per-actor edit fan-
210        // in (lattice_lsp::fan_in) at spawn time using this
211        // bus, and the post-spawn handle does not expose
212        // `set_event_bus`.
213        // Slice C (`:files` warm-up, real launches only): the opened file's
214        // path, captured before `document` is consumed below so the tail can
215        // pre-walk its project. Gated out of test builds — `Editor::boot` runs
216        // in thousands of unit tests, and each must not spawn a project walk.
217        #[cfg(not(test))]
218        let boot_doc_path = document.path().map(|p| p.to_path_buf());
219        let event_bus = Arc::new(EventBus::new());
220        // Canonical LSP runtime handle: a process-wide singleton
221        // lazily initialised on first call so every later caller
222        // (per-feature `spawn_on_lsp_runtime` for hover /
223        // definition / etc., the attach driver, every test that
224        // exercises the LSP write path) reuses the same instance.
225        let runtime_handle = lattice_runtime::runtime::lsp_runtime().handle().clone();
226
227        // ── Phase A (boot-composition BC.3a): generic primitives ──────────
228        // The host's generic-primitive surface, built up front and bundled
229        // into a `BootContext`. Each subsystem's command / mode / service
230        // registration runs through `boot` (BC.3a); BC.3b+ collapse each
231        // subsystem's scattered wiring into one `install(boot)` call. These
232        // bindings used to be created mid-boot (`async_landed` ~706,
233        // `tick_callbacks` ~1153, `render_state` ~821, `buffers` ~767,
234        // `buffer_store` / `diag_query` inside the `services:` block);
235        // hoisting them here is a mechanical `let`-reorder that PRESERVES Arc
236        // identity (the workers + Editor fields + service registrations below
237        // still clone these exact Arcs — never re-`Arc::new`).
238        //
239        // Slice B.1: `async_landed` seats the reparse-worker `on_publish`
240        // wake + the `Editor::async_landed` field; the actor loop awaits it.
241        let async_landed: Arc<tokio::sync::Notify> = Arc::default();
242        // IDE-protocol I1.1: the per-tick drain-closure registry (one Arc for
243        // the editor's lifetime; modes add drains from `on_activate`).
244        let tick_callbacks: lattice_mode::TickCallbackRegistryHandle =
245            Arc::new(lattice_mode::TickCallbackRegistry::new());
246        // Phase 5.8.AF.5 / Slice 3a: the published render-state cell. The
247        // overlay / cells / virtual-rows workers (spawned below) + every
248        // `publish_render_state` share this exact Arc identity.
249        let render_state_arc: Arc<ArcSwap<crate::render_state::RenderState>> = Arc::new(
250            ArcSwap::from_pointee(crate::render_state::RenderState::default()),
251        );
252        // The buffer registry; created empty here and seeded with the initial
253        // document at the spawn site below (`BufferRegistry` is `Clone` via an
254        // inner Arc, so the handle handed to `boot` observes that seeding).
255        let buffers = BufferRegistry::new();
256        // BC.3a read-tool handles derived from the Phase-A cells: the generic
257        // buffer-store (over `buffers`) + the diagnostics query (over the
258        // render-state cell). Registered as services below + handed to the
259        // claude-code read tools; `boot` holds clones for the BC.3b migration.
260        let buffer_store_handle = {
261            let store: Arc<dyn lattice_mode::BufferStore> = Arc::new(buffers.clone());
262            lattice_mode::BufferStoreHandle::new(store)
263        };
264        // WK.3: the idle-gate registry — subsystem-armed deadlines. Built
265        // here in Phase A (like `tick_callbacks`) because both `boot` and the
266        // `Editor` literal need the same `Arc`: subsystems register gates
267        // through `boot`, and the actor loop reads `earliest()` off the
268        // editor to target its pinned sleep.
269        let idle_gates: lattice_mode::idle_gate::IdleGateRegistryHandle =
270            Arc::new(lattice_mode::idle_gate::IdleGateRegistry::new());
271        let diag_query: lattice_lsp::modes::DiagnosticsQueryHandle = Arc::new(
272            crate::diagnostics_query::HostDiagnosticsQuery::new(render_state_arc.clone()),
273        );
274        // BC.3a (decision 2-b): `boot` owns the three registries during the
275        // build phase. Every mode / command / service registration below runs
276        // through `boot.modes_mut()` / `boot.commands_mut()` /
277        // `boot.register_service()`; the `freeze_*` calls hand back the shared
278        // `Arc`s the `Editor` literal seats. The registries are passed empty.
279        let mut boot = BootContext::new(
280            event_bus.clone(),
281            tick_callbacks.clone(),
282            async_landed.clone(),
283            runtime_handle.clone(),
284            buffer_store_handle.clone(),
285            idle_gates.clone(),
286            CommandRegistry::new(),
287            ModeRegistry::new(),
288            ServiceRegistry::new(),
289        );
290        // BC.3b: register the generic diagnostics-query service in Phase A so a
291        // subsystem `install(boot)` can reach it via `boot.service::<DiagnosticsQueryHandle>()`
292        // (the `SubsystemBoot` surface can't name the lattice-lsp type). Read by
293        // claude-code's read tools; registered once here, not in the late block.
294        boot.register_service::<lattice_lsp::modes::DiagnosticsQueryHandle>(diag_query.clone());
295
296        // Typed-options registry (DESIGN.md §5.12). Single source of truth
297        // for every option's *current value*: each `Option<T>` owns a
298        // wait-free `ArcSwap<T>` cell that `:set` parses into, hot-path
299        // readers load from, and the (future) customize buffer view edits
300        // through.
301        //
302        // DB.5 hoist (2026-07-03): built + registered here in Phase A —
303        // moved up from ~line 700 (right after `event_bus`, its only
304        // dependency), the same class of hoist BC.3a already did for
305        // `async_landed` / `tick_callbacks` / `buffers`. `ServiceRegistry`
306        // only reflects what's registered by the time a reader calls
307        // `boot.service::<T>()`; a subsystem's `install(&mut boot)` runs in
308        // the Phase-B list further down, so `lattice_dashboard::install`
309        // (DB.5's startup-trigger subscription, which reads
310        // `dashboard.enabled` via `boot.service::<Arc<ConfigRegistry>>()`)
311        // could never observe a registration added later in the same boot
312        // call under the old ordering. Mechanical reorder; every later
313        // reader still clones this exact `Arc` — never re-`Arc::new`.
314        let config = Arc::new(ConfigRegistry::new());
315        let bus_for_publisher = event_bus.clone();
316        config.set_event_publisher(Arc::new(move |event| {
317            bus_for_publisher.publish(event);
318        }));
319        // M.2.0c: every option (core + renderer-specific) self-
320        // registers via the proc-macro-emitted `register_fn`
321        // thunks aggregated in `OPTION_DECLS`. One
322        // `init_from_linkme()` call boots them all; idempotent
323        // if called again.
324        config.init_from_linkme();
325        // MH.A3 (2026-06-19): expose the ConfigRegistry so extension-crate
326        // code (`create_multibuffer_view`) can read global option defaults
327        // — e.g. `ui.nerd_fonts` for the rich excerpt-header icon palette —
328        // without depending on `lattice-host`'s typed option decls. Read by
329        // name (`get_bool_by_name`). Same `Arc<X>` register/lookup pair per
330        // the ServiceRegistry Arc/TypeId rule.
331        boot.register_service::<Arc<ConfigRegistry>>(config.clone());
332
333        // BC.8b: the merged `lsp.*` config tree is shared (`Arc<ArcSwap>`) so the
334        // mode-owned configuration inbound handler reads the *current* tree (the
335        // host re-`store`s it on reload). Built empty here; populated by the
336        // config loader post-construction (`store`), exactly as the old
337        // `toml::Table` field was assigned. The SAME `Arc` is seated in the
338        // `Editor.lsp_config_tree` field below (NOT `Editor::default()`'s fresh
339        // one), so the handler and the editor observe one tree.
340        let lsp_config_tree = std::sync::Arc::new(ArcSwap::from_pointee(toml::Table::new()));
341        // BC.8b: wire the `workspace/configuration` bus as the generic inbound
342        // primitive — `send` wakes the editor; the per-tick drain runs the
343        // mode-owned `make_handler` (a pure read → reply, no Effect). Done in
344        // Phase A (pre-spawn) because the supervisor fans the bus out to its
345        // actors; the drain token rides `into_registrations` onto the Editor.
346        let lsp_configuration_bus = boot.inbound::<lattice_lsp::InboundConfigurationRequest, _>(
347            lattice_lsp::configuration::make_handler(lsp_config_tree.clone()),
348        );
349        // BC.8c: create the LSP logger in Phase A so the mode-owned
350        // show-document handler can capture a clone (LspLogger is Arc-backed —
351        // every clone shares the same log rings). Wire the show-document bus as
352        // the generic inbound primitive: `send` wakes the editor; the per-tick
353        // drain runs `make_handler`, which maps each request to a HOST-APPLIED
354        // open effect (OpenExternalUri / OpenBufferAtColumn) + an optimistic
355        // reply. Host-applied because this bus drains off-keystroke, where
356        // peer-applied open effects are not forwarded.
357        let lsp_logger = lattice_lsp::LspLogger::with_defaults();
358        let lsp_show_document_bus = boot.inbound::<lattice_lsp::InboundShowDocument, _>(
359            lattice_lsp::show_document::make_handler(lsp_logger.clone()),
360        );
361        // BC.8d: the apply-edit bus is the host-drained generic `InboundBus`
362        // (wake-baked sender + raw receiver). The host seats the receiver on the
363        // Editor (`pending_apply_edit_rx`) and drains it in `run_tick_pending`
364        // (the apply is irreducibly `&mut Editor` + `lsp_types`, so it can't be
365        // a mode-owned handler); `send` wakes the editor so the edit lands
366        // off-keystroke instead of on the next keypress.
367        let (lsp_apply_edit_bus, lsp_apply_edit_rx) = boot.inbound_raw::<InboundApplyEdit>();
368        // BC.8e: the show-message-request bus is likewise the host-drained
369        // generic `InboundBus`. The request is a deferred user choice routed
370        // through the host picker primitive, so the host seats the receiver on
371        // the Editor (`pending_show_message_request_rx`) and drains it in
372        // `run_tick_pending`; `send` wakes the editor so the picker is raised
373        // off-keystroke.
374        let (lsp_show_message_request_bus, lsp_show_message_request_rx) =
375            boot.inbound_raw::<InboundShowMessageRequest>();
376        // I4 (Claude Code IDE peer, `openDiff`): the programmatic-diff bus is the
377        // host-drained generic `InboundBus`, same shape as BC.8d apply-edit. The
378        // host seats the receiver on the Editor (`pending_programmatic_diff_rx`)
379        // and drains it in `run_tick_pending` (`open_programmatic_diff` is
380        // irreducibly `&mut Editor` + lattice-diff types, so it can't be a
381        // mode-owned `Effect` handler). The sender is registered as a Phase-A
382        // service so the IDE peer's `install(boot)` reads it via
383        // `boot.service::<ProgrammaticDiffBus>()` — keeping the host free of any
384        // IDE-peer reference (the bus is a generic diff-subsystem type). This is
385        // diff-subsystem residue (alongside the `DiffSubsystem` bind below), not
386        // an LSP channel.
387        let (programmatic_diff_bus, programmatic_diff_rx) =
388            boot.inbound_raw::<lattice_diff::ProgrammaticDiffRequest>();
389        boot.register_service::<lattice_diff::ProgrammaticDiffBus>(programmatic_diff_bus);
390        let (lsp, lsp_diagnostics) = build_lsp_subsystem(
391            event_bus.clone(),
392            &runtime_handle,
393            lsp_logger.clone(),
394            lsp_configuration_bus,
395            lsp_show_document_bus,
396            lsp_apply_edit_bus,
397            lsp_show_message_request_bus,
398        );
399        // BC.8a: register the supervisor handle as a Phase-A service HERE (moved
400        // up from the late service block) so `lattice_lsp::install` below can
401        // read it via `boot.service::<LspSupervisorHandle>()` to register
402        // `lsp-completion-mode`. The handle is host-created (the supervisor +
403        // its four server-initiated buses live in `build_lsp_subsystem`, which
404        // produces Editor fields — the diff `DiffSubsystem`-bind residue), so it
405        // is registered host-side; `install` only reads it. Same `Arc` identity.
406        boot.register_service(lsp.clone());
407
408        // BC.3b: the Claude Code IDE peer is no longer hand-wired here. Its
409        // server spawn, ex-commands, mode, service handle, and read/write tools
410        // all install through one Phase-B call below
411        // (`lattice_claude_code::install(&mut boot)`), against the generic
412        // `SubsystemBoot` surface — the mode-ownership acid test.
413
414        let builtins = grammar_builtins_populate(boot.commands_mut());
415        // Register the built-in ex-commands as peers of motions /
416        // operators / text objects (DESIGN.md §5.2.1). The returned
417        // ids aren't held in App state today -- the parser front-
418        // end looks them up by name -- but registering them
419        // populates the registry so `:`-line parsing can route to
420        // them.
421        let _ex_builtins = lattice_grammar::ex_commands::populate(boot.commands_mut());
422
423        // SU.3a: register surround operators in the shared CommandRegistry
424        // so the surround-mode keymap can resolve its chain-form bindings.
425        let surround_operators =
426            lattice_mode::modes::surround::register_surround_operators(boot.commands_mut());
427
428        // CSM.5: shared snippet-registry handle. Built before the
429        // mode registry so `register_snippet_modes` can capture a
430        // clone of the outer Arc -- the same outer Arc the Editor
431        // field below holds. `:reload-snippets` updates the inner
432        // via `.store()`; the mode + source see the fresh data on
433        // the next produce().
434        //
435        // Constructed empty; the embedded built-in packs + user
436        // packs load at the production startup seam via
437        // `Editor::load_snippets_at_startup` (called from the TUI /
438        // GPUI entry points alongside `load_persistent_config`), NOT
439        // here. Keeping content-loading out of the constructor means
440        // test `App`s start with an empty registry — completion
441        // tests stay isolated from the built-in snippet set unless
442        // they opt in via `:reload-snippets`.
443        let snippet_registry_handle: Arc<ArcSwap<SnippetRegistry>> =
444            Arc::new(ArcSwap::from_pointee(SnippetRegistry::new()));
445
446        // M.5.1 (mode-architecture §9.6.1): build the mode
447        // registry first so we can iterate it and register a
448        // `:<mode-name>` toggle ex-command per mode. The mode
449        // registry is then wrapped in `Arc`.
450        // SN.3b: captured out of the registry-build block so boot
451        // can fold `snippet.activation` / `snippet.languages` into
452        // it (below) and `Editor` can hold the clone the cascade
453        // re-folds.
454
455        // BC.3a: mode registration runs through `boot.modes_mut()` (the
456        // registration seam). Order preserved verbatim from the prior
457        // `let mr = { … }` block.
458        lattice_mode::register_foundation_modes(boot.modes_mut());
459        // SU.3a: register surround-mode with the operator handles it owns.
460        lattice_mode::modes::surround::register_surround_modes(
461            boot.modes_mut(),
462            surround_operators.clone(),
463        );
464        lattice_syntax::register_language_modes(boot.modes_mut());
465        // BC.8a: the LSP modes (`register_lsp_log_modes` + the
466        // supervisor-handle-bound `lsp-completion-mode`) moved into
467        // `lattice_lsp::install(boot)` (Phase-B list below). The completion mode
468        // reads the supervisor handle via `boot.service::<LspSupervisorHandle>()`
469        // (registered in Phase A above), so no host-side handle threading.
470        // LM.3: one install() registers the listing modes (oil / file-tree /
471        // directory-listing) AND the oil chord `action:*` commands, so
472        // oil-mode's keymap + action_handlers wire up through the generic
473        // K.2.4 + register_mode_action_handlers walks — no host-side oil
474        // dispatch code.
475        lattice_listing::install(&mut boot);
476        let snippet_activation_policy = lattice_snippet::register_snippet_modes(
477            boot.modes_mut(),
478            snippet_registry_handle.clone(),
479        );
480        // BC.4: terminal-mode registration moved into
481        // `lattice_terminal::install` (Phase-B list below).
482        crate::modes::register_buffer_kind_modes(boot.modes_mut());
483        // PI.2 (preview isolation): `preview-mode` — the read-only minor
484        // `mount_preview` activates on a previewed buffer's own stack.
485        // Host-owned (no feature crate), registered alongside the other
486        // host modes.
487        boot.modes_mut()
488            .register(crate::preview::PreviewMode)
489            .expect("preview-mode must register without conflict");
490        // MB.1 (rich minibuffer): `command-line-mode` — the major mode on
491        // the synthetic `*command-line*` buffer. Host-owned; its Insert-
492        // layer keymap (submit / cancel / history / completion chords)
493        // resolves through `translate_mode_keymaps` at boot.
494        boot.modes_mut()
495            .register(crate::command_line_mode::CommandLineMode)
496            .expect("command-line-mode must register without conflict");
497        // MB.2: `command-line-expand-mode` — the tier-2 expanded band
498        // major mode (same buffer, full-modal). Activated on expand,
499        // deactivated on collapse. Owns the expanded band's option
500        // overrides and keymap surface independently of tier 1.
501        boot.modes_mut()
502            .register(crate::command_line_expand_mode::CommandLineExpandMode)
503            .expect("command-line-expand-mode must register without conflict");
504        // MB.5a (rich minibuffer): `search-line-mode` — the major mode on
505        // the synthetic `*search-line*` buffer. Host-owned; its Insert-
506        // layer keymap (submit / cancel chords) resolves through
507        // `translate_mode_keymaps` at boot.
508        boot.modes_mut()
509            .register(crate::search_line_mode::SearchLineMode)
510            .expect("search-line-mode must register without conflict");
511        // `prompt-line-mode` — the generic one-line minibuffer text
512        // prompt backing `Effect::OpenPrompt`. Host-owned; unlike
513        // command-line/search-line it has no purpose-specific keymap
514        // beyond submit/cancel — the caller supplies the label,
515        // initial text, and submit-target per invocation.
516        boot.modes_mut()
517            .register(crate::prompt_line_mode::PromptLineMode)
518            .expect("prompt-line-mode must register without conflict");
519        // BC.7 (2026-06-24): `multibuffer-mode` (+ its `DocumentClosed`
520        // cleanup subscriber), `narrow-mode`, and the project-search
521        // provider mode moved into `lattice_multibuffer::install(boot)`
522        // (Phase-B install list below), alongside its commands + services +
523        // the `MultibufferExcerptsReady` wake. The registry handle is now
524        // crate-owned (created inside `install`); the host reads it back via
525        // `services.get::<MultibufferRegistryHandle>()` in `resolve_narrow_target`.
526        // BC.6/DX.7: `diff-mode` registration moved into
527        // `lattice_diff::install(boot)` (Phase-B install list below),
528        // alongside terminal + claude-code. K.1.c still gates the
529        // `do`/`dp` chords on per-buffer diff participation.
530        // BC.5: `emacs-keys-mode` is now a `lattice-mode` builtin — registered
531        // with the foundation set by `register_foundation_modes` above, not
532        // here. The host keeps only its keymap-layer push (keymap block below).
533        crate::tutor::register_tutor_modes(boot.modes_mut());
534
535        // ── BC.3b: Phase-B subsystem install list ──────────────────────────
536        // One line per subsystem; each `install(boot)` does ALL of its own
537        // wiring (modes, commands, services, the off-keystroke inbound bus,
538        // event wakes) against the generic `SubsystemBoot` surface — zero host
539        // internals (no `Editor::` method, no host `Action`/`Effect` variant).
540        // Placed after the inline mode block + before the mode freeze, so an
541        // installed mode is present when `register_mode_toggle_commands`
542        // enumerates the registry, and while both registries are still open.
543        // As each remaining subsystem migrates (terminal → emacs-keys → diff →
544        // multibuffer → LSP, newest→oldest) its inline wiring collapses into an
545        // `install` here. **BC.final (2026-06-25):** all subsystems are migrated;
546        // this list is the single Phase-B touch-point (the acid test — a new
547        // subsystem adds ONE `install` line, guarded by the BC.2 pins). NOTE:
548        // `editor_boot` is THREE parts, not two — Phase-A primitives, the inline
549        // host-native *builtins* (grammar / ex-commands / foundation+language+
550        // oil+file-tree+snippet+tutor+buffer-kind modes / host actions, via
551        // `boot.{commands,modes}_mut()`), and this Phase-B `install` list. The
552        // builtins are not subsystems and register inline by design — the
553        // earlier "two-list, `*_mut` removed" goal was falsified on inspection.
554        //
555        // AI (AI-1b / AG-4): the single `lattice-ai` install wires BOTH agent
556        // transports (the AG-4 fold collapsed the former `lattice-claude-code`
557        // crate into `lattice_ai::mcp`):
558        //   - ACP client: AiLogger + supervisor + AiLogMode + :opencode /
559        //     :ai-prompt / :ai-stop + AiClientHandle/AiLogger services. Agent
560        //     output streams into per-session *ai:<provider>:<index>* rings
561        //     (the :ai-log picker view is 12b).
562        //   - MCP IDE peer: server spawn + `:claude-code-*` ex-commands +
563        //     `claude-code-mode` + the `ClaudeCodeServerHandle` service + the I2
564        //     read tools (buffer-store + diagnostics via `boot.service`) + the I3
565        //     write bus (`boot.inbound`, whose drain token rides
566        //     `into_registrations` into the Editor below).
567        // AUX‑2: create VirtualRowProviderRegistry and register as a service so
568        // subsystem installs can register headerline providers.
569        let vrp: std::sync::Arc<crate::virtual_rows_worker::VirtualRowProviderRegistry> =
570            std::sync::Arc::default();
571        boot.register_service::<Arc<dyn lattice_mode::VirtualRowRegistrar>>(
572            vrp.clone() as Arc<dyn lattice_mode::VirtualRowRegistrar>
573        );
574        // PV.1 (2026-08-12): the provider-view registry. Same shape as
575        // `vrp` above and registered for the same reason — a host-created
576        // table that subsystem installs *write into*, so it must exist
577        // before the install list runs. Providers register an opener
578        // under a name (`boot.service::<ProviderViewRegistryHandle>()`);
579        // the `AppEffect::OpenProviderView` arm looks the name up and
580        // calls it with the Editor as the activator. This is what lets a
581        // provider crate ship a multibuffer view with ZERO host changes —
582        // see `lattice_mode::provider_view` + multibuffer-views.md §3.7a.
583        boot.register_service::<lattice_mode::ProviderViewRegistryHandle>(Arc::new(
584            lattice_mode::ProviderViewRegistry::new(),
585        ));
586        // Which directory is the buffer *called this* about? Providers whose
587        // buffers open by name (magit) register a source here; the synthetic
588        // creation chokepoint asks. Published in Phase B, ahead of every
589        // subsystem `install`, so no provider can be installed before the
590        // registry it registers into exists.
591        let scope_sources: lattice_mode::BufferScopeSourceRegistryHandle = Arc::new(
592            arc_swap::ArcSwap::from_pointee(lattice_mode::BufferScopeSourceRegistry::new()),
593        );
594        boot.register_service::<lattice_mode::BufferScopeSourceRegistryHandle>(scope_sources);
595        // PR.2: the project resolver — "which project does this buffer
596        // belong to", the one answer terminal / compilation / search /
597        // the file picker all root from.
598        //
599        // Registered in Phase B, ahead of every subsystem `install`, so
600        // no consumer can be installed before the service it resolves
601        // through exists. Under the exact `ProjectResolverHandle` alias
602        // per the ServiceRegistry Arc/TypeId rule — registering the
603        // concrete `Arc<MarkerResolver>` would key it under the wrong
604        // TypeId and every `get` would silently return `None`.
605        //
606        // Built with the DEFAULT markers, not the configured ones, and
607        // that ordering is forced rather than lazy: the persistent-config
608        // loader finds `.lattice/config.toml` by resolving a project
609        // root, so the resolver must exist before the config that
610        // configures it has been read. The subscription below re-points
611        // it the moment `project.root-markers` resolves to anything else
612        // — from TOML at startup or from `:set` later, the same path.
613        let project_resolver: lattice_core::ProjectResolverHandle =
614            std::sync::Arc::new(lattice_core::MarkerResolver::with_default_markers(
615                std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
616            ));
617        {
618            let resolver = project_resolver.clone();
619            let (markers_tx, mut markers_rx) = tokio::sync::mpsc::unbounded_channel();
620            event_bus.subscribe(
621                lattice_runtime::EventFilter::kind(lattice_protocol::EventKind::OptionChanged),
622                lattice_runtime::SubscriptionTarget::Channel(markers_tx),
623            );
624            boot.runtime_handle().spawn(async move {
625                while let Some(event) = markers_rx.recv().await {
626                    let lattice_protocol::Event::OptionChanged { name, new, .. } = event else {
627                        continue;
628                    };
629                    if name != "project.root-markers" {
630                        continue;
631                    }
632                    // Parse rather than carry the typed value: the event
633                    // ships the new value as a string, so this needs no
634                    // `lattice-config` value-type coupling on the async
635                    // side (the `plugin.trace-level` precedent).
636                    match <lattice_config::RootMarkers as lattice_config::OptionType>::parse(&new) {
637                        Ok(markers) => resolver.set_markers(markers.to_vec()),
638                        Err(e) => {
639                            tracing::warn!(value = %new, error = %e, "ignoring project.root-markers")
640                        }
641                    }
642                }
643            });
644        }
645        boot.register_service::<lattice_core::ProjectResolverHandle>(project_resolver);
646
647        lattice_ai::install(&mut boot);
648        // NOTIF.1a: the notification store + the inbound bus expiry
649        // rides on. Installed early because it owns no modes and no
650        // buffers — later subsystems (magit's remote ops first) look up
651        // `NotificationStoreHandle` to post.
652        lattice_notify::install(&mut boot);
653        // terminal (BC.4): `terminal-mode` (+ Normal / Insert) registration. Its
654        // `TerminalStoreHandle` service is a host-published primitive (in the
655        // service block below) and its invocation runner stays host-side (the
656        // shared invocation-runner mechanism) — see `lattice_terminal::install`.
657        lattice_terminal::install(&mut boot);
658        // diff (BC.6/DX.7): `diff-mode` registration. Two touch-points stay
659        // host-side and are NOT mode-ownership violations — the `DiffSubsystem`
660        // bind (uses the host `BufferRegistryDocumentResolver`; produces the
661        // `diff_subsystem` / `diff_subscription_guard` / `diff_forwarders`
662        // actor-loop fields below) and the `+N ~M` modeline element (its
663        // `ModelineService` is created after this list). The `do`/`dp` keymap
664        // is fully mode-owned (MO.x): `DiffMode::keymap()` + the K.2.4 pass —
665        // see `lattice_diff::install` for the full rationale.
666        lattice_diff::install(&mut boot);
667        // multibuffer (BC.7): `multibuffer-mode` + `narrow-mode` + the
668        // project-search mode, the excerpt-jump motions + `:multibuffer-*` /
669        // `:narrow` / `:widen` / `:search` ex-commands + the `zn` operator SPEC,
670        // the `MultibufferRegistryHandle` + project-search services, and the
671        // `MultibufferExcerptsReady` off-keystroke wake. The registry handle is
672        // crate-owned (no host-state dependency). Residue staying host-side
673        // (NOT mode-ownership violations): the universal `zn` operator BINDING
674        // at the `Builtin` operator-pending layer (resolved by name below —
675        // BC.7 decision A) and the `AppEffect::{Search,Narrow,MultibufferExpand}`
676        // dispatch arms (Effect-vocabulary-is-the-host-boundary) — see
677        // `lattice_multibuffer::install` for the full rationale.
678        lattice_multibuffer::install(&mut boot);
679        // CM.1: native compilation subsystem — registers
680        // `compilation-mode` (major, ReadOnly + NoFile), the
681        // `:compile`/`:recompile`/`:make` ex-commands (return
682        // `Effect::AppAction(AppEffect::CompileRun)`, applied by the
683        // host arm — creates the `*compilation*` buffer host-side +
684        // runs the `CompilationServiceHandle`),
685        // the `CompilationServiceHandle` process-lifecycle service,
686        // and the `CompilationOutputPushed` off-keystroke wake so the
687        // streaming `*compilation*` buffer repaints without a keypress.
688        lattice_compilation::install(&mut boot);
689        // DB.2: dashboard subsystem — registers `dashboard-mode` (major), the
690        // `:dashboard` ex-command (returns `Effect::OpenDashboard`, applied by
691        // `Editor::do_open_dashboard`), and the built-in `DashboardRegistry`
692        // service. See `lattice_dashboard::install` + dashboard.md §9.
693        lattice_dashboard::install(&mut boot);
694        // WK.6: which-key registers `which-key-mode` (the popup buffer's
695        // major) HERE, before the mode registry freezes below; its lifecycle
696        // is wired further down by `wire_which_key`, once the keymap and
697        // command-registry services exist. The split is boot ordering, not
698        // design — see `lattice_mode::modes::which_key::install`.
699        let which_key_grid = lattice_mode::modes::install_which_key(&mut boot);
700        // PL8.H.2: the plugin-manager view — registers `plugins-mode` (major,
701        // read-only) + the `:plugins` ex-command (returns
702        // `Effect::OpenSyntheticBuffer`, applied by `Editor::open_synthetic_buffer`).
703        // A pure provider crate: the mode resolves `PluginLoaderHandle` at
704        // activation, so this needs no ordering vs the loader install below.
705        lattice_plugin_manager::install(&mut boot);
706        // PO.4.1: the plugin boundary-trace views — registers `plugin-trace-mode`
707        // (major, read-only) + the `:plugin-trace` ex-command (returns
708        // `Effect::OpenSyntheticBuffer`). A pure provider crate: the mode resolves
709        // `PluginTracerHandle` at activation (registered by the loader install),
710        // so this needs no ordering vs the loader install.
711        lattice_plugin_trace::install(&mut boot);
712        // MG.1: Magit — git porcelain as a core plugin. Registers
713        // `magit-core-mode` (minor), `magit-status-mode` (major),
714        // and the `:magit-status` ex-command. See magit.md §5.
715        // TR.1: the transient-menu registry — a `lattice-picker` mechanism the
716        // EDITOR owns, registered before any install that populates it.
717        //
718        // It used to be constructed and registered by `lattice-magit::install`
719        // below, which made every transient menu in the editor conditional on
720        // magit having loaded. Magit was only the first user; a
721        // plugin-contributed menu (TR.2) would otherwise work or not depending
722        // on whether an unrelated feature crate happened to be present, with
723        // nothing at the point of failure to explain it. Magit now registers
724        // its sources into the service it looks up here.
725        boot.register_service::<lattice_picker::TransientSourceRegistryHandle>(
726            std::sync::Arc::new(lattice_picker::TransientSourceRegistry::new()),
727        );
728
729        lattice_magit::install(&mut boot);
730        // LSP (BC.8a — last + largest, sub-sliced BC.8a–e): registers the LSP
731        // modes (`lsp-completion-mode` reads the supervisor handle via
732        // `boot.service::<LspSupervisorHandle>()`, registered in Phase A) + the
733        // four `workspace/*/refresh` off-keystroke wakes (`boot.wake_on_event`).
734        // Residue staying host-side (NOT violations): `build_lsp_subsystem`
735        // (produces Editor fields — the diff `DiffSubsystem`-bind class), the
736        // host-created services (logger / diagnostics-query), and the four
737        // inbound buses + drains (reshaped onto `boot.inbound::<T>` in BC.8b–e).
738        // See `lattice_lsp::install` for the full rationale.
739        lattice_lsp::install(&mut boot);
740
741        // BC.3a: freeze the mode registry into its shared `Arc` BEFORE
742        // `register_mode_toggle_commands`. The toggle helper needs
743        // `&mut CommandRegistry` + `&ModeRegistry` simultaneously; both live
744        // in `boot`, so a concurrent `boot.commands_mut()` + mode-read borrow
745        // would conflict. Freezing first hands back an `Arc<ModeRegistry>`
746        // (derefs to `&ModeRegistry`); the registry is fully populated here,
747        // so the auto-generated `:<mode-name>` toggles are identical.
748        let mode_registry = boot.freeze_mode_registry();
749        register_mode_toggle_commands(boot.commands_mut(), &mode_registry.load());
750        // PL8.B: share the runtime-mutable mode registry so the plugin loader can
751        // RCU-register a mode plugin at runtime (`service::<ModeRegistryHandle>()`).
752        boot.register_service::<lattice_mode::ModeRegistryHandle>(mode_registry.clone());
753
754        // Slice 8.i action ids: each `CommandKind::Action` entry
755        // returns `Effect::AppAction(AppEffect::Foo)`; per-mode
756        // keymap modules consume the resulting `ActionIds` to
757        // build typed `CommandInvocation`s for chord bindings.
758        let action_ids = crate::actions::populate(boot.commands_mut(), &builtins);
759
760        // `repl-mode` (foundation minor, registered above) owns its
761        // `action:repl-focus-input` command. Register it here so
762        // `translate_mode_keymaps` resolves the mode's keymap `cmd` name and
763        // `register_mode_action_handlers` binds the handler — both run later in
764        // boot. The `register_ai_conversation_actions` pattern, kept with the
765        // mode's own crate.
766        lattice_mode::register_repl_mode_actions(boot.commands_mut());
767
768        // `help-mode` owns `<Esc>` → `action:help-dismiss` (the LM.4 file-tree
769        // pattern). Register the command here so `translate_mode_keymaps`
770        // resolves the mode's keymap `cmd` name; its body emits
771        // `Effect::DismissPopup`, which the host applies as the right dismiss
772        // for the help buffer's display (close split pane / dismiss popup /
773        // restore active-pane). Without this the binding drops with a warn.
774        lattice_mode::register_help_mode_actions(boot.commands_mut());
775
776        // RV.1: same shape for `refreshable-view-mode`'s
777        // `action:view-refresh` — the generic target the shared `gr`
778        // binds to. Its registered `apply` never runs: chord dispatch
779        // intercepts the CommandId, resolves the active modes'
780        // `refresh_action()`, and dispatches that instead. It exists so
781        // the name resolves for the keymap binding.
782        lattice_mode::register_refreshable_view_actions(boot.commands_mut());
783
784        // OA.4b: and `foldable-view-mode`'s three. `action:view-fold-toggle`
785        // is the same dead-apply indirection as `action:view-refresh`; the
786        // other two are real bodies, because "cycle the fold at the cursor"
787        // and "cycle every fold" are generic and have nothing per-view to
788        // declare.
789        lattice_mode::register_foldable_view_actions(boot.commands_mut());
790
791        // TB.1: and `table-mode`'s eleven. All real bodies — pipe-table
792        // editing is generic, so there is nothing per-major to declare and no
793        // indirection to route through. Registered here so
794        // `translate_mode_keymaps` can resolve the mode's `cmd` names; an
795        // unresolvable name is dropped from the layer with a `warn`, which is
796        // a whole keymap that silently does nothing.
797        lattice_mode::register_table_actions(boot.commands_mut());
798
799        // BC.7 (2026-06-24): the multibuffer excerpt-jump motions
800        // (`]e`/`[e`/`]E`/`[E`), the `:multibuffer-*` / `:narrow` / `:widen` /
801        // `:search` ex-commands, AND the `zn` narrow operator SPEC are all
802        // registered by `lattice_multibuffer::install(boot)` above. The host no
803        // longer threads the operator's `OperatorId` from registration to the
804        // `zn` binding — the binding resolves `operator:narrow` by name (the
805        // K.2.5 motion name-resolution pattern); see the
806        // `register_operator_bindings` call below.
807
808        // N.1.4c: register the structural (tree-sitter) text objects
809        // (`af`/`if`/`ac`/`ic`/`aa`/`ia`/`al`/`il`) -- owned by
810        // lattice-syntax -- and capture their ids so the universal
811        // operator-pending keymap (`register_normal_bindings` + the `zn`
812        // operator below) can bind their chords. Must run while the command
813        // registry is still mutable (before `freeze_command_registry` below).
814        let syntax_textobject_ids =
815            lattice_syntax::register_syntax_text_objects(boot.commands_mut());
816
817        // TSM.4: register the sixteen structural (tree-sitter) MOTIONS
818        // (`]f`/`[f`/`]F`/`[F`, `]c`/`[c`/`]C`/`[C`, `]a`/`[a`/`]A`/`[A`,
819        // `]l`/`[l`/`]L`/`[L`) -- the motion counterpart to the structural
820        // text objects registered just above. Same discipline: owned by
821        // lattice-syntax, threaded to the keymap binders so the host only
822        // wires chord -> id. Must run while the command registry is still
823        // mutable (before `freeze_command_registry` below).
824        let syntax_motion_ids = lattice_syntax::register_syntax_motions(boot.commands_mut());
825
826        // §5.11.3 completion pipeline: register the built-in
827        // generators / matchers / rankers / annotators and wire
828        // sensible defaults (prefix matcher, score ranker, kind
829        // + doc annotators).
830        let mut completion_registry = CompletionRegistry::new();
831        let _completion_builtins = lattice_completion::populate(&mut completion_registry);
832
833        // Help-topic registry + its completion generator
834        // (`gen:help-topics`). Registering here lets `:help <Tab>`
835        // enumerate built-in + plugin-supplied topics through
836        // the same pipeline `:e <Tab>` and `:describe-command <Tab>`
837        // use.
838        // CR.1: the builtin set is the registry's *initial* contents, not
839        // the whole of it — wrap it in the RCU handle the `help` plugin
840        // seam registers through, and hand the same handle to the
841        // candidate generator so `:help <Tab>` enumerates plugin topics
842        // too (a boot-time snapshot there would list the builtins
843        // forever).
844        let help_topics = crate::help_topics::builtin_topics().into_handle();
845        completion_registry.register_generator(
846            "gen:help-topics",
847            "Every registered free-form help topic (`:help <topic>`).",
848            crate::help_topics::HelpTopicsGenerator {
849                topics: help_topics.clone(),
850            },
851        );
852        // CR.1: published as a service so `lattice_plugin_loader::install`
853        // (further down this function) can RCU a `help` plugin's topics
854        // in. Ordering is load-bearing, not incidental — a handle
855        // registered after the loader is a silent no-contribution, which
856        // is what `PluginLoaderError::NotWired` exists to make loud.
857        boot.register_service::<lattice_help::topics::HelpTopicRegistryHandle>(help_topics.clone());
858
859        // Subscribe the editor's cascade-handler channel to
860        // `OptionChanged` events on the bus. The receiver lives
861        // on `Editor.option_change_rx`; the runtime's per-tick
862        // drain pulls from it. This decouples cascades from the
863        // publish path: any consumer that calls `config.set`
864        // -- the cmdline, plugins, the future customize buffer
865        // view -- triggers the cascade through the same channel.
866        let (option_tx, option_change_rx) = tokio::sync::mpsc::unbounded_channel();
867        event_bus.subscribe(
868            lattice_runtime::EventFilter::kind(lattice_protocol::EventKind::OptionChanged),
869            lattice_runtime::SubscriptionTarget::Channel(option_tx),
870        );
871        // A plugin finishing its load must WAKE the editor. Contributions land
872        // on a background task (the loader discovers + loads off the boot
873        // thread), and the producer pumps that consume them —
874        // `maybe_refresh_wasm_context`, `maybe_refresh_wasm_decorations` — run
875        // only inside `run_tick_pending`, which fires on a keystroke or on
876        // `async_landed`. Nothing else fires it when a producer registers, so
877        // the pumps sat until something UNRELATED woke the editor.
878        //
879        // In practice that was the LSP becoming ready, which is why the sticky
880        // context strip appeared only after the LSP scan finished despite
881        // having nothing to do with the LSP. Exactly the shape
882        // `boot-composition.md` §3 designs out: a result that reaches the
883        // screen only because something else happened to knock.
884        {
885            let (plugin_tx, mut plugin_rx) = tokio::sync::mpsc::unbounded_channel();
886            event_bus.subscribe(
887                lattice_runtime::EventFilter::kind(lattice_protocol::EventKind::PluginLoaded),
888                lattice_runtime::SubscriptionTarget::Channel(plugin_tx),
889            );
890            let wake = Arc::clone(&async_landed);
891            boot.runtime_handle().spawn(async move {
892                while plugin_rx.recv().await.is_some() {
893                    wake.notify_one();
894                }
895            });
896        }
897        // LSP log live-tail.
898        let (lsp_log_tx, lsp_log_event_rx) =
899            tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspLogPushed>();
900        event_bus.subscribe_typed(lsp_log_tx);
901        // ML.3c: LSP `$/progress` + `experimental/serverStatus` are no
902        // longer accumulated host-side. `lattice_lsp::modeline`'s
903        // forwarder subscribes them, folds them into the shared
904        // `LspProgressStore` (created below), and pushes the `lsp` element
905        // per attached buffer; the host reads the same store only for
906        // `:lsp-progress-cancel`.
907        // `LspBufferDetached`: `LspMode::on_deactivate` publishes
908        // this; the per-tick drain calls `lsp_close_buffer` for
909        // each so the wire-level `didClose` + `buffer_uris`
910        // cleanup runs *after* the mode lifecycle.
911        let (lsp_detach_tx, lsp_detach_rx) =
912            tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspBufferDetached>();
913        event_bus.subscribe_typed(lsp_detach_tx);
914        // M-async.3: mode lifecycle events for `ModeActivationFailed`
915        // (and aborted cascade parents); the per-tick drain calls
916        // `deactivate_mode_by_id` on each.
917        let (mode_lifecycle_tx, mode_lifecycle_rx) =
918            tokio::sync::mpsc::unbounded_channel::<lattice_mode::ModeEvent>();
919        event_bus.subscribe_typed(mode_lifecycle_tx);
920        // ML.3: modeline element content pushed by modes/plugins over the
921        // bus. `drain_modeline_element_updates` applies each into the
922        // shared `modeline` content store (single-writer, actor thread);
923        // a separate subscription in the L1c wake block fires
924        // `async_landed` so the push repaints off-keystroke (§12 wake).
925        let (modeline_update_tx, modeline_update_rx) =
926            tokio::sync::mpsc::unbounded_channel::<lattice_mode::ModelineElementUpdate>();
927        event_bus.subscribe_typed(modeline_update_tx);
928        // MA.2: minor-activation resolver input. One channel
929        // subscribed to `Event::MajorEntered`; the per-tick
930        // `drain_minor_activation` reads it, looks up each buffer's
931        // kind, and auto-activates the minors whose ActivationPolicy
932        // admits the entered major (Global gated to document buffers).
933        let (major_entered_tx, major_entered_rx) =
934            tokio::sync::mpsc::unbounded_channel::<lattice_protocol::Event>();
935        event_bus.subscribe(
936            lattice_runtime::EventFilter::kind(lattice_protocol::EventKind::MajorEntered),
937            lattice_runtime::SubscriptionTarget::Channel(major_entered_tx),
938        );
939        // CI.4: the mode-enablement bridge. A plugin's `enable-mode` publishes
940        // `Event::ModeEnablementRequested`; the per-tick `drain_mode_enablement`
941        // flips the mode registry + re-activates open buffers (the guest can't
942        // reach the activator, so it routes through here — config-and-init.md §6).
943        // The buffer-local option bridge. A plugin's `set-option-in-buffer`
944        // publishes `Event::BufferOptionOverrideRequested`; the per-tick drain
945        // writes the buffer-local override layer. Same shape and same reason as
946        // the enablement bridge below — the guest cannot reach the Editor.
947        let (buffer_option_override_tx, buffer_option_override_rx) =
948            tokio::sync::mpsc::unbounded_channel::<lattice_protocol::Event>();
949        event_bus.subscribe(
950            lattice_runtime::EventFilter::kind(
951                lattice_protocol::EventKind::BufferOptionOverrideRequested,
952            ),
953            lattice_runtime::SubscriptionTarget::Channel(buffer_option_override_tx),
954        );
955        let (mode_enablement_tx, mode_enablement_rx) =
956            tokio::sync::mpsc::unbounded_channel::<lattice_protocol::Event>();
957        event_bus.subscribe(
958            lattice_runtime::EventFilter::kind(
959                lattice_protocol::EventKind::ModeEnablementRequested,
960            ),
961            lattice_runtime::SubscriptionTarget::Channel(mode_enablement_tx),
962        );
963        // OA.15a: the provider-view refresh bridge. A plugin's `refresh-view`
964        // publishes `ProviderViewRefreshRequested`; `drain_provider_view_refresh`
965        // re-opens the view through the registered opener (the guest cannot
966        // reach the activator, so it routes through here — `enable-mode`'s
967        // shape, one seam over).
968        //
969        // TWO subscriptions, and the second is the load-bearing one. The
970        // channel accumulates for the per-tick drain; the wake forwarder fires
971        // `async_landed` so that tick HAPPENS without a keystroke. A plugin
972        // toggling a display mode and seeing nothing until the next keypress is
973        // precisely the failure class `boot-composition.md` §3 designs out, and
974        // it has been re-introduced by reaching for a bare channel more than
975        // once.
976        let (view_refresh_tx, view_refresh_rx) = tokio::sync::mpsc::unbounded_channel::<
977            lattice_mode::provider_view::ProviderViewRefreshRequested,
978        >();
979        event_bus.subscribe_typed(view_refresh_tx);
980        {
981            let (wake_tx, mut wake_rx) = tokio::sync::mpsc::unbounded_channel::<
982                lattice_mode::provider_view::ProviderViewRefreshRequested,
983            >();
984            event_bus.subscribe_typed(wake_tx);
985            let wake = async_landed.clone();
986            runtime_handle.spawn(async move {
987                while wake_rx.recv().await.is_some() {
988                    wake.notify_one();
989                }
990            });
991        }
992        // SN.2: the live snippet session, shared between the host
993        // (creates it on expand) and `SnippetActiveMode`'s
994        // `<Tab>`/`<S-Tab>` handlers (navigate it). The same Arc is
995        // both stored on the Editor and registered in ServiceRegistry.
996        let snippet_session: lattice_snippet::SnippetSessionHandle =
997            Arc::new(lattice_snippet::SnippetSession::new());
998        // Inlay-hint refresh.
999        let (lsp_inlay_refresh_tx, lsp_inlay_refresh_rx) =
1000            tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspInlayHintRefresh>();
1001        event_bus.subscribe_typed(lsp_inlay_refresh_tx);
1002        // Semantic-tokens refresh.
1003        let (lsp_semantic_tokens_refresh_tx, lsp_semantic_tokens_refresh_rx) =
1004            tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspSemanticTokensRefresh>();
1005        event_bus.subscribe_typed(lsp_semantic_tokens_refresh_tx);
1006        // Pull-diagnostic refresh.
1007        let (lsp_diagnostic_refresh_tx, lsp_diagnostic_refresh_rx) =
1008            tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspDiagnosticRefresh>();
1009        event_bus.subscribe_typed(lsp_diagnostic_refresh_tx);
1010        // Code-lens refresh.
1011        let (lsp_code_lens_refresh_tx, lsp_code_lens_refresh_rx) =
1012            tokio::sync::mpsc::unbounded_channel::<lattice_lsp::LspCodeLensRefresh>();
1013        event_bus.subscribe_typed(lsp_code_lens_refresh_tx);
1014        // `*messages*` buffer live-tail subscriber.
1015        let (message_event_tx, message_event_rx) =
1016            tokio::sync::mpsc::unbounded_channel::<lattice_runtime::MessagePushed>();
1017        event_bus.subscribe_typed(message_event_tx);
1018
1019        // msg-mode.1: install the global `tracing::Subscriber`
1020        // bridge. `install_messages_subscriber` is idempotent
1021        // (process-wide `set_global_default`); only the first
1022        // call succeeds. Tests with multiple Editor instances
1023        // share the first-installed layer, which is fine because
1024        // tests use the per-test layer constructor for unit
1025        // coverage rather than relying on the global install.
1026        //
1027        // 2026-05-22 messages-overhaul: read initial filter from
1028        // the runtime's `boot_log_level` (set by the CLI from
1029        // -v/-q/--log-level flags before App::new runs). Falls
1030        // back to "info" when unset (library / test callers).
1031        // The subscriber composes a fmt layer + MessagesLayer so
1032        // tracing events land in BOTH stderr and `*messages*`.
1033        // Live-editable via `:set messages.filter=<level>`.
1034        let messages_ring = Arc::new(std::sync::Mutex::new(MessagesRing::default()));
1035        let initial_filter =
1036            lattice_runtime::boot_log_level().unwrap_or_else(|| "info".to_string());
1037        // Issue #36 (2026-05-22): TUI peers must NOT enable
1038        // stderr — stderr IS the terminal ratatui paints into.
1039        // CLI sets `boot_stderr_enabled` based on the selected
1040        // renderer (false for TUI; true for GPUI). Library
1041        // callers / tests that don't set it get the safe
1042        // default (false — never accidentally corrupt a TUI
1043        // screen).
1044        let stderr_enabled = lattice_runtime::boot_stderr_enabled().unwrap_or(false);
1045        let _ = lattice_runtime::install_messages_subscriber(
1046            messages_ring.clone(),
1047            event_bus.clone(),
1048            &initial_filter,
1049            stderr_enabled,
1050        );
1051        // Wire the logger's publisher to the same bus. The
1052        // closure captures an Arc<EventBus> clone so the
1053        // logger's lifetime is independent of any single field.
1054        let bus_for_log = event_bus.clone();
1055        lsp_logger.set_event_publisher(Arc::new(move |event| {
1056            bus_for_log.publish_typed(event);
1057        }));
1058
1059        // 4.4.o: seed the LSP logger from typed-options defaults.
1060        // Invalid values were filtered by the option validators
1061        // before they hit the registry; we treat any miss here
1062        // as "use the built-in default the logger already has."
1063        if let Some(level_str) = config.get_typed::<lattice_config::core_options::LspLogLevel>()
1064            && let Some(level) = lattice_lsp::LogLevel::parse(&level_str)
1065        {
1066            lsp_logger.set_default_level(level);
1067        }
1068        if let Some(cap) = config.get_typed::<lattice_config::core_options::LspLogCapacity>() {
1069            lsp_logger.set_default_capacity((*cap).max(0) as usize);
1070        }
1071
1072        // `gen:options` -- completion source for `:set <Tab>` and
1073        // `:set name=<Tab>`. Wired to the same `ConfigRegistry`
1074        // the `:set` parser consults so completions never drift
1075        // from the canonical option list.
1076        completion_registry.register_generator(
1077            "gen:options",
1078            "Every registered option name + (when applicable) its enumerated values.",
1079            lattice_config::OptionsGenerator::new(config.clone()),
1080        );
1081        // Editor-state completion sources for `:describe-*` /
1082        // `:customize` / `:lsp-*` commands. Each generator
1083        // captures the slice of state it needs; names are
1084        // stable so `ArgSpec::completion` references stay in
1085        // sync.
1086        completion_registry.register_generator(
1087            "gen:events",
1088            "Every typed event registered via `register_event!`; used by `:describe-event <Tab>`.",
1089            crate::host_generators::EventsGenerator,
1090        );
1091        completion_registry.register_generator(
1092            "gen:log-levels",
1093            "The five log levels (`error`/`warn`/`info`/`debug`/`trace`); used by `:lsp-log-level <Tab>`.",
1094            crate::host_generators::LogLevelsGenerator,
1095        );
1096        completion_registry.register_generator(
1097            "gen:lsp-servers",
1098            "Currently running LSP server ids; used by `:lsp-log <Tab>` / `:lsp-restart <Tab>` / etc.",
1099            crate::host_generators::LspServersGenerator { lsp: lsp.clone() },
1100        );
1101        completion_registry.register_generator(
1102            "gen:customize",
1103            "Group names + mode names; used by `:customize <Tab>`.",
1104            crate::host_generators::CustomizeNamesGenerator {
1105                registry: Arc::downgrade(&mode_registry),
1106                config: config.clone(),
1107            },
1108        );
1109
1110        // BC.3a: freeze the command registry into its shared `Arc` — all
1111        // command registration (builtins, ex-commands, toggles, actions,
1112        // multibuffer motions/ex-commands, narrow, syntax text objects) is
1113        // done by here, and the `Arc` is consumed just below (picker sources
1114        // that capture it, document handles). Subsequent `boot.commands_mut()`
1115        // panics — a boot-sequencing bug.
1116        let registry = boot.freeze_command_registry();
1117        // T.3/T.4 (theme-system): the theme-element registry, seeded
1118        // with the default palette + all builtin elements (resolved +
1119        // ready). Created here (ahead of the struct literal) so the
1120        // T.12a colorscheme picker source can capture a clone, AND the
1121        // `services:` block / `builtin_element_ids` capture / the
1122        // `theme_registry` field all share the one Arc. See
1123        // theme-system.md §3.5 / §7.
1124        let theme_registry: lattice_theme::ThemeRegistryHandle =
1125            Arc::new(lattice_theme::InMemoryThemeRegistry::with_defaults());
1126        // MP.2b: create the keymap handle here (ahead of the
1127        // `keymap:` struct field that registers all bindings) so
1128        // the commands picker can capture a reverse-lookup
1129        // adapter over the *same* registry. `KeymapHandle` is
1130        // dependency-free and `Clone` over an inner
1131        // `Arc<KeymapRegistry>`; the adapter holds a clone of the
1132        // registry's `ArcSwap` reverse cache, so bindings
1133        // registered later (in the `keymap:` block, via the moved
1134        // handle) are visible to the picker at open time. See
1135        // `marginalia.md` §6 + the wiring rationale in the
1136        // picker-marginalia slice plan (MP.2b).
1137        let keymap_handle = crate::keymap_registry::KeymapHandle::new();
1138        // VM.4: hand the keymap the live command registry, so "a motion is live
1139        // in Visual" is enforced at every keymap write rather than by a pass
1140        // someone has to remember to re-run. Set before any binder runs, though
1141        // correctness doesn't depend on that: setting it later rescans every
1142        // existing layer. The operator-pending half stays in
1143        // `expand_grammar_rows`, which needs `Builtins`.
1144        keymap_handle.set_command_registry(registry.clone());
1145        let keybinding_reverse: Arc<dyn lattice_completion::KeymapReverseLookup> =
1146            crate::keymap_registry::KeymapReverseLookupHandle::new(
1147                &keymap_handle,
1148                registry.clone(),
1149            );
1150        // PH.3: the shared lang registry — created here (ahead of the
1151        // document `Syntax` below, which reuses it) so the grep picker's
1152        // preview highlighter selects grammars from the same set the
1153        // buffers use. The highlighter parses each grep hit's preview
1154        // line on the grep blocking task (off the render thread; the
1155        // picker crate has no syntax dep). See picker-preview-highlight.md §7.
1156        let lang_registry = LangRegistry::standard().expect("standard lang registry");
1157        // MG.26c: expose the SAME registry every buffer's grammar comes
1158        // from, so a mode that owns a pathless synthetic buffer can
1159        // highlight it. Building a second `LangRegistry::standard()`
1160        // inside such a mode would load every grammar twice and would
1161        // drift from whatever the host is actually configured with.
1162        boot.register_service::<Arc<LangRegistry>>(lang_registry.clone());
1163        let grep_highlighter: Option<
1164            Arc<dyn lattice_picker::picker_sources::GrepPreviewHighlighter>,
1165        > = Some(crate::grep_highlight::SyntaxGrepHighlighter::new());
1166        // PL8.B: held behind `ArcSwap` so the plugin loader can RCU-register a
1167        // loaded picker plugin's source at runtime while the picker-open path
1168        // reads it wait-free. Registered as a `PickerRegistryHandle` service
1169        // below so `lattice-plugin-loader` reaches it without a host dep.
1170        let picker_registry: lattice_picker::PickerRegistryHandle =
1171            Arc::new(arc_swap::ArcSwap::from_pointee(built_in_picker_registry(
1172                registry.clone(),
1173                config.clone(),
1174                keybinding_reverse,
1175                grep_highlighter,
1176                snippet_registry_handle.clone(),
1177                theme_registry.clone(),
1178                // MR.6: magit's pickers list the repository the picker was
1179                // opened over. Both handles exist by now — this runs after
1180                // the Phase-B install list, where magit registers its
1181                // `RepoScopes` — which is what lets the lens be built here
1182                // rather than threaded through the whole builder.
1183                match boot.service::<lattice_magit::repo_scope::RepoScopesHandle>() {
1184                    Some(scopes) => lattice_magit::picker_sources::RepoLens::new(
1185                        buffer_store_handle.clone(),
1186                        (*scopes).clone(),
1187                    ),
1188                    None => lattice_magit::picker_sources::RepoLens::default(),
1189                },
1190            )));
1191        boot.register_service::<lattice_picker::PickerRegistryHandle>(picker_registry.clone());
1192
1193        // PL8.E: the runtime-mutable registry of async gutter-decoration
1194        // producers. Held behind `ArcSwap` so the plugin loader
1195        // (`drain_decorations`) RCU-registers a loaded decoration plugin's
1196        // producer while the host's per-tick refresh reads it wait-free.
1197        // Registered as a service so `lattice-plugin-loader` reaches it without
1198        // a host dep, and cloned onto the `Editor` below so the refresh drives
1199        // it. Starts empty — no producer until a decoration plugin loads.
1200        let decoration_registry: lattice_mode::GutterDecorationSourceRegistryHandle = Arc::new(
1201            arc_swap::ArcSwap::from_pointee(lattice_mode::GutterDecorationSourceRegistry::new()),
1202        );
1203        boot.register_service::<lattice_mode::GutterDecorationSourceRegistryHandle>(
1204            decoration_registry.clone(),
1205        );
1206        // OA.30: the counter a guest bumps to say its decorations changed
1207        // though the document did not. Registered as a service so the plugin
1208        // loader can hand it to the host, and cloned onto the `Editor` below so
1209        // the refresh pump compares it.
1210        let decoration_epoch: lattice_mode::DecorationEpochHandle =
1211            Arc::new(lattice_mode::DecorationEpoch::default());
1212        boot.register_service::<lattice_mode::DecorationEpochHandle>(decoration_epoch.clone());
1213
1214        // IM.7: the sibling registry for inline-media producers. Same shape and
1215        // the same reason — RCU-registered by the loader, read wait-free by the
1216        // host's per-tick refresh.
1217        let media_registry: lattice_mode::MediaSourceRegistryHandle = Arc::new(
1218            arc_swap::ArcSwap::from_pointee(lattice_mode::MediaSourceRegistry::new()),
1219        );
1220        boot.register_service::<lattice_mode::MediaSourceRegistryHandle>(media_registry.clone());
1221        // `image-mode`'s own producer: a buffer backed by a picture draws that
1222        // picture. Registered here because this is where the registry is
1223        // created; the producer itself lives with the mode, so the major and
1224        // the thing that renders its buffers stay one surface.
1225        lattice_mode::modes::register_image_media_source(&media_registry);
1226
1227        // OM.A1: the sibling registry for agenda-row producers. Same shape and
1228        // the same reason — RCU-registered by the loader, read wait-free by
1229        // the agenda provider's scan. Registered here unconditionally, even in
1230        // a build whose multibuffer `agenda` feature is off: the seam is wired
1231        // for the LOADER, and whether a view consumes it is a separate
1232        // question. `wired_seams().all()` asserts it.
1233        let agenda_registry: lattice_mode::ScannedExcerptSourceRegistryHandle = Arc::new(
1234            arc_swap::ArcSwap::from_pointee(lattice_mode::ScannedExcerptSourceRegistry::new()),
1235        );
1236        boot.register_service::<lattice_mode::ScannedExcerptSourceRegistryHandle>(agenda_registry);
1237
1238        // TC.2: the sibling registry for async context-scope producers
1239        // (`drain_context`). Same shape and the same reason — RCU-registered by
1240        // the loader, read wait-free by the host's reparse-driven refresh.
1241        // Registered here at boot so `wired_seams().all()` holds; the refresh
1242        // that consumes it lands with the host layer (TC.3).
1243        let context_registry: lattice_mode::ContextSourceRegistryHandle = Arc::new(
1244            arc_swap::ArcSwap::from_pointee(lattice_mode::ContextSourceRegistry::new()),
1245        );
1246        boot.register_service::<lattice_mode::ContextSourceRegistryHandle>(
1247            context_registry.clone(),
1248        );
1249
1250        // MRU cache load. Honor `picker.mru.persist` at boot;
1251        // failure modes: no persist path (sandboxed), no file
1252        // (fresh install), or corrupt file (log + reset).
1253        let persist = config
1254            .get_typed::<lattice_config::core_options::PickerMruPersist>()
1255            .map(|b| *b)
1256            .unwrap_or(true);
1257        let picker_mru_path = if persist {
1258            lattice_picker::default_persist_path()
1259        } else {
1260            None
1261        };
1262        let mru_cap = config
1263            .get_typed::<lattice_config::core_options::PickerMruCapPerNamespace>()
1264            .map(|n| (*n).max(1) as usize)
1265            .unwrap_or(lattice_picker::DEFAULT_CAP_PER_NAMESPACE);
1266        let picker_mru = match &picker_mru_path {
1267            Some(path) => match lattice_picker::PickerMruIndex::load_from(path) {
1268                Ok(Some(idx)) => idx,
1269                Ok(None) => lattice_picker::PickerMruIndex::with_cap(mru_cap),
1270                Err(e) => {
1271                    // Route through tracing, not raw stderr: stderr may be
1272                    // the TUI's terminal (see the picker-MRU-save fix in
1273                    // dispatch.rs). → *messages*.
1274                    tracing::warn!("discarding corrupt MRU cache at {}: {e}", path.display());
1275                    lattice_picker::PickerMruIndex::with_cap(mru_cap)
1276                }
1277            },
1278            None => lattice_picker::PickerMruIndex::with_cap(mru_cap),
1279        };
1280        // `gen:modes` — one candidate per registered mode, backing
1281        // `:describe-mode <Tab>`. `ModesGenerator` existed in
1282        // `host_generators` but was never constructed, so the source
1283        // `:describe-mode`'s ArgSpec advertises resolved to nothing and
1284        // `<Tab>` silently produced no candidates — exactly the bug
1285        // class `every_advertised_completion_source_resolves_at_boot`
1286        // was written to catch.
1287        completion_registry.register_generator(
1288            "gen:modes",
1289            "Every registered mode name; drives `:describe-mode <Tab>` \
1290             completion.",
1291            crate::host_generators::ModesGenerator {
1292                registry: Arc::downgrade(&mode_registry),
1293            },
1294        );
1295        completion_registry.register_generator(
1296            "gen:picker-sources",
1297            "Every source id registered with the `PickerRegistry`; \
1298             drives `:picker <Tab>` completion.",
1299            crate::host_generators::PickerSourcesGenerator {
1300                registry: Arc::downgrade(&picker_registry),
1301            },
1302        );
1303        // T.9.d follow-up: `gen:elements` — theme-element / face names for
1304        // `:describe-element <Tab>` / `:describe-face <Tab>`. Holds a clone of
1305        // the same `theme_registry` handle the renderers + `:describe-element`
1306        // handler read, so completion never drifts from the live element set.
1307        completion_registry.register_generator(
1308            "gen:elements",
1309            "Every registered theme element / face name; used by \
1310             `:describe-element <Tab>` / `:describe-face <Tab>`.",
1311            crate::host_generators::ElementsGenerator {
1312                registry: theme_registry.clone(),
1313            },
1314        );
1315        completion_registry.register_generator(
1316            "gen:themes",
1317            "Every registered colour theme (`:colorscheme <Tab>`).",
1318            crate::host_generators::ThemesGenerator {
1319                registry: theme_registry.clone(),
1320            },
1321        );
1322        completion_registry.register_generator(
1323            "gen:plugin-api-seams",
1324            "Every WIT interface in the plugin-API catalog \
1325             (`:describe-plugin-api <Tab>`).",
1326            crate::host_generators::PluginApiSeamsGenerator,
1327        );
1328        completion_registry.register_generator(
1329            "gen:plugin-api-formats",
1330            "Export formats accepted by `:export-plugin-api`.",
1331            crate::host_generators::PluginApiFormatsGenerator,
1332        );
1333        // MB.5: `gen:history-kinds` — valid args for `:history <Tab>`.
1334        completion_registry.register_generator(
1335            "gen:history-kinds",
1336            "Valid history kind arguments (`commands`, `searches`, `pane-buffers`); used by `:history <Tab>`.",
1337            crate::host_generators::HistoryKindsGenerator,
1338        );
1339
1340        // `lang_registry` (one per Editor, shared between the document
1341        // buffer's `Syntax`, every `HelpBuffer`, and the grep preview
1342        // highlighter) was created above with the picker registry.
1343        let lang = Lang::detect_from_path(document.path());
1344        // Build the underlying `Syntax` synchronously + seed it
1345        // with one parse of the initial text so the renderer's
1346        // first frame has highlights without waiting for the
1347        // worker. After that the handle takes over: subsequent
1348        // `request_reparse` calls run the parse on a worker
1349        // thread; the renderer reads the latest snapshot via
1350        // `ArcSwap`.
1351        let initial_text = document.text();
1352        let initial_text_version = document.text_version();
1353        // BC.3a: `async_landed` is a Phase-A primitive (created up top, owned
1354        // by `boot`). The reparse worker below takes it as its `on_publish`
1355        // wake and the diagnostics layer's `set_wake` arms it here.
1356        // Wake the render loop on every server `publishDiagnostics`
1357        // push so diagnostic changes — a new error OR the clear when one
1358        // is fixed — repaint off-keystroke, instead of waiting for the
1359        // next cursor-driven publish. The layer fires `async_landed`
1360        // from `apply`; shared across every actor pump via the cloned
1361        // `DiagnosticsLayer` (Arc-backed). See lsp-architecture.md §12.
1362        lsp_diagnostics.set_wake(async_landed.clone());
1363        let syntax: Option<SyntaxHandle> =
1364            match Syntax::for_language_with_registry(lang, lang_registry.clone()) {
1365                Ok(Some(mut s)) => {
1366                    s.parse_at(&initial_text, initial_text_version);
1367                    let al = async_landed.clone();
1368                    let eb = event_bus.clone();
1369                    Some(SyntaxHandle::seeded_with_runtime(
1370                        s,
1371                        &runtime_handle,
1372                        Some(std::sync::Arc::new(move || {
1373                            al.notify_one();
1374                            eb.publish_typed(crate::events::SyntaxReparsed);
1375                        })),
1376                    ))
1377                }
1378                _ => None,
1379            };
1380        let last_parsed_text_version = initial_text_version;
1381
1382        // M.2.b.0.A: allocate BufferId before spawning so the
1383        // handle carries its own registry id (used by
1384        // `MotionContext::buffer_id` for kind-specific motion
1385        // handlers).
1386        let document_buffer_id = BufferId::next();
1387        // Hand the document to the actor (DESIGN.md §5.7).
1388        // After this call the only way to read or mutate it is
1389        // through the returned `RopeDocumentHandle`.
1390        let handle = spawn_document(document_buffer_id, document, registry.clone());
1391        let snapshot_cache = handle.snapshot_cache();
1392        // M.0: wrap the handle in `ActiveDocument` so the slot
1393        // can hold either a regular doc or (M.1+) a multibuffer
1394        // handle without kind-branching at the use site.
1395        let document = lattice_runtime::ActiveDocument::new(handle);
1396        let initial_pane = PaneState {
1397            id: PaneId::next(),
1398            buffer: BufferKind::Document,
1399            buffer_id: document_buffer_id,
1400            cursor: Position::ZERO,
1401            scroll: 0,
1402            leftcol: 0,
1403            // Populated by the renderer's per-frame layout pass
1404            // (Issue #25, 2026-05-22). Zero at boot is safe — the
1405            // first frame's `set_viewport_*` calls update before
1406            // any motion / ensure-visible reads.
1407            viewport_height: 0,
1408            viewport_width: 0,
1409            committed_buffer_id: None,
1410            // VM.3j-3: a fresh window has no `scroll` of its own.
1411            scroll_lines: None,
1412        };
1413        let pane_tree = PaneTree::single(initial_pane);
1414
1415        // Seed the buffer registry with the initial document. `buffers` is a
1416        // Phase-A primitive (created empty up top, handed to `boot` via the
1417        // `BufferStoreHandle`); this seeding is observable through that handle
1418        // (shared inner Arc). The hot-path `Editor.document` / `Editor.syntax`
1419        // / `Editor.last_parsed_text_version` mirror what's stored here for the
1420        // active buffer; switching buffers swaps them.
1421        buffers.insert(BufferEntry {
1422            id: document_buffer_id,
1423            flags: BufferFlags::default(),
1424            data: BufferData::Document(DocumentEntry {
1425                id: document_buffer_id,
1426                handle: document.as_arc(),
1427            }),
1428            name: None,
1429        });
1430
1431        // M.3.2.c.4: seed the initial document's buffer-locals
1432        // so reader-side flips can route through the locals map
1433        // for inactive buffers uniformly. The active buffer's
1434        // hot-path fields (Editor.syntax / Editor.folds / ...)
1435        // are still canonical until the readers flip; locals are
1436        // updated at each de-activation boundary via
1437        // `seed_document_entry_locals`.
1438        let mut buffer_locals: HashMap<BufferId, lattice_mode::BufferLocals> = HashMap::new();
1439        let mut initial_locals = lattice_mode::BufferLocals::default();
1440        initial_locals.insert(crate::modes::DocumentSyntax(None));
1441        initial_locals.insert(crate::modes::DocumentLastParsedTextVersion(0));
1442        initial_locals.insert(crate::modes::DocumentLastSyncedSyntaxVersion(0));
1443        initial_locals.insert(crate::modes::DocumentFolds(Vec::new()));
1444        buffer_locals.insert(document_buffer_id, initial_locals);
1445
1446        // Phase 5.8.AF.5 / Slice X2.4 (gut + rename B4.2):
1447        // instantiate the overlay worker's shared cell BEFORE the
1448        // Editor literal so we can hand the worker its own clone at
1449        // spawn time. The Editor literal below assigns these into the
1450        // struct fields explicitly (overriding the
1451        // `..Editor::default()` tail) so all three holders (Editor,
1452        // RenderState, worker) share the SAME Arc identity — the
1453        // worker's writes into the quads cell are observable through
1454        // every
1455        // `render_state.load_full().syntax.static_overlay_quads.load()`.
1456        // CG.2: the foreground-cancellation slot. Created before the
1457        // Editor literal and assigned explicitly (overriding the
1458        // `..Editor::default()` tail) so the Editor and the
1459        // ServiceRegistry share the SAME Arc identity — a provider
1460        // arming through `services.get::<ForegroundCancelHandle>()`
1461        // must land in the very slot `<C-g>` cancels, not a sibling
1462        // copy of it. Registered with the other Phase-A handles below.
1463        let foreground_cancel: lattice_mode::ForegroundCancelHandle = Arc::default();
1464        let overlay_wake = crate::editor::OverlayWake::default();
1465        // Perf plan B.2 slice B.2.a: cell carrying the worker's
1466        // per-row pre-bucketed static-overlay quads (doc_highlight /
1467        // all_matches / substitute). Created here so the worker
1468        // (spawned below), the Editor field, and the
1469        // `SyntaxRenderState.static_overlay_quads` clone on every
1470        // `publish_render_state` all share the SAME `Arc` identity.
1471        // Without this shared identity the worker's `.store()` would
1472        // not be observable through `RenderState.load_full()` after
1473        // later publishes.
1474        let syntax_static_overlay_quads_cell: std::sync::Arc<
1475            arc_swap::ArcSwap<crate::render_state::StaticOverlayQuads>,
1476        > = std::sync::Arc::default();
1477        // BC.3a: `render_state_arc` is a Phase-A primitive (created up top,
1478        // owned by `boot`); the workers below + every `publish_render_state`
1479        // share its exact Arc identity.
1480        // X1b: paint-request signal. Created here so the worker
1481        // (spawned below) and the Editor (constructed below) hold
1482        // the same `Arc<Notify>`. The renderer peer subscribes to
1483        // `editor.paint_request` and translates wakes to its own
1484        // redraw mechanism (TUI I.3: a bridge task forwards each notify
1485        // as a `Wake::Repaint` onto the input-reader channel the main
1486        // loop blocks on; GPUI: foreground-executor future that calls
1487        // `cx.notify()`).
1488        let paint_request: std::sync::Arc<tokio::sync::Notify> = std::sync::Arc::default();
1489        runtime_handle.spawn(crate::overlay_worker::run(
1490            render_state_arc.clone(),
1491            overlay_wake.clone(),
1492            syntax_static_overlay_quads_cell.clone(),
1493            paint_request.clone(),
1494        ));
1495
1496        // S2.2 (2026-05-26): cell-builder worker. Same same-Arc-
1497        // identity pattern as the overlay worker — `cells_wake`
1498        // and `cells_matrix_cell` are constructed here, cloned into
1499        // the worker, then assigned into the Editor literal below
1500        // (overriding `..Editor::default()` so all three holders
1501        // share the SAME Arc identities). The worker's `.store()`
1502        // on `cells_matrix_cell` is therefore observable through
1503        // every `render_state.load_full().cells.matrix.load()`.
1504        let cells_wake = crate::editor::CellsWake::default();
1505        let cells_matrix_cell: std::sync::Arc<arc_swap::ArcSwap<lattice_cells::CellMatrix>> =
1506            std::sync::Arc::default();
1507        // D.4.d.0 (2026-05-29): per-document cells-matrix
1508        // registry. Seed with the initial document's matrix
1509        // sharing the same Arc identity as
1510        // `cells_matrix_cell` so the existing worker write
1511        // path and renderer read path stay coherent.
1512        // Subsequent buffer switches insert their own
1513        // entries lazily via `Editor::cells_matrix_for`.
1514        let cells_matrices: std::sync::Arc<
1515            std::sync::Mutex<
1516                std::collections::HashMap<
1517                    lattice_core::BufferId,
1518                    std::sync::Arc<arc_swap::ArcSwap<lattice_cells::CellMatrix>>,
1519                >,
1520            >,
1521        > = {
1522            let mut map = std::collections::HashMap::new();
1523            map.insert(document_buffer_id, cells_matrix_cell.clone());
1524            std::sync::Arc::new(std::sync::Mutex::new(map))
1525        };
1526        // B2.1 (2026-06-04): per-line display-cache output cell +
1527        // per-document registry. Same Arc-identity discipline as the
1528        // cells seed above: the active document's registry entry
1529        // shares its Arc with `display_matrix_cell` so the worker's
1530        // future `.store()` (B2.2) and the renderer's read land on
1531        // the same cell. Subsequent buffers lazy-insert via
1532        // `Editor::display_matrix_for`.
1533        let display_matrix_cell: std::sync::Arc<
1534            arc_swap::ArcSwap<crate::display_matrix::DisplayMatrix>,
1535        > = std::sync::Arc::default();
1536        let display_matrices: std::sync::Arc<
1537            std::sync::Mutex<
1538                std::collections::HashMap<
1539                    lattice_core::BufferId,
1540                    std::sync::Arc<arc_swap::ArcSwap<crate::display_matrix::DisplayMatrix>>,
1541                >,
1542            >,
1543        > = {
1544            let mut map = std::collections::HashMap::new();
1545            map.insert(document_buffer_id, display_matrix_cell.clone());
1546            std::sync::Arc::new(std::sync::Mutex::new(map))
1547        };
1548        // D.4.d.1.b (2026-05-29): the worker now writes per
1549        // pane via `cells.panes[i].matrix` (each entry's cell
1550        // comes from `Editor::cells_matrix_for`), so the
1551        // single top-level `cells_matrix_cell.clone()` arg the
1552        // pre-d.1.b worker took is gone. The active pane's
1553        // entry shares Arc identity with `cells_matrix_cell`
1554        // via the seeded `cells_matrices` registry above, so
1555        // the existing renderer read path keeps landing on
1556        // the worker's writes until D.4.d.1.c teaches the
1557        // renderer about the per-pane map.
1558        runtime_handle.spawn(crate::cells_worker::run(
1559            render_state_arc.clone(),
1560            cells_wake.clone(),
1561            paint_request.clone(),
1562        ));
1563
1564        // D.0a.1 (2026-05-29): virtual-rows worker. Sibling of
1565        // the cells worker — same Arc-sharing discipline so
1566        // `Editor::virtual_rows_matrix_cell` and the worker's
1567        // sibling clone resolve to the same publish target.
1568        let virtual_rows_wake = crate::editor::VirtualRowsWake::default();
1569        let virtual_rows_matrix_cell: std::sync::Arc<
1570            arc_swap::ArcSwap<lattice_cells::VirtualRowMatrix>,
1571        > = std::sync::Arc::default();
1572        // D.4.d.2.0 (2026-05-29): per-document virtual-rows
1573        // matrix registry. Seed with the initial document's
1574        // matrix sharing the same Arc identity as
1575        // `virtual_rows_matrix_cell` so the existing single-
1576        // writer hot path (virtual_rows_worker →
1577        // virtual_rows_matrix_cell → RenderState.virtual_rows.matrix)
1578        // stays bit-identical. Subsequent buffer switches insert
1579        // their own entries lazily via
1580        // `Editor::virtual_rows_matrix_for`. Mirror of the
1581        // `cells_matrices` seeding above.
1582        let virtual_rows_matrices: std::sync::Arc<
1583            std::sync::Mutex<
1584                std::collections::HashMap<
1585                    lattice_core::BufferId,
1586                    std::sync::Arc<arc_swap::ArcSwap<lattice_cells::VirtualRowMatrix>>,
1587                >,
1588            >,
1589        > = {
1590            let mut map = std::collections::HashMap::new();
1591            map.insert(document_buffer_id, virtual_rows_matrix_cell.clone());
1592            std::sync::Arc::new(std::sync::Mutex::new(map))
1593        };
1594        // Reuse the VirtualRowProviderRegistry created during Phase A (the
1595        // `vrp` binding above). Cloning the `Arc` shares the same registry,
1596        // so the service registration, the worker below, and the Editor field
1597        // all see the same providers.
1598        let virtual_row_providers = vrp.clone();
1599        runtime_handle.spawn(crate::virtual_rows_worker::run(
1600            render_state_arc.clone(),
1601            virtual_rows_wake.clone(),
1602            virtual_row_providers.clone(),
1603            paint_request.clone(),
1604        ));
1605
1606        // Event-bus → cells_wake bridges.
1607        //
1608        // SyntaxReparsed: fired by the on_publish callback in every
1609        // SyntaxHandle after a snapshot is published. Wakes the cells
1610        // worker so a fresh display matrix is built once tree-sitter
1611        // finishes — without this, a reparse that completes with no
1612        // keystroke in flight doesn't repaint.
1613        {
1614            use tokio::sync::mpsc;
1615            let (tx, mut rx) = mpsc::unbounded_channel::<crate::events::SyntaxReparsed>();
1616            event_bus.subscribe_typed::<crate::events::SyntaxReparsed>(tx);
1617            let cw = cells_wake.clone();
1618            runtime_handle.spawn(async move {
1619                while rx.recv().await.is_some() {
1620                    cw.0.notify_one();
1621                }
1622            });
1623        }
1624
1625        // BC.7 (2026-06-24): the `MultibufferExcerptsReady` →
1626        // `async_landed` wake forwarder moved into
1627        // `lattice_multibuffer::install(boot)` as `boot.wake_on_event::<…>()`
1628        // (the wake is now baked into the primitive). Behaviour unchanged:
1629        // each appended batch fires `async_landed` so the actor republishes
1630        // render state (picking up the new excerpt_syntax entries) and the
1631        // `AsyncRenderStatePublished` → cells bridge below wakes `cells_wake`
1632        // AFTER the ArcSwap store — same ordering, no race.
1633
1634        // L1c: wake the render pipeline when render-relevant LSP
1635        // events arrive off-keystroke. Without this, `$/progress` and
1636        // the `*/refresh` notifications only reach the screen on the
1637        // next keypress (their drains run inside `run_tick_pending`), so
1638        // indexing progress accumulates then batch-drains to empty and
1639        // is never seen, and a refresh that lands while idle doesn't
1640        // repaint. Mirrors the SyntaxReparsed / MultibufferExcerptsReady
1641        // forwarders: dedicated subscriptions whose only job is to fire
1642        // `async_landed`; the existing per-type drain channels still do
1643        // the accumulation. See lsp-architecture.md §12,
1644        // slice-plans/lsp.md L1c.
1645        {
1646            use tokio::sync::mpsc;
1647            async fn wake_on<T: Send + 'static>(
1648                mut rx: mpsc::UnboundedReceiver<T>,
1649                al: std::sync::Arc<tokio::sync::Notify>,
1650            ) {
1651                while rx.recv().await.is_some() {
1652                    al.notify_one();
1653                }
1654            }
1655            // ML.3c: the `$/progress` + `serverStatus` wake forwarders
1656            // are gone — those events now reach the screen through the
1657            // `lattice_lsp::modeline` forwarder, which folds them and
1658            // publishes `ModelineElementUpdate`; the modeline wake below
1659            // fires `async_landed` for that push.
1660            // BC.8a: the four `workspace/*/refresh` wake forwarders
1661            // (`LspInlayHintRefresh` / `LspSemanticTokensRefresh` /
1662            // `LspDiagnosticRefresh` / `LspCodeLensRefresh`) moved into
1663            // `lattice_lsp::install(boot)` as `boot.wake_on_event::<E>()`
1664            // (byte-identical: subscribe-typed + spawn a notify task). The
1665            // per-type drain channels (host-side `pending_*_refresh_rx`) still
1666            // do the cache-eviction in `run_tick_pending` — those stay here.
1667            // ML.3: a pushed modeline-element content update repaints
1668            // off-keystroke. Same shape as the LSP forwarders above: a
1669            // dedicated subscription whose only job is to fire
1670            // `async_landed`; `drain_modeline_element_updates` (its own
1671            // channel) does the accumulation in `run_tick_pending`.
1672            let (ml_tx, ml_rx) = mpsc::unbounded_channel::<lattice_mode::ModelineElementUpdate>();
1673            event_bus.subscribe_typed(ml_tx);
1674            runtime_handle.spawn(wake_on(ml_rx, async_landed.clone()));
1675        }
1676
1677        // AsyncRenderStatePublished: fired by the actor after every
1678        // publish_render_state triggered by the async_landed arm.
1679        // Wakes cells_wake so the cells worker reads the freshly-
1680        // written PaneCellsInputs. Ordering guarantee: the event is
1681        // published after the ArcSwap store, so cells always sees
1682        // fresh state. This replaces the racy direct notify_one that
1683        // previously fired from the async event handlers.
1684        {
1685            use tokio::sync::mpsc;
1686            let (tx, mut rx) =
1687                mpsc::unbounded_channel::<crate::events::AsyncRenderStatePublished>();
1688            event_bus.subscribe_typed::<crate::events::AsyncRenderStatePublished>(tx);
1689            let cw = cells_wake.clone();
1690            runtime_handle.spawn(async move {
1691                while rx.recv().await.is_some() {
1692                    cw.0.notify_one();
1693                }
1694            });
1695        }
1696
1697        // LA.2: `LanguagesRegistered` — a plugin load changed the mode/language
1698        // catalog, so buffers opened against the old one need their major mode
1699        // and language re-resolved (`mode-architecture.md` §7.4).
1700        //
1701        // TWO subscriptions on purpose, the shape the modeline-element (ML.3)
1702        // and LSP-refresh forwarders already use: one channel the Editor drains
1703        // in `run_tick_pending`, and one whose only job is to fire
1704        // `async_landed`. Without the second, the re-resolution would sit
1705        // untouched until the user happened to press a key — the symptom reads
1706        // as a rendering bug and is exactly the failure mode
1707        // `boot-composition.md` §3 exists to design out.
1708        let catalog_change_rx = {
1709            use tokio::sync::mpsc;
1710            let (tx, rx) = mpsc::unbounded_channel::<lattice_plugin_loader::LanguagesRegistered>();
1711            event_bus.subscribe_typed::<lattice_plugin_loader::LanguagesRegistered>(tx);
1712            let (wake_tx, mut wake_rx) =
1713                mpsc::unbounded_channel::<lattice_plugin_loader::LanguagesRegistered>();
1714            event_bus.subscribe_typed::<lattice_plugin_loader::LanguagesRegistered>(wake_tx);
1715            let al = async_landed.clone();
1716            runtime_handle.spawn(async move {
1717                while wake_rx.recv().await.is_some() {
1718                    al.notify_one();
1719                }
1720            });
1721            rx
1722        };
1723
1724        // D.3.a.1 (2026-05-29): bind the diff subsystem to the
1725        // event bus. The drainer task subscribes to
1726        // DocumentChanged + DocumentClosed and routes through
1727        // the per-session debouncer. The guard's `Drop`
1728        // unsubscribes + aborts the drainer when the editor
1729        // tears down. `_enter` lets us spawn the drainer task
1730        // onto `runtime_handle` even though `bind` uses
1731        // `tokio::spawn`.
1732        let diff_subsystem: std::sync::Arc<crate::diff::subsystem::DiffSubsystem> =
1733            std::sync::Arc::default();
1734        // D.5.a (2026-05-30): the subsystem owns its diff-mode
1735        // bridge via `Default`. Editor accesses it through
1736        // `diff_subsystem.mode_bridge()` during the dispatch
1737        // tail (`apply_pending_diff_mode_changes`); no separate
1738        // wiring step required.
1739        let diff_subscription_guard = {
1740            let _enter = runtime_handle.enter();
1741            let resolver: std::sync::Arc<dyn crate::diff::subsystem::DocumentBufferResolver> =
1742                std::sync::Arc::new(crate::diff::subsystem::BufferRegistryDocumentResolver::new(
1743                    buffers.clone(),
1744                ));
1745            diff_subsystem.bind(event_bus.clone(), resolver)
1746        };
1747        let diff_forwarders: std::sync::Arc<
1748            std::sync::Mutex<
1749                std::collections::HashMap<lattice_core::BufferId, tokio::task::JoinHandle<()>>,
1750            >,
1751        > = std::sync::Arc::default();
1752
1753        // VCS.2 (2026-07-25): auto-inline-diff against git HEAD.
1754        // Subscribes to DocumentOpened / DocumentClosed and
1755        // auto-registers a DiffSession with GitBaseline(HEAD)
1756        // for files inside git repos, producing immediate gutter
1757        // signs. Gated by `git.auto-head-diff` (default true).
1758        let vcs_subsystem = std::sync::Arc::new(crate::vcs::VcsSubsystem::new());
1759        let vcs_subscription_guard = {
1760            let _enter = runtime_handle.enter();
1761            let resolver: std::sync::Arc<dyn crate::diff::subsystem::DocumentBufferResolver> =
1762                std::sync::Arc::new(crate::diff::subsystem::BufferRegistryDocumentResolver::new(
1763                    buffers.clone(),
1764                ));
1765            vcs_subsystem.bind(
1766                event_bus.clone(),
1767                diff_subsystem.clone(),
1768                config.clone(),
1769                resolver,
1770            )
1771        };
1772
1773        // M.7: pre-create shared fold registry so `FoldOverlayServiceImpl`
1774        // and the `fold_registry:` field both point at the same Arc.
1775        let fold_registry = std::sync::Arc::new(std::sync::Mutex::new(
1776            crate::fold_provider::FoldRegistry::with_builtins(),
1777        ));
1778
1779        // SN.3b: fold the loaded `snippet.activation` /
1780        // `snippet.languages` config into the shared policy cell so
1781        // the `snippet-mode` gate resolves with the user's settings
1782        // from the first buffer onward. Defaults (`global` / empty)
1783        // reproduce the pre-SN.3b Global behavior. Re-folded live on
1784        // `:set` via `apply_option_cascade`.
1785        {
1786            let activation = config
1787                .get_typed::<lattice_snippet::SnippetActivation>()
1788                .map(|v| *v)
1789                .unwrap_or_default();
1790            let languages = config
1791                .get_typed::<lattice_snippet::SnippetLanguages>()
1792                .map(|v| (*v).clone())
1793                .unwrap_or_default();
1794            snippet_activation_policy.store(std::sync::Arc::new(
1795                lattice_snippet::fold_activation_policy(activation, &languages),
1796            ));
1797        }
1798
1799        // SN.3c.0: the shared action-handler registry. Created here
1800        // (not inside the `services:` block below) so the boot walk
1801        // can register modes' declarative *global* action handlers
1802        // (`Mode::action_handlers()`) and the resulting app-lifetime
1803        // tokens land on `Editor.global_action_handler_regs`. The
1804        // same Arc is registered as a service so per-buffer handlers
1805        // still register from `on_activate`. See
1806        // `mode_action_handlers::register_mode_action_handlers`.
1807        let action_handlers: lattice_mode::ActionHandlerRegistryHandle =
1808            Arc::new(lattice_mode::ActionHandlerRegistry::new());
1809        let global_action_handler_regs = crate::mode_action_handlers::register_mode_action_handlers(
1810            &action_handlers,
1811            &mode_registry.load(),
1812            &registry.load(),
1813        );
1814
1815        // IDE-protocol I1.1 / BC.3a: the per-tick drain-closure registry is a
1816        // Phase-A primitive (`tick_callbacks`, created up top, owned by
1817        // `boot`). A single Arc spans the editor's lifetime; registered as a
1818        // service so modes add their drain from `on_activate` (e.g. the Claude
1819        // Code IDE peer's `IdeInbound` drain, I3). The host runs every
1820        // registered closure once per tick inside `run_tick_pending`
1821        // (`drain_tick_callbacks`) and applies the returned `Effect`s.
1822
1823        // T.3/T.4 (theme-system): the builtin element ids, captured from
1824        // the theme-element registry created earlier (above the picker
1825        // registry so the T.12a colorscheme picker could capture a
1826        // clone). The registry is registered into `services` below + held
1827        // in the `theme_registry` field. See theme-system.md §3.5 / §7.
1828        let builtin_element_ids =
1829            lattice_theme::BuiltinElementIds::capture(theme_registry.as_ref());
1830        // SG.4a: assigned where the built-in signs are registered, below.
1831        // Declared without an initialiser on purpose — definite-assignment
1832        // then makes it a compile error to add a path that registers the
1833        // signs but forgets their ids, which a `Default::default()` seed would
1834        // have turned into a silently empty gutter.
1835        let builtin_sign_ids: lattice_mode::BuiltinSignIds;
1836
1837        // ML.0b-2: one ModelineService instance, shared three ways —
1838        // registered into `services` (modes reach it via
1839        // `ctx.service::<ModelineServiceHandle>()`), stashed on
1840        // `Editor.modeline` (host built-ins write content + the publish
1841        // path snapshots it), and thus read wait-free by the renderers.
1842        let modeline_service: lattice_mode::ModelineServiceHandle = std::sync::Arc::default();
1843        // ML.1a-render: register the host's built-in descriptors
1844        // (`core.mode` / `core.path` / `core.position` / `core.lang`).
1845        // Content is resolved per-pane host-side at render time
1846        // (`crate::modeline::resolve_builtin_content`) so both renderers
1847        // paint identical content.
1848        crate::modeline::register_builtin_elements(&modeline_service);
1849        // ML.3b: the diff subsystem owns its `diff` element descriptor.
1850        // Content is pushed by the actor's `sync_diff_modeline_element`.
1851        crate::diff::mode::register_diff_modeline_element(&modeline_service);
1852        // ML.3c: lattice-lsp owns the `lsp` element. Its forwarder folds
1853        // `$/progress` + `serverStatus` into the shared `LspProgressStore`
1854        // and pushes the badge per attached buffer; the host keeps the
1855        // store handle for `:lsp-progress-cancel`.
1856        lattice_lsp::modeline::register_lsp_modeline_element(&modeline_service);
1857        let lsp_progress_store: lattice_lsp::modeline::LspProgressStoreHandle =
1858            std::sync::Arc::default();
1859        lattice_lsp::modeline::spawn_modeline_forwarder(
1860            event_bus.clone(),
1861            lsp_progress_store.clone(),
1862            &runtime_handle,
1863        );
1864
1865        // ── BC.3a: service registration through `boot` ───────────────────
1866        // Hoisted out of the former `services: { … }` field in the `Editor`
1867        // literal so each registration runs through `boot.register_service()`
1868        // / `boot.services_mut()` (decision 2-b). `freeze_service_registry`
1869        // hands back the shared `Arc<ServiceRegistry>` the literal seats.
1870        // Registration order preserved verbatim from the old block.
1871        // BC.8a: `boot.register_service(lsp.clone())` moved up to Phase A
1872        // (right after `build_lsp_subsystem`) so `lattice_lsp::install` can read
1873        // the supervisor handle for `lsp-completion-mode` registration.
1874        // ML.0b-2: same Arc as `Editor.modeline` below, so modes
1875        // register/update the instance the renderer snapshots.
1876        boot.register_service(modeline_service.clone());
1877        // M.6.cd.1 (2026-07-16): CurrentDirHandle — shared current
1878        // working directory for mode-owned handlers (e.g. search
1879        // `gr` refresh) to re-resolve scan roots after `:cd`.
1880        // Registered as an `Arc<Mutex<Option<PathBuf>>>` under the
1881        // typed alias so the `ServiceRegistry` Arc/TypeId rule holds.
1882        boot.register_service(
1883            std::sync::Arc::<std::sync::Mutex<Option<std::path::PathBuf>>>::new(
1884                std::sync::Mutex::new(std::env::current_dir().ok()),
1885            ),
1886        );
1887        // T-mode-1 (2026-05-27): TerminalStoreHandle so `TerminalNormalMode`
1888        // can install / clear the SyntheticDoc on a TerminalBuffer from its
1889        // lifecycle hooks. Same `BufferRegistry` backs both stores — cheap
1890        // clone (Arc inside). BC.4: this is a HOST-PUBLISHED primitive (the host
1891        // `BufferRegistry` exposed under `dyn TerminalStore`), so it stays here
1892        // — not in `lattice_terminal::install` — sibling to `buffer_store` /
1893        // `diagnostics`. Terminal owning it would need `TerminalStore` impl'd
1894        // over `BufferStoreHandle` (a terminal-crate slice).
1895        let term_store: Arc<dyn lattice_terminal::TerminalStore> = Arc::new(buffers.clone());
1896        boot.register_service(lattice_terminal::TerminalStoreHandle::new(term_store));
1897        // BC.3a: the generic buffer-store is a Phase-A handle (`boot` already
1898        // holds a clone via `BootContext::new`); register the clone for mode
1899        // lookups via `services().get::<BufferStoreHandle>()`.
1900        boot.register_service(buffer_store_handle.clone());
1901        // CG.2: same Arc the Editor holds — see its construction above.
1902        // Registered under `ForegroundCancelHandle` per the
1903        // ServiceRegistry Arc/TypeId rule, which is the type every
1904        // provider looks it up by.
1905        boot.register_service::<lattice_mode::ForegroundCancelHandle>(foreground_cancel.clone());
1906        // CB.0 (clipboard.md): default clipboard backing. `FakeClipboard` is
1907        // the safe default (no OS resource, no display dependency); the TUI
1908        // peer (CB.2 — `arboard` + OSC52 fallback) and the GPUI peer (CB.4 —
1909        // gpui-native) override this with a real backend at renderer boot.
1910        // Registered under `ClipboardHandle` per the ServiceRegistry Arc/TypeId
1911        // rule so the host register layer (CB.1) AND `terminal-mode` (CB.3)
1912        // look it up by that exact type.
1913        let clipboard: lattice_core::ClipboardHandle = Arc::new(lattice_core::FakeClipboard::new());
1914        boot.register_service(clipboard);
1915        boot.register_service(lsp_logger.clone());
1916        // PI.3/PI.4: the (initially empty) plugin-id → metadata map (name +
1917        // the plugin's own doc). The Phase-8 plugin loader populates it via
1918        // `Editor::register_plugin`; provenance (`:list-commands`) reads the
1919        // name, `:describe-plugin` / `:list-plugins` read the full metadata.
1920        boot.register_service(crate::dispatch::PluginMetaRegistry::default());
1921        // PL8.B: expose the SAME meta registry as a `PluginMetaSinkHandle` so
1922        // the plugin loader can write provenance (name/doc) as each plugin loads
1923        // without naming this host type. `service` returns the Arc just
1924        // registered; coerce it to the trait object (same instance the host's
1925        // `register_plugin` / `plugin_meta` read through).
1926        if let Some(meta) = boot.service::<crate::dispatch::PluginMetaRegistry>() {
1927            // `gen:plugins` — registered HERE rather than beside the other
1928            // generators above, because it needs the Arc the line above just
1929            // created. The completion registry stays mutable until it is moved
1930            // into the Editor, so ordering costs nothing; reaching backwards
1931            // for the service would have meant moving this registration up
1932            // through a boot sequence whose order is load-bearing elsewhere.
1933            completion_registry.register_generator(
1934                "gen:plugins",
1935                "Every loaded plugin (`:describe-plugin`, `:plugin-unload`, …).",
1936                crate::host_generators::PluginsGenerator { meta: meta.clone() },
1937            );
1938            let sink: lattice_mode::PluginMetaSinkHandle = meta;
1939            boot.register_service::<lattice_mode::PluginMetaSinkHandle>(sink);
1940        }
1941
1942        // L4b (lsp-architecture.md §15): the diagnostics-query service
1943        // (`lsp-diagnostics-mode`'s `gl` handler + claude-code's read tools read
1944        // it) is registered in Phase A above, so a subsystem `install(boot)` can
1945        // reach it via `boot.service::<DiagnosticsQueryHandle>()`. Not re-registered here.
1946        // (BC.3b: the claude-code `install_services` read/write wiring moved into
1947        // `lattice_claude_code::install` in the Phase-B list above.)
1948        // BC.7 (2026-06-24): the `MultibufferRegistryHandle` + the
1949        // project-search services moved into `lattice_multibuffer::install(boot)`
1950        // (registered via `boot.register_service` / `boot.services_mut()`
1951        // there). The host still reads the registry handle back via
1952        // `services.get::<MultibufferRegistryHandle>()` at dispatch time
1953        // (`resolve_narrow_target`).
1954        // M.4 (2026-06-01): expose the EventBus as a generic Phase-A primitive —
1955        // multibuffer views subscribe to source events + publish typed events
1956        // (`MultibufferSourceClosed`, `MultibufferHeaderlineChanged`) via
1957        // `services().get::<EventBus>()`, and other subsystems consume it too.
1958        // Host-owned (not multibuffer-owned), so it stays here.
1959        boot.register_service(event_bus.clone());
1960        // M.10.1.b (2026-06-03): action-handler registry — mode-contributed
1961        // chord/ex-command handler closures. Modes register from `on_activate`
1962        // via `ctx.service::<ActionHandlerRegistryHandle>()`; one Arc serves
1963        // every activation. SN.3c.0: reuse the Arc created above (after the
1964        // boot action-handler walk). See `mode-architecture.md` §5.3 +
1965        // `feedback_mode_owns_its_surface`.
1966        boot.register_service::<lattice_mode::ActionHandlerRegistryHandle>(action_handlers.clone());
1967        // IDE-protocol I1.1: per-tick drain-closure registry. Read each tick by
1968        // `Editor::drain_tick_callbacks`; written by modes' `on_activate` via
1969        // `ctx.service::<TickCallbackRegistryHandle>()`.
1970        boot.register_service::<lattice_mode::TickCallbackRegistryHandle>(tick_callbacks.clone());
1971        // WK.3: the idle-gate registry's peer registration. The actor reads
1972        // it through `Editor::idle_gate_deadline` / `fire_idle_gates`, which
1973        // look it up here by the same `T` they were registered under (per the
1974        // ServiceRegistry Arc/TypeId rule).
1975        boot.register_service::<lattice_mode::idle_gate::IdleGateRegistryHandle>(
1976            idle_gates.clone(),
1977        );
1978        // MG.2: shared map for pending synthetic-buffer highlight spans
1979        // (magit status, etc.). The async refresh writes per-line StyledSpan
1980        // entries here; the Editor drains them in run_tick_pending to set
1981        // ExtraHighlights on each buffer.
1982        // MG.2: register the PendingSyntheticHighlights directly (the
1983        // ServiceRegistry wraps it in Arc automatically, and get::<T>()
1984        // returns Option<Arc<T>>).
1985        boot.register_service::<lattice_mode::PendingSyntheticHighlights>(
1986            lattice_mode::PendingSyntheticHighlights::new(),
1987        );
1988        // DL.3b: the inlay peer — mode-published inline virtual text
1989        // (listing icons). Drained into each buffer's `ExtraInlays`
1990        // local in the same tick as the highlights above.
1991        boot.register_service::<lattice_mode::PendingInlays>(lattice_mode::PendingInlays::new());
1992
1993        // M.10.3 (2026-06-03): expose the CommandRegistry as a service so mode
1994        // handlers (registered via M.10.1.b ActionHandlerRegistry) can look up
1995        // CommandIds by action name at `on_activate` time — e.g.
1996        // `cmd_registry.id_by_name("action:search-refresh")` — without
1997        // depending on host-internal types. Same `Arc<X>` alias pattern.
1998        boot.register_service::<lattice_grammar::CommandRegistryHandle>(registry.clone());
1999        // PL8.B (drain_mode): expose the keymap handle so `lattice-plugin-loader`
2000        // can pass it to `spawn_mode_plugin` — a mode plugin's per-mode
2001        // `MinorMode` keymap bindings land in it. `KeymapHandle` is Arc-backed +
2002        // `Clone` with interior-mutable (mutex+ArcSwap) writes, so the loader's
2003        // captured clone shares the one live registry (bindings are immediately
2004        // visible). Registered as `KeymapHandle` (already a shareable handle — no
2005        // `Arc<X>` wrapper needed); the loader looks it up under the same type.
2006        boot.register_service::<crate::keymap_registry::KeymapHandle>(keymap_handle.clone());
2007        // CM.2: the operator-chord wirer, beside the keymap handle it writes
2008        // through and for the same ordering reason — `lattice_plugin_loader::
2009        // install` below captures its services once, so a handle registered
2010        // after it contributes nothing. That failure is loud rather than
2011        // silent (`PluginLoaderError::NotWired`), but it should not happen at
2012        // all. `builtins` / `syntax_*_ids` are resolved far above; the
2013        // composition needs all three, which is why the loader cannot do this
2014        // itself.
2015        let chord_wirer: lattice_mode::OperatorChordWirerHandle =
2016            std::sync::Arc::new(crate::operator_chord_wirer::HostOperatorChordWirer::new(
2017                keymap_handle.clone(),
2018                builtins,
2019                syntax_textobject_ids,
2020                syntax_motion_ids,
2021            ));
2022        boot.register_service::<lattice_mode::OperatorChordWirerHandle>(chord_wirer);
2023        // WK.6: the other half of which-key's install. Must follow the two
2024        // registrations above: the gate handler resolves the keymap (to fold
2025        // the composite the dispatcher walks) and the command registry (rungs
2026        // 2-3 of the label chain) at popup-build time, and an install-time
2027        // `None` for either would degrade every label to `<unbound>` without
2028        // failing anything — the silent no-op this ordering comment exists to
2029        // prevent.
2030        lattice_mode::modes::wire_which_key(&mut boot, which_key_grid);
2031
2032        // OM.4b: after a plugin loads, give any motion / text object it
2033        // contributed its operator-pending rows.
2034        //
2035        // `bind_mode_keymap` (two crates down, in `lattice-plugin-host`) binds
2036        // a mode's declared chords in Normal and stops. That is right for an
2037        // action and useless for the other two kinds: `dar` and `d]]` are
2038        // bound as explicit paths, expanded across every operator, and for
2039        // builtins that expansion comes from hardcoded tables a plugin cannot
2040        // reach. So a plugin text object was unreachable entirely and a plugin
2041        // motion unreachable after an operator.
2042        //
2043        // It runs HERE because the expansion needs `Builtins` — the
2044        // host-resolved operator ids — which live downstream of the plugin
2045        // host. Pushing them down would leak the operator vocabulary two
2046        // crates and add another service that can be left unwired. This
2047        // framing is also the honest one: the host applies its universal
2048        // operator vocabulary to a contribution, exactly as it does for
2049        // builtins.
2050        {
2051            let (grammar_tx, mut grammar_rx) = tokio::sync::mpsc::unbounded_channel();
2052            event_bus.subscribe(
2053                lattice_runtime::EventFilter::kind(lattice_protocol::EventKind::PluginLoaded),
2054                lattice_runtime::SubscriptionTarget::Channel(grammar_tx),
2055            );
2056            let expand_keymap = keymap_handle.clone();
2057            let expand_commands = registry.clone();
2058            let expand_modes = mode_registry.clone();
2059            let expand_builtins = builtins;
2060            let expand_wake = Arc::clone(&async_landed);
2061            boot.runtime_handle().spawn(async move {
2062                while grammar_rx.recv().await.is_some() {
2063                    // Every registered mode, not only the loading plugin's:
2064                    // the event carries no mode ids, the pass is idempotent,
2065                    // and re-walking is immune to a plugin whose modes landed
2066                    // under another's provenance. One walk of each layer's
2067                    // Normal trie, on plugin load only — never a keystroke.
2068                    let commands = expand_commands.load();
2069                    for (mode_id, kind) in expand_modes.load().iter_meta() {
2070                        let layer = match kind {
2071                            lattice_mode::ModeKind::Major => {
2072                                lattice_keymap::KeymapLayer::MajorMode(mode_id)
2073                            }
2074                            lattice_mode::ModeKind::Minor => {
2075                                lattice_keymap::KeymapLayer::MinorMode(mode_id)
2076                            }
2077                        };
2078                        crate::keymap_normal::expand_grammar_rows(
2079                            &expand_keymap,
2080                            &commands,
2081                            &expand_builtins,
2082                            layer,
2083                        );
2084                    }
2085                    // The rows change what a keystroke resolves to, so the
2086                    // screen must not wait for the next keypress to notice.
2087                    expand_wake.notify_one();
2088                }
2089            });
2090        }
2091
2092        // Phase 8 (PL8.A/B): the plugin loader. Stands the wasmtime runtime up,
2093        // registers the `PluginLoaderHandle` service, and spawns on-disk
2094        // discovery off the boot thread. First subsystem whose install pulls the
2095        // plugin *runtime* into the editor (the host was wasmtime-free through
2096        // Phase 7 — only the `lattice-plugin-api` catalog). **Seated last among
2097        // its service dependencies**, which is why it lives here and not in the
2098        // Phase-B install list: the drains capture the picker (~L893) + meta sink
2099        // + mode registry + config + the `CommandRegistryHandle` (drain_grammar,
2100        // just above) + the `KeymapHandle` (drain_mode, just above), so it MUST
2101        // follow every one of those `register_service` calls. A boot-ordering
2102        // regression (moving this earlier) would silently degrade grammar/mode
2103        // plugin loading to a `NotWired` skip — the boot pin
2104        // `plugin_loader_captures_every_drain_service` guards against exactly
2105        // that. A host that fails to build degrades to no-plugin-support, logged,
2106        // T.3/T.4 (theme-system): register the theme-element registry (created
2107        // above) so modes + renderers look it up via
2108        // `services().get::<ThemeRegistryHandle>()`. Register + look up the
2109        // SAME `Arc<dyn ThemeRegistry>` per the ServiceRegistry Arc/TypeId rule
2110        // (`feedback_servicesregistry_arc_typeid`). Renderers read the resolved
2111        // table via the `RenderState` snapshot (T.4); modes intern their own
2112        // `ElementId`s from `on_activate` (T.7).
2113        //
2114        // TC.4: registered BEFORE `lattice_plugin_loader::install` below,
2115        // because the loader captures its drain services at install time — a
2116        // `theme` plugin's elements have nowhere to land otherwise, and the
2117        // `plugin_loader_captures_every_drain_service` boot pin is what caught
2118        // the original ordering.
2119        boot.register_service::<lattice_theme::ThemeRegistryHandle>(theme_registry);
2120        // SG.2b: the sign-definition registry, registered on the same
2121        // alias it is looked up on (the ServiceRegistry Arc/TypeId
2122        // rule). Registered here rather than beside `mode_registry` on
2123        // `Editor` because a sign definition is written by whoever
2124        // places signs — a mode, a provider, a plugin's install — and
2125        // read by the publish path; nothing about it belongs to the
2126        // dispatcher. Empty until a producer defines one, which is
2127        // what a mechanism with no built-in meanings looks like at
2128        // rest — except for the built-ins below, which are the mechanism
2129        // being used by the host itself rather than a special case in it.
2130        //
2131        // SG.4a: diagnostics and diff marks are signs now. They are
2132        // registered here, into the same registry a plugin writes, so the
2133        // host has no privileged gutter path left: what used to be two
2134        // hardcoded columns is two producers naming what they mean.
2135        let sign_registry: lattice_mode::SignRegistryHandle = {
2136            let mut registry = lattice_mode::SignRegistry::new();
2137            let ids = lattice_mode::register_builtin_signs(
2138                &mut registry,
2139                diagnostic_glyphs_from(&config),
2140            );
2141            builtin_sign_ids = ids;
2142            std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(registry))
2143        };
2144        boot.register_service::<lattice_mode::SignRegistryHandle>(sign_registry);
2145        // never a failed boot — see `lattice_plugin_loader::install`.
2146        lattice_plugin_loader::install(&mut boot);
2147        // (BC.3b: the `ClaudeCodeServerHandle` service is registered by
2148        // `lattice_claude_code::install` in the Phase-B list above.)
2149        // M.7: expose the fold-overlay service so `MultibufferMode::on_activate`
2150        // can register `ExcerptFoldProvider` without depending on
2151        // `lattice-host`. Same Arc as `fold_registry` above.
2152        let fold_svc: lattice_core::FoldOverlayServiceHandle = Arc::new(
2153            crate::fold_provider::FoldOverlayServiceImpl::new(fold_registry.clone()),
2154        );
2155        boot.register_service::<lattice_core::FoldOverlayServiceHandle>(fold_svc);
2156        // DX.3-C7 (2026-06-24): publish the diff subsystem so
2157        // `DiffMode::on_activate` can look up a buffer's session and
2158        // register its `HunkFoldSource` via the fold service above
2159        // (mode-owned hunk folds, mirroring multibuffer's
2160        // `MultibufferRegistryHandle`). Same `Arc` as the
2161        // `Editor.diff_subsystem` field.
2162        boot.register_service::<crate::diff::subsystem::DiffSubsystemHandle>(
2163            diff_subsystem.clone(),
2164        );
2165        // SN.2: register the live snippet session so `SnippetActiveMode`'s
2166        // `<Tab>`/`<S-Tab>` handlers can reach it from `on_activate`. Same Arc
2167        // as the `Editor.snippet_session` field (set below).
2168        boot.register_service::<lattice_snippet::SnippetSessionHandle>(snippet_session.clone());
2169        // MH.A3 / DB.5: `Arc<ConfigRegistry>` is registered as a Phase-A
2170        // service near `config`'s construction (above, next to `event_bus`)
2171        // rather than here — see the DB.5 hoist note there for why a
2172        // Phase-B `install(&mut boot)` (e.g. `lattice_dashboard::install`)
2173        // needs it to already be registered.
2174        // BC.3a: freeze the service registry into its shared `Arc` (last, after
2175        // the full services block). The `Editor` literal seats it below.
2176        let services = boot.freeze_service_registry();
2177        // BC.3b: consume `boot`, taking the boot-lifetime tick-callback
2178        // registration tokens (e.g. the claude-code inbound drain wired in the
2179        // Phase-B install list). They move onto the `Editor` so the drains live
2180        // for the program rather than being dropped when `boot` drops here.
2181        let boot_tick_registrations = boot.into_registrations();
2182
2183        let mut editor = Editor {
2184            messages: messages_ring.clone(),
2185            pending_message_event_rx: Some(message_event_rx),
2186            option_change_rx: Some(option_change_rx),
2187            pending_catalog_change_rx: Some(catalog_change_rx),
2188            lang_registry: lang_registry.clone(),
2189            syntax,
2190            last_parsed_text_version,
2191            picker_registry: picker_registry.clone(),
2192            // PL8.E: hand the decoration-producer registry to the editor so the
2193            // per-tick refresh (`maybe_refresh_wasm_decorations`) drives loaded
2194            // producers off the render path. The loader registers into the same
2195            // handle via the service registered above.
2196            wasm_decorations: crate::wasm_decorations::WasmDecorationState::with_registry(
2197                decoration_registry.clone(),
2198            )
2199            .with_decoration_epoch(decoration_epoch.clone()),
2200            wasm_media: crate::wasm_media::WasmMediaState::with_registry(media_registry.clone()),
2201            wasm_context: crate::wasm_context::WasmContextState::with_registry(
2202                context_registry.clone(),
2203            ),
2204            picker_mru,
2205            picker_mru_path,
2206            // MH.A3 (2026-06-19): clone so the `services:` block below
2207            // can register the same `Arc<ConfigRegistry>` (read by
2208            // multibuffer's `create_multibuffer_view` for the
2209            // `ui.nerd_fonts` icon-palette default). Field initializers
2210            // run top-down; `config,` would otherwise move the binding
2211            // before `services:` evaluates.
2212            config: config.clone(),
2213            // K.2.4 (2026-06-01): clone the Arc so the
2214            // `keymap: { ... }` block below can still borrow
2215            // `mode_registry` to run the mode-keymap
2216            // translation pass. Field initializers run top-down
2217            // in source order; the original binding would
2218            // otherwise be moved into the struct before
2219            // `keymap:` evaluates.
2220            mode_registry: mode_registry.clone(),
2221            // ML.0b-2: the shared modeline service (same Arc registered
2222            // into `services` below).
2223            modeline: modeline_service.clone(),
2224            // BC.3a: the frozen service registry (registration hoisted above,
2225            // through `boot.register_service` / `boot.services_mut`).
2226            services,
2227            // BC.3b: boot-lifetime tick-callback drain tokens (claude-code's
2228            // inbound write-bus drain today). Held for the editor's lifetime.
2229            _boot_tick_registrations: boot_tick_registrations,
2230            // T.4 (theme-system): builtin ids captured (above) from the
2231            // theme registry, which is registered into `services` for
2232            // the renderer snapshot + mode lookups.
2233            builtin_element_ids,
2234            // SG.4a: interned once at boot so a producer emitting a mark per
2235            // visible line reads a field instead of hashing a name per line.
2236            builtin_sign_ids,
2237            // Perf plan B.4: wrap the seeded HashMap so the
2238            // buffer_locals sub-state cache can detect when no
2239            // mutation has fired between publishes.
2240            buffer_locals: crate::versioned::Versioned::new(buffer_locals),
2241            help_topics,
2242            // K.2.4.A.0.3 (2026-06-02): clone the Arc so the
2243            // `keymap: { ... }` block below can still borrow
2244            // `registry` to pass as the `&CommandRegistry`
2245            // argument that K.2.4.A.0.3 added to
2246            // `translate_mode_keymaps`. Field initializers run
2247            // top-down in source order; the original Arc would
2248            // otherwise be moved into the struct before
2249            // `keymap:` evaluates. Same shape as the
2250            // `mode_registry: mode_registry.clone()` cell
2251            // above.
2252            registry: registry.clone(),
2253            event_bus: event_bus.clone(),
2254            foreground_cancel: foreground_cancel.clone(),
2255            builtins,
2256            action_ids,
2257            keymap: {
2258                // MP.2b: reuse the handle created above so the
2259                // commands picker's reverse-lookup adapter and the
2260                // binding registration below share one registry.
2261                let h = keymap_handle;
2262                crate::keymap_replace::register_replace_bindings(&h, &action_ids);
2263                crate::keymap_visual::register_visual_bindings(
2264                    &h,
2265                    &builtins,
2266                    &action_ids,
2267                    &syntax_textobject_ids,
2268                );
2269                // Select has no binder: it binds no bare printable, and its
2270                // motions come from the keymap's mirror (select-mode.md §4).
2271                crate::keymap_insert::register_insert_bindings(&h, &action_ids);
2272                // CG.1: `<C-g>` → `action:cancel`. Builtin, so it does
2273                // not depend on `:set emacs-keys`.
2274                crate::keymap_cancel::register_cancel_bindings(&h, &action_ids);
2275                crate::keymap_normal::register_normal_bindings(
2276                    &h,
2277                    &builtins,
2278                    &action_ids,
2279                    &syntax_textobject_ids,
2280                    &syntax_motion_ids,
2281                );
2282                // N.1.3 (2026-06-10): wire the narrow `zn` operator
2283                // chord into the universal operator-pending layer.
2284                // `zn{motion|text-object}` narrows that span; `znn`
2285                // narrows the current line. The operator SPEC + apply
2286                // are owned by `lattice-multibuffer::providers::narrow`;
2287                // only this chord-wiring lives host-side (it needs the
2288                // resolved `Builtins`).
2289                //
2290                // BC.7 (2026-06-24, decision A): the SPEC is registered by
2291                // `lattice_multibuffer::install(boot)`; the host resolves its
2292                // `OperatorId` by name here (the K.2.5 motion name-resolution
2293                // pattern) rather than threading the registration return value.
2294                // `operator:narrow` is registered above install, so the lookup
2295                // is infallible at this point.
2296                let narrow_operator_id = lattice_grammar::registry::OperatorId(
2297                    registry
2298                        .load()
2299                        .id_by_name("operator:narrow")
2300                        .expect("operator:narrow registered by lattice_multibuffer::install"),
2301                );
2302                crate::keymap_normal::register_operator_bindings(
2303                    &h,
2304                    &[
2305                        lattice_protocol::chord::ChordPattern::Literal(
2306                            lattice_protocol::chord::KeyChord::char('z'),
2307                        ),
2308                        lattice_protocol::chord::ChordPattern::Literal(
2309                            lattice_protocol::chord::KeyChord::char('n'),
2310                        ),
2311                    ],
2312                    narrow_operator_id,
2313                    Some(lattice_protocol::chord::ChordPattern::Literal(
2314                        lattice_protocol::chord::KeyChord::char('n'),
2315                    )),
2316                    &builtins,
2317                    &syntax_textobject_ids,
2318                    &syntax_motion_ids,
2319                    false,
2320                );
2321                // K.3.2 (2026-06-02): emacs-style <C-h> map at
2322                // KeymapLayer::Builtin (Normal-mode only) —
2323                // <C-h><C-h> / <C-h>? open :help-for-help;
2324                // <C-h>{k,c,o,e,m,b,a,K} route to the
2325                // respective :describe-* / :apropos / :keymap.
2326                // Resolves command names against the registry
2327                // (populated above by ex_commands::populate +
2328                // actions::populate).
2329                crate::keymap_help::register_help_prefix_bindings(&h, &registry.load());
2330                // MO.x (2026-06-24): the diff-mode `do`/`dp` keymap is
2331                // contributed via `DiffMode::keymap()` and pushed by the
2332                // K.2.4 `translate_mode_keymaps` pass below (under
2333                // `MinorMode(diff-mode)`, K.1.c-gated, names resolved against
2334                // the registry) — the bespoke explicit host push is retired.
2335                // The mode now owns its binding choice end-to-end (no
2336                // diff-specific host push remains).
2337                // emacs-keys (S1): push the `<C-x>` leader layer once.
2338                // K.1.c's filter gates the chords to buffers where the
2339                // mode is active. S1b reads the configurable leader prefix
2340                // + enable flag (`emacs-keys-prefix` / `emacs-keys`) from
2341                // config so lattice.toml can rebind or disable the tribute;
2342                // `:set` re-pushes the layer live (see dispatch.rs). `config`
2343                // is still borrowable here (the field above clones it).
2344                // Disabled, or a malformed prefix => empty layer (no panic).
2345                // OM.2b: set what `<leader>` expands to BEFORE anything
2346                // registers a binding. Expansion is bind-time, so a leader
2347                // installed after a subsystem or plugin has bound its chords
2348                // would not reach them — this must be the first thing done to
2349                // the keymap handle, not merely an early one.
2350                let leader = config
2351                    .get_typed::<lattice_config::core_options::KeymapLeader>()
2352                    .map(|v| (*v).clone())
2353                    .unwrap_or_else(|| lattice_keymap::DEFAULT_LEADER.to_string());
2354                h.set_leader(&leader);
2355
2356                let emacs_keys_enabled = config
2357                    .get_typed::<lattice_config::core_options::EmacsKeys>()
2358                    .map(|v| *v)
2359                    .unwrap_or(true);
2360                let emacs_keys_prefix = config
2361                    .get_typed::<lattice_config::core_options::EmacsKeysPrefix>()
2362                    .map(|v| (*v).clone())
2363                    .unwrap_or_else(|| "<C-x>".to_string());
2364                h.push_layer(
2365                    crate::keymap_registry::PushLayerKind::MinorMode(
2366                        lattice_mode::EmacsKeysMode::mode_id(),
2367                    ),
2368                    "emacs-keys-mode",
2369                    lattice_mode::emacs_keys_layer_bindings(
2370                        emacs_keys_enabled,
2371                        &emacs_keys_prefix,
2372                        &registry.load(),
2373                    ),
2374                );
2375                // K.2.5 (2026-06-02): explicit push_layer calls
2376                // for `multibuffer-mode` and
2377                // `project-search-mode` retired.
2378                // Their bindings now flow through
2379                // `MultibufferMode::keymap()` and
2380                // `ProjectSearchMode::keymap()` via
2381                // the K.2.4 translation pass below — host glue
2382                // no longer needs to know about them. (Diff
2383                // mode's bindings still go through the explicit
2384                // path above; migrating them is a separate
2385                // MO.x slice tracked under
2386                // `mode-ownership-cleanup.md`.)
2387
2388                // SU.4: register `ys{motion}{char}` operator-pending
2389                // bindings. The `post_motion_char: true` flag appends
2390                // `ChordPattern::CharLiteral` to every motion/text-object
2391                // path so the wrapping character is captured as
2392                // `Args::Char` by the wildcard resolution.
2393                crate::keymap_normal::register_operator_bindings(
2394                    &h,
2395                    &[
2396                        lattice_protocol::chord::ChordPattern::Literal(
2397                            lattice_protocol::chord::KeyChord::char('y'),
2398                        ),
2399                        lattice_protocol::chord::ChordPattern::Literal(
2400                            lattice_protocol::chord::KeyChord::char('s'),
2401                        ),
2402                    ],
2403                    surround_operators.add,
2404                    Some(lattice_protocol::chord::ChordPattern::Literal(
2405                        lattice_protocol::chord::KeyChord::char('s'),
2406                    )),
2407                    &builtins,
2408                    &syntax_textobject_ids,
2409                    &syntax_motion_ids,
2410                    true, // post_motion_char
2411                );
2412
2413                // K.2.4 (2026-06-01): translate every registered
2414                // mode's `Mode::keymap()` contribution into a
2415                // `MinorMode(mode_id)` layer on `h`. Today most
2416                // modes still return `Keymap::default()` (the empty
2417                // contribution is skipped); K.2.5 promotes the
2418                // multibuffer + project-search bindings into their
2419                // owning mode crates so this pass becomes
2420                // load-bearing for them, and the explicit
2421                // `push_layer` calls above for those modes retire
2422                // in the same slice. The pass is idempotent on
2423                // `mode_id`: re-pushing replaces the layer rather
2424                // than minting a sibling (K.1.b).
2425                crate::keymap_mode_contributions::translate_mode_keymaps(
2426                    &h,
2427                    &mode_registry.load(),
2428                    &registry.load(),
2429                );
2430                // VM.1 (2026-09-15): derive every OPERATOR-PENDING row a
2431                // binding implies but nobody wrote (`dgg`, `d]]`, `dar`), plus a
2432                // text object's Visual row. A motion's Visual / Select rows come
2433                // from the keymap itself since VM.4. Runs
2434                // LAST, over the Builtin layer and over every mode layer
2435                // registered so far, because it fills gaps and must see the
2436                // deliberate bindings first: `keymap_visual`'s `x` / `s` / `r`
2437                // aliases and the find-char paths' `Args::Char` routing are
2438                // explicit statements the derivation must not clobber.
2439                //
2440                // This is what makes `dgg`, `d<C-d>` and org's `d]]` work. The
2441                // Visual half (`vgg`, `vf)`, org's `[[` in Visual) is the
2442                // keymap's mirror, armed by `set_command_registry` where the
2443                // handle is created. Plugin layers are re-walked on
2444                // `PluginLoaded` by the subscriber spawned above.
2445                {
2446                    let cmds = registry.load();
2447                    crate::keymap_normal::expand_grammar_rows(
2448                        &h,
2449                        &cmds,
2450                        &builtins,
2451                        lattice_keymap::KeymapLayer::Builtin,
2452                    );
2453                    for (mode_id, kind) in mode_registry.load().iter_meta() {
2454                        let layer = match kind {
2455                            lattice_mode::ModeKind::Major => {
2456                                lattice_keymap::KeymapLayer::MajorMode(mode_id)
2457                            }
2458                            lattice_mode::ModeKind::Minor => {
2459                                lattice_keymap::KeymapLayer::MinorMode(mode_id)
2460                            }
2461                        };
2462                        crate::keymap_normal::expand_grammar_rows(&h, &cmds, &builtins, layer);
2463                    }
2464                }
2465                // MARG.2 (2026-06-03): now that every layer's
2466                // bindings are registered, the reverse cache
2467                // reflects the full Normal-mode keymap. Build
2468                // the keybinding annotator against the
2469                // registry's reverse-cache adapter and
2470                // register it into the completion pipeline so
2471                // command-completion candidates surface their
2472                // chord. Subsequent `:map` / `:unmap` rebuild
2473                // the cache automatically (see the
2474                // `rebuild_reverse_cache` call in every
2475                // KeymapRegistry mutation site); the
2476                // annotator references the cache through an
2477                // `Arc<ArcSwap<_>>` so it always reads the
2478                // current snapshot. See
2479                // `docs/dev/architecture/marginalia.md` §6.
2480                let kb_anno = lattice_completion::KeybindingAnnotator::new(
2481                    crate::keymap_registry::KeymapReverseLookupHandle::new(&h, registry.clone()),
2482                );
2483                let kb_anno_id = completion_registry.register_annotator(
2484                    "anno:keybinding",
2485                    "Append the chord(s) bound to a command in Normal mode (e.g. `<C-w>v` next to `:split-pane-vertical`).",
2486                    kb_anno,
2487                );
2488                // 2026-06-03 placement fix: insert at position
2489                // 0 so the keybinding renders LEFTMOST in the
2490                // annotation column — immediately to the right
2491                // of the command name where the user's eye
2492                // already is. The previous `.push(...)` placed
2493                // it last, after kind + doc snippet; user
2494                // reported it was "too far away to notice."
2495                // Column alignment for the kind / doc labels
2496                // is sacrificed (keybinding is inherently
2497                // variable-width) but proximity to the command
2498                // is the higher-value scan affordance.
2499                completion_registry.default_annotators.insert(0, kb_anno_id);
2500                h
2501            },
2502            completion_registry,
2503            completion_state: None,
2504            // Perf plan B.4: wrap in `Versioned` so per-publish
2505            // identity tracking starts at version 0; subsequent
2506            // pane-tree mutations bump it via `DerefMut`.
2507            pane_tree: crate::versioned::Versioned::new(pane_tree),
2508            // Issue #29 (2026-05-22): boot with one tab. The
2509            // slot's `panes` is a default placeholder; the real
2510            // pane tree above is live on `editor.pane_tree`.
2511            // `TabSlot::new` mints a fresh TabId.
2512            // Perf plan B.4.b: wrap in `Versioned` so version
2513            // starts at 0; subsequent tab list mutations bump via
2514            // DerefMut autoref.
2515            tabs: crate::versioned::Versioned::new(vec![lattice_core::ui::tab::TabSlot::new()]),
2516            active_tab: 0,
2517            document,
2518            snapshot_cache,
2519            document_buffer_id,
2520            buffers,
2521            active_buffer: BufferKind::Document,
2522            viewport_height: 1,
2523            lsp,
2524            lsp_diagnostics,
2525            lsp_logger,
2526            // 5.8.AA.o / 5.8.AF.5: lazy-spawn on first
2527            // `workspace/didChangeWatchedFiles` registration via
2528            // `Editor::refresh_lsp_file_watcher`. The handle here
2529            // is the cmd_tx side; the watcher itself + the
2530            // notify/event loop live on a tokio task on the LSP
2531            // runtime so nothing runs on the renderer's per-tick.
2532            lsp_watcher: None,
2533            lsp_watcher_subscriptions: std::collections::HashMap::new(),
2534            lsp_watcher_watched_roots: std::collections::HashSet::new(),
2535            // Phase 5.8.AF.5 / Slice 3a: empty `RenderState` so
2536            // the first dispatch publication has somewhere to
2537            // store into. Renderers reading before the first
2538            // dispatch (e.g. the initial paint at boot) see the
2539            // default empty sub-states, which is correct -- no
2540            // diagnostics, no popups, no pickers exist yet.
2541            //
2542            // X2.4: the same Arc is now also held by the
2543            // highlights worker (spawned above) so its
2544            // reads of `syntax` inputs see the SAME atomic snapshots
2545            // the renderer reads.
2546            render_state: render_state_arc,
2547            // X2.4 (gut + rename B4.2): same-Arc-identity values
2548            // constructed above and shared with the overlay worker.
2549            // Overrides the `..Editor::default()` tail (which would
2550            // otherwise construct fresh, unshared cells).
2551            overlay_wake,
2552            syntax_static_overlay_quads_cell,
2553            paint_request,
2554            // Slice B.1: same Notify the initial document's reparse
2555            // worker fires on publish (handed in above); the actor
2556            // loop awaits it to re-publish on idle reparse completion.
2557            async_landed,
2558            // S2.2 (2026-05-26): same-Arc-identity values for the
2559            // cell-builder worker. Overrides `..Editor::default()`
2560            // so the matrix the worker `.store()`s into is the
2561            // same one `render_state.cells.matrix` points at.
2562            cells_wake,
2563            cells_matrix_cell,
2564            cells_matrices,
2565            // B2.1 (2026-06-04): same-Arc-identity values for the
2566            // per-line display cache; active doc seeded above.
2567            display_matrix_cell,
2568            display_matrices,
2569            // D.0a.1 (2026-05-29): the worker's three Arcs +
2570            // wake match the cells pattern — same identities
2571            // here as the `runtime_handle.spawn(...)` above.
2572            virtual_rows_wake,
2573            virtual_rows_matrix_cell,
2574            // D.4.d.2.0 (2026-05-29): same-Arc-identity
2575            // seeding for the active doc's virtual-rows
2576            // matrix; subsequent buffers lazy-insert via
2577            // `Editor::virtual_rows_matrix_for`.
2578            virtual_rows_matrices,
2579            virtual_row_providers,
2580            // D.3.a.1 (2026-05-29): diff subsystem + its bus
2581            // subscription guard + the per-session wake
2582            // forwarder map. `:diff` mutates the map at slice
2583            // mount; `:diffoff` aborts a forwarder and clears
2584            // its entry.
2585            diff_subsystem,
2586            diff_subscription_guard: Some(diff_subscription_guard),
2587            diff_forwarders,
2588            vcs_subscription_guard: Some(vcs_subscription_guard),
2589            lsp_log_event_rx: Some(lsp_log_event_rx),
2590            lsp_progress_store: lsp_progress_store.clone(),
2591            modeline_update_rx: Some(modeline_update_rx),
2592            pending_apply_edit_rx: Some(lsp_apply_edit_rx),
2593            // I4: seat the host-drained programmatic-diff receiver + its
2594            // accept-path map (the sender was registered as a service above).
2595            pending_programmatic_diff_rx: Some(programmatic_diff_rx),
2596            programmatic_diff_accept_paths: std::collections::HashMap::new(),
2597            programmatic_diff_panes: std::collections::HashMap::new(),
2598            // D-fix.5: empty until the first diff-fold refresh observes a
2599            // session's published revision.
2600            diff_fold_seen_revisions: std::collections::HashMap::new(),
2601            // BC.8b: `pending_configuration_rx` removed (the generic inbound
2602            // drain replaces the host receiver). The SHARED config tree is
2603            // seated below (overriding `..Editor::default()`'s fresh Arc) so the
2604            // mode-owned handler + the editor observe one tree.
2605            lsp_config_tree,
2606            // BC.8c: `pending_show_document_rx` removed — the generic inbound
2607            // drain (mode-owned handler → host-applied open effects) replaces
2608            // the host receiver field.
2609            pending_show_message_request_rx: Some(lsp_show_message_request_rx),
2610            pending_lsp_detach_rx: Some(lsp_detach_rx),
2611            pending_mode_lifecycle_rx: Some(mode_lifecycle_rx),
2612            pending_major_entered_rx: Some(major_entered_rx),
2613            pending_mode_enablement_rx: Some(mode_enablement_rx),
2614            pending_buffer_option_override_rx: Some(buffer_option_override_rx),
2615            pending_provider_view_refresh_rx: Some(view_refresh_rx),
2616            pending_inlay_hint_refresh_rx: Some(lsp_inlay_refresh_rx),
2617            inlay_refresh_pending: std::collections::HashSet::new(),
2618            semantic_tokens_refresh_pending: std::collections::HashSet::new(),
2619            pending_semantic_tokens_refresh_rx: Some(lsp_semantic_tokens_refresh_rx),
2620            pending_code_lens_refresh_rx: Some(lsp_code_lens_refresh_rx),
2621            pending_diagnostic_refresh_rx: Some(lsp_diagnostic_refresh_rx),
2622            popup_back_stack: Vec::new(),
2623            insert_completion: None,
2624            snippet_registry: snippet_registry_handle,
2625            snippet_activation_policy,
2626            global_action_handler_regs,
2627            insert_completion_snippet_meta: Vec::new(),
2628            completion_accept_freq: HashMap::new(),
2629            pending_config_structural_sections: std::collections::BTreeMap::new(),
2630            per_language_completion: lattice_completion::per_language_defaults(),
2631            completion_in_path_context: false,
2632            // Generic session-backed-minor registration (composition
2633            // root): the host reconciles `active-snippet-mode` from
2634            // the shared `SnippetSession` predicate each overlay-sync
2635            // (`Editor::sync_keymap_overlays`) instead of a
2636            // snippet-specific block. `feedback_mode_owns_its_surface`:
2637            // the "when is my mode active?" policy lives in
2638            // `lattice-snippet` (`snippet_active_predicate`); the host
2639            // runs a generic loop. Clone the handle so
2640            // `snippet_session` still moves into its own field below.
2641            session_backed_minors: vec![crate::editor::SessionBackedMinor {
2642                active: lattice_snippet::snippet_active_predicate(snippet_session.clone()),
2643                mode_id: lattice_snippet::modes::SnippetActiveMode::mode_id(),
2644            }],
2645            snippet_session,
2646            // Default user snippet dir: `~/.config/lattice/snippets`
2647            // (the same XDG config root as `lattice.toml`, via
2648            // `lattice_config::config_home` — honours `$XDG_CONFIG_HOME`
2649            // and reads `~/.config` on macOS, NOT the platform-native
2650            // dir, so config + snippets always live together).
2651            // `:reload-snippets` merges any `<language>.json` packs here
2652            // on top of the embedded built-ins. Absent dir → skipped
2653            // gracefully by the reload path (not an error — the user just
2654            // hasn't added any packs).
2655            snippet_dirs: lattice_config::config_home()
2656                .map(|d| d.join("lattice").join("snippets"))
2657                .into_iter()
2658                .collect(),
2659            // M.7: use the pre-created Arc so the `services:` block
2660            // and `fold_registry` field share identity.
2661            fold_registry,
2662            ..Editor::default()
2663        };
2664        // MG.2: wire the async_landed Notify into the pending-highlights
2665        // service so async refresh tasks can fire it after storing spans.
2666        if let Some(pending) = editor
2667            .services
2668            .get::<lattice_mode::PendingSyntheticHighlights>()
2669        {
2670            *pending.waker.lock().expect("waker init") = Some(editor.async_landed.clone());
2671        }
2672        // DL.3b: same wiring for the inlay channel. Without the waker a
2673        // producer's rows sit until the user happens to press a key —
2674        // the `feedback_async_needs_wake` failure mode, which reads as a
2675        // rendering bug rather than a missing wake.
2676        if let Some(pending) = editor.services.get::<lattice_mode::PendingInlays>() {
2677            pending.set_waker(editor.async_landed.clone());
2678        }
2679
2680        // 2026-05-26: register the built-in invocation runners
2681        // under the mode-ids each owning [`lattice_mode::Mode`]
2682        // exposes via [`lattice_mode::Mode::invocation_runner`].
2683        // `run_invocation` resolves the runner by walking the
2684        // active modes on the active pane (minors first, then
2685        // major) and looking the first match up here. Plugin-
2686        // installed modes (post Phase 7) reuse
2687        // [`Editor::register_invocation_runner`] for the same
2688        // effect.
2689        editor.register_invocation_runner(
2690            lattice_mode::HelpMode::mode_id(),
2691            Editor::run_help_invocation,
2692        );
2693        editor.register_invocation_runner(
2694            lattice_listing::oil::OilMode::mode_id(),
2695            Editor::run_oil_invocation,
2696        );
2697        editor.register_invocation_runner(
2698            lattice_listing::file_tree::FileTreeMode::mode_id(),
2699            Editor::run_file_tree_invocation,
2700        );
2701        editor.register_invocation_runner(
2702            lattice_terminal::TerminalMode::mode_id(),
2703            Editor::run_terminal_invocation,
2704        );
2705        // AU‑3 gap fix: the AI conversation buffer is read-only above an
2706        // editable prompt tail. Its runner gates vim operators (`x` / `dd`)
2707        // to the tail so they can't mutate the frozen transcript — the
2708        // editable-tail read-only gate otherwise only covered the
2709        // `apply_edit_blocking` char path, not the operator path.
2710        editor.register_invocation_runner(
2711            lattice_ai::acp::conversation_mode::AiConversationMode::mode_id(),
2712            Editor::run_editable_tail_invocation,
2713        );
2714        // PD.4: the same gap, one layer more general. `read-only-mode`
2715        // contributes `ReadOnly = true`, and that option is only consulted
2716        // by `read_only_edit_rejected` — which guards `apply_edit_blocking`
2717        // and its batch peer, i.e. the insert-mode char path. Operators
2718        // never go near it: a plain `Document`'s grammar dispatch applies
2719        // its edits inside the document actor and hands the host an
2720        // already-applied `Effect::Edits`. Without a runner the mode
2721        // stopped typing and left `x` / `dd` / `cw` working, which is worse
2722        // than not gating at all — the buffer looks protected and isn't.
2723        //
2724        // `run_read_only_motion` is the right runner rather than the
2725        // conversation buffer's tail-aware one: there is no editable tail
2726        // here, the whole buffer is frozen. Motions still move, `:` and `/`
2727        // still fall through to the central dispatcher, and mutating
2728        // operators echo "buffer is read-only" instead of silently doing
2729        // nothing.
2730        editor.register_invocation_runner(
2731            lattice_mode::modes::ReadOnlyMode::mode_id(),
2732            Editor::run_read_only_motion,
2733        );
2734
2735        // Slice C (`:files` warm-up): pre-walk the project's file list in the
2736        // background so the FIRST `:files` open is served from the session
2737        // cache instead of paying the full walk. Resolve the SAME root `:files`
2738        // resolves — the project root of the opened file (or the cwd for a
2739        // no-file launch) via the project resolver — then hand the walk to a
2740        // detached thread: boot never blocks on I/O (paramount #1 / #4), and a
2741        // cold cache simply means the first open walks, exactly as before.
2742        #[cfg(not(test))]
2743        {
2744            let start = boot_doc_path.or_else(|| std::env::current_dir().ok());
2745            if let Some(start) = start {
2746                let root = editor
2747                    .services
2748                    .get::<lattice_core::ProjectResolverHandle>()
2749                    .map(|resolver| resolver.for_path(&start).root)
2750                    .unwrap_or(start);
2751                std::thread::spawn(move || {
2752                    lattice_picker::picker_sources::warm_files_cache(&root);
2753                });
2754            }
2755        }
2756        editor
2757    }
2758}
2759
2760/// SG.4a — the four `ui.diagnostic-*-glyph` values as chars, for the built-in
2761/// diagnostic sign definitions.
2762///
2763/// Read here rather than in `lattice-mode` so that crate keeps no typed-options
2764/// dependency. The glyphs are live options, so this is called again whenever one
2765/// changes and the definitions are re-registered — redefinition keeps the id
2766/// (SG.1), which is what makes that safe with placements already in flight.
2767pub(crate) fn diagnostic_glyphs_from(config: &ConfigRegistry) -> lattice_mode::DiagnosticGlyphs {
2768    let d = lattice_mode::DiagnosticGlyphs::default();
2769    let ch = |s: Option<std::sync::Arc<String>>, dflt: char| {
2770        s.and_then(|v| v.chars().next()).unwrap_or(dflt)
2771    };
2772    lattice_mode::DiagnosticGlyphs {
2773        error: ch(
2774            config.get_typed::<crate::ui::theme_options::UiDiagnosticErrorGlyph>(),
2775            d.error,
2776        ),
2777        warning: ch(
2778            config.get_typed::<crate::ui::theme_options::UiDiagnosticWarningGlyph>(),
2779            d.warning,
2780        ),
2781        info: ch(
2782            config.get_typed::<crate::ui::theme_options::UiDiagnosticInfoGlyph>(),
2783            d.info,
2784        ),
2785        hint: ch(
2786            config.get_typed::<crate::ui::theme_options::UiDiagnosticHintGlyph>(),
2787            d.hint,
2788        ),
2789    }
2790}