Skip to main content

lattice_mode/
subsystem_boot.rs

1//! `SubsystemBoot` — the capability surface a subsystem wires against at boot.
2//!
3//! Boot-composition BC.3b. Every subsystem (the Claude Code IDE peer, LSP,
4//! multibuffer, terminal, …) self-installs through one crate-owned entry point
5//!
6//! `pub fn install(boot: &mut impl SubsystemBoot)` that does *all* of its wiring — modes, commands, services, the off-keystroke
7//! inbound bus, event wakes — against the generic primitives this trait exposes.
8//! The host (`editor_boot`) then has a single Phase-B *install list*: one line
9//! per subsystem. Adding a subsystem touches the host in exactly that one place
10//! and zero host internals (no `Editor::` method, no host `Action` variant) —
11//! the mode-ownership acid test, and the property that keeps host churn flat as
12//! the mode count grows into the hundreds.
13//!
14//! ## Why a trait (not the concrete context)
15//!
16//! The concrete bundle (`lattice_host::boot_context::BootContext`) lives in
17//! `lattice-host`, which depends on every subsystem crate — so a subsystem
18//! crate cannot name it without a dependency cycle. `SubsystemBoot` lives here
19//! in `lattice-mode` (below every subsystem crate), exposes only the generic
20//! install surface, and is implemented by `BootContext`. Subsystems depend on
21//! the capability, not the host. Host-only lifecycle (registry freezing,
22//! tick-token hand-off, the LSP-specific diagnostics handle) stays inherent on
23//! `BootContext` and never leaks into this surface — the LSP diagnostics handle,
24//! for instance, is reached through the generic [`service`](SubsystemBoot::service)
25//! lookup, so this trait never names a `lattice-lsp` type.
26//!
27//! The generic methods make the trait non-object-safe; installs take
28//! `&mut impl SubsystemBoot` (static dispatch), so object safety is not needed.
29//!
30//! # Examples
31//!
32//! A complete install for a subsystem with one mode and one off-thread
33//! producer. The producer's results reach the screen with no keystroke
34//! because the wake lives inside [`InboundBus::send`]:
35//!
36//! ```
37//! use lattice_grammar::effect::{EchoLevel, Effect};
38//! use lattice_mode::inbound::InboundBus;
39//! use lattice_mode::{LifecycleFuture, Mode, ModeContext, ModeId, ModeKind, SubsystemBoot};
40//!
41//! struct WeatherMode;
42//! impl Mode for WeatherMode {
43//!     type Guard = ();
44//!     fn id(&self) -> ModeId { ModeId::new("weather-mode") }
45//!     fn kind(&self) -> ModeKind { ModeKind::Minor }
46//!     fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
47//!         Box::pin(async { Ok(()) })
48//!     }
49//! }
50//!
51//! /// A report from the off-thread fetcher.
52//! struct Report(String);
53//!
54//! /// The one line the host's install list calls.
55//! pub fn install(boot: &mut impl SubsystemBoot) {
56//!     if let Err(e) = boot.modes_mut().register(WeatherMode) {
57//!         tracing::warn!(%e, "weather-mode not registered"); // log + skip, never panic
58//!     }
59//!     // The handler runs on the editor actor, once per drained item.
60//!     let bus: InboundBus<Report> = boot.inbound(|Report(text)| {
61//!         vec![Effect::Echo { level: EchoLevel::Info, text }]
62//!     });
63//!     boot.runtime_handle().spawn(async move {
64//!         // … fetch off-thread, then:
65//!         let _ = bus.send(Report("sunny".into())); // wakes the editor
66//!     });
67//! }
68//! ```
69
70use std::any::Any;
71use std::sync::Arc;
72
73use lattice_grammar::CommandRegistry;
74use lattice_grammar::effect::Effect;
75use lattice_protocol::event_registry::Event as TypedEvent;
76use lattice_runtime::EventBus;
77use tokio::runtime::Handle;
78
79use crate::idle_gate::{IdleGateHandle, IdleGateHandler};
80use crate::inbound::InboundBus;
81use crate::tick_callback::TickCallback;
82use crate::{BufferStoreHandle, ModeRegistry, ServiceRegistry};
83
84/// The generic-primitive surface a subsystem's `install(boot)` wires against.
85///
86/// Implemented by the host's `BootContext`. See the module docs for the
87/// install pattern and why this is a trait rather than the concrete bundle.
88pub trait SubsystemBoot {
89    /// Mutable access to the command registry — register ex-commands,
90    /// operators, motions, text objects. Valid until the host freezes the
91    /// registry (after the install list); calling it post-freeze panics.
92    fn commands_mut(&mut self) -> &mut CommandRegistry;
93
94    /// Mutable access to the mode registry — register the subsystem's major /
95    /// minor modes. Valid until the host freezes the registry.
96    fn modes_mut(&mut self) -> &mut ModeRegistry;
97
98    /// Mutable access to the service registry — for bulk / multi-step service
99    /// wiring. Most callers want [`register_service`](Self::register_service).
100    fn services_mut(&mut self) -> &mut ServiceRegistry;
101
102    /// Register a single service handle under its `TypeId`. Per the
103    /// `ServiceRegistry` Arc/TypeId rule, register and look up with the same
104    /// `T` (register `Arc<X>` ⇒ look up `Arc<X>`).
105    fn register_service<T: Any + Send + Sync>(&mut self, service: T);
106
107    /// Look up a service by type — the seam a subsystem uses to reach a
108    /// *generic* handle another layer owns (e.g. the LSP `DiagnosticsQueryHandle`
109    /// the host registered in Phase A) without this trait naming that type.
110    /// Returns `Arc<T>`; for an `Arc<X>` registration this is `Arc<Arc<X>>`,
111    /// so unwrap one layer (`(*h).clone()`).
112    fn service<T: Any + Send + Sync>(&self) -> Option<Arc<T>>;
113
114    /// The bundled **inbound** primitive: a channel whose `send` wakes the
115    /// editor off-keystroke (the wake is inside the sender — structurally
116    /// impossible to forget) and whose items are drained per-tick, each run
117    /// through `handler` (validate → map to an [`Effect`] → resolve any
118    /// oneshot). Returns the sender for the off-thread producer; the drain's
119    /// registration is retained for the editor's lifetime by the host.
120    fn inbound<T, H>(&mut self, handler: H) -> InboundBus<T>
121    where
122        T: Send + 'static,
123        H: FnMut(T) -> Vec<Effect> + Send + 'static;
124
125    /// Subscribe a typed event and **wake the editor** whenever one is
126    /// published — the off-keystroke-repaint primitive for event-driven work.
127    fn wake_on_event<E>(&self)
128    where
129        E: TypedEvent + Clone;
130
131    /// Register a raw per-tick drain closure (for drains that are not channel
132    /// inbound buses, e.g. a state-poll). The registration is retained for the
133    /// editor's lifetime by the host.
134    fn tick_callback(&mut self, callback: TickCallback);
135
136    /// Register an **idle gate** — a handler the editor actor runs when
137    /// an armed deadline elapses, applying the `Effect`s it returns and
138    /// repainting. The subsystem arms it from its own event handler
139    /// (`handle.arm(Instant::now() + delay)`) and disarms when the reason
140    /// evaporates (WK.3).
141    ///
142    /// This is the time-domain peer of [`inbound`](Self::inbound): the wake is
143    /// inside the primitive, so a gate's effects reach the screen WITHOUT a
144    /// keystroke. Registering a bare deadline field on the editor instead is
145    /// the smell this replaces — see `idle_gate`'s module docs.
146    fn idle_gate(&mut self, name: &'static str, handler: IdleGateHandler) -> IdleGateHandle;
147
148    /// The typed event bus (subscribe / publish).
149    fn event_bus(&self) -> &Arc<EventBus>;
150
151    /// The shared async runtime handle (off-thread spawns).
152    fn runtime_handle(&self) -> &Handle;
153
154    /// The generic buffer-store handle — the uniform buffer-access substrate
155    /// (read a buffer's text / path / id by `BufferId`).
156    fn buffer_store(&self) -> &BufferStoreHandle;
157}