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 ®istry.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, ®istry.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 ®istry.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 ®istry.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}