Skip to main content

lattice_multibuffer/
install.rs

1//! BC.7 (2026-06-24): the crate-owned `install(boot)` entry point.
2//!
3//! The multibuffer subsystem registers its own **modes + commands + services +
4//! off-keystroke wake** through the generic [`SubsystemBoot`] surface,
5//! collapsing the host's ~6 scattered `editor_boot` sites into one Phase-B
6//! line (`lattice_multibuffer::install(&mut boot)`) — the terminal /
7//! claude-code / diff shape.
8//!
9//! ## What collapses here
10//!
11//! - **The registry handle** ([`InMemoryMultibufferRegistry::handle`]) is
12//!   created *inside* `install` — multibuffer-owned, because it carries **no
13//!   host-state dependency** (unlike diff's resolver-backed
14//!   `DiffSubsystemHandle` or terminal's host-`BufferRegistry`-backed
15//!   `TerminalStoreHandle`, both of which the host must construct). It is used
16//!   for mode registration + the excerpt-jump motion handlers, and published
17//!   as a service the host reads back at dispatch time
18//!   (`Editor::resolve_narrow_target`) via `services.get::<MultibufferRegistryHandle>()`.
19//! - **Modes** — `multibuffer-mode` (+ its `DocumentClosed` cleanup
20//!   subscriber), `narrow-mode`, and the `search` provider mode.
21//! - **Commands** — the excerpt-jump motions (`]e`/`[e`/`]E`/`[E`), the
22//!   `:multibuffer-*` ex-commands, `:narrow`/`:widen`, the `zn` narrow
23//!   operator SPEC, and the `:search` ex-command.
24//! - **Services** — the registry handle + the project-search service.
25//! - **The off-keystroke wake** — `boot.wake_on_event::<MultibufferExcerptsReady>()`,
26//!   replacing the host's hand-rolled mpsc→`async_landed` forwarder so streamed
27//!   search excerpts repaint without a keypress. The wake is now a *property of
28//!   the primitive* (BC design §3), not by-discipline.
29//!
30//! ## What stays host-side (documented residue, NOT mode-ownership violations)
31//!
32//! - **The `zn` narrow-operator *binding*** at the universal operator-pending
33//!   (`Builtin`) layer. The operator *spec/handler* lives here
34//!   ([`crate::providers::narrow::register_narrow_operator`]); only the
35//!   *binding* is host-side, because `zn` is **universal grammar** — it
36//!   composes with the universal motion/text-object vocabulary and needs the
37//!   host-resolved `Builtins`. This is the same category as `lattice-syntax`'s
38//!   structural text objects (N.1.4c): registered-in-crate, bound-at-`Builtin`.
39//!   There is no single "owning mode" for a universal operator. BC.7 decision
40//!   (A): the host resolves the operator by name
41//!   (`registry.id_by_name("operator:narrow")`), so the registration *return
42//!   value* no longer threads through the host — the K.2.5 motion
43//!   name-resolution pattern.
44//! - **Host `Effect` appliers** — `Editor::resolve_narrow_target` + the
45//!   `AppEffect::{SearchTrigger,NarrowTrigger,NarrowLines,NarrowWiden,MultibufferExpand}`
46//!   dispatch arms mutate `&mut Editor` (open buffers, pane tree, splits) — the
47//!   Effect-vocabulary-is-the-host-boundary rule (diff's `do_diff_*` precedent).
48//!   The trigger substrate fns (`project_search`, `create_narrow_view`,
49//!   `multibuffer_expand_excerpt_at`) are crate-owned helpers those arms call.
50//! - **The generic `event_bus` *service*** stays a Phase-A host primitive —
51//!   many subsystems consume it; it is not multibuffer-owned.
52
53use lattice_mode::SubsystemBoot;
54
55/// Wire the multibuffer subsystem's modes + commands + services + wake into the
56/// editor at boot. One Phase-B line in `editor_boot.rs`.
57pub fn install(boot: &mut impl SubsystemBoot) {
58    // The registry handle: crate-owned (no host-state dependency). Captured for
59    // mode registration + the motion handlers + published as a service below.
60    let registry_handle = crate::registry::InMemoryMultibufferRegistry::handle();
61    // Own a clone of the event bus up front: `register_multibuffer_modes` needs
62    // `&Arc<EventBus>` *and* `boot.modes_mut()` in one call, so borrowing
63    // `boot.event_bus()` across the `&mut` would conflict. The owned local
64    // sidesteps it.
65    let event_bus = boot.event_bus().clone();
66
67    // ── Modes ───────────────────────────────────────────────────────────────
68    // `multibuffer-mode` (H.2 kind-bound to `BufferKind::Multibuffer`) + its
69    // `DocumentClosed` cleanup subscriber, which needs the event bus + the
70    // registry handle. The subscriber spawns only when a tokio runtime is in
71    // scope (production boot); test paths skip it gracefully.
72    crate::mode::register_multibuffer_modes(boot.modes_mut(), &event_bus, registry_handle.clone());
73    // N.1.1: narrow provider-minor mode (marker for narrow views). First-class.
74    crate::providers::narrow::register_narrow_mode(boot.modes_mut());
75    // CM.4: problems provider-minor mode (marker for `*problems*` views). First-class.
76    crate::providers::problems::register_problems_mode(boot.modes_mut());
77    // M.6: project-search provider-minor mode (feature-gated with its provider).
78    #[cfg(feature = "search")]
79    crate::providers::search::register_project_search_mode(boot.modes_mut());
80    // OM.A3: the agenda view's minor. `gr` arrives through the implies
81    // cascade because it declares a `refresh_action`.
82    #[cfg(feature = "scan-view")]
83    crate::providers::scan_view::register_scan_view_mode(boot.modes_mut());
84
85    // ── Commands ────────────────────────────────────────────────────────────
86    // M.2.b.3 / K.2.5: excerpt-jump motions (`]e`/`[e`/`]E`/`[E`). The returned
87    // `MultibufferMotionIds` is discarded — `MultibufferMode::keymap()`
88    // references the motions by canonical name, resolved at the host's K.2.4
89    // translation pass; the registration side-effect (the names in the
90    // registry) is what keeps that lookup successful.
91    let _ =
92        crate::motions::register_multibuffer_motions(boot.commands_mut(), registry_handle.clone());
93    crate::mode::register_multibuffer_ex_commands(boot.commands_mut());
94    // N.1.1: `:narrow` + `:widen`. First-class — no feature gate.
95    crate::providers::narrow::register_narrow_ex_commands(boot.commands_mut());
96    // CM.4: `:copen` + `:cclose`. First-class — no feature gate.
97    crate::providers::problems::register_problems_ex_commands(boot.commands_mut());
98    // N.1.3 / BC.7 (A): register the `zn` narrow operator SPEC. The returned
99    // `OperatorId` is discarded — the host's universal operator-pending binding
100    // resolves it by name (`operator:narrow`), the motion name-resolution
101    // pattern. The binding lives host-side because `zn` is universal grammar
102    // that composes with the resolved `Builtins`; only the spec is mode-owned.
103    let _ = crate::providers::narrow::register_narrow_operator(boot.commands_mut());
104    // M.6: `:search` ex-command (feature-gated with its provider).
105    #[cfg(feature = "search")]
106    crate::providers::search::register_search_ex_command(boot.commands_mut());
107    // OM.A1: `:agenda` (feature-gated with its provider). OM.A3 adds the
108    // refresh action the view mode's `refresh_action` names.
109    #[cfg(feature = "scan-view")]
110    {
111        // AG.1: no ex-command here any more. The provider is registered above
112        // and is generic; the TRIGGER belongs to whoever produces the rows,
113        // because only they can name it in their users' vocabulary. `:agenda`
114        // was the host naming an org feature generically and the plugin having
115        // no way to fix it — org registers `:org-agenda` itself now, through
116        // `app-effect::open-provider-view`.
117        crate::providers::scan_view::register_scan_view_actions(boot.commands_mut());
118    }
119
120    // ── Services ────────────────────────────────────────────────────────────
121    // M.2.b.2: expose the typed multibuffer-handle lookup so providers
122    // (`create_multibuffer_view`, the `:search` minor) AND the host's
123    // `resolve_narrow_target` reach it via `services.get::<MultibufferRegistryHandle>()`.
124    boot.register_service(registry_handle);
125    // M.6: the project-search service so `project_search` triggers find it.
126    #[cfg(feature = "search")]
127    crate::providers::search::register_project_search_service(boot.services_mut());
128    // OM.A1: the agenda's per-view state.
129    //
130    // MV.3: its OPENER is no longer registered here. The agenda is org's
131    // feature, and org now declares it through `multibuffer-view-source` like
132    // any other plugin-owned view — the host supplies the machinery (walk,
133    // read-and-parse-once, sort, group runs, headerline) and org supplies the
134    // identity. Registering it natively too would refuse org's declaration,
135    // since `ProviderViewRegistry::register` refuses rather than replaces.
136    //
137    // Nothing is lost when org is absent: the agenda's ROWS come from org's
138    // scan source, so a host without it had an agenda that could only say "no
139    // plugin provides rows for it". The trigger (`:org-agenda`) is org's too,
140    // so the provider and its command now arrive together instead of the
141    // provider existing alone and empty.
142    #[cfg(feature = "scan-view")]
143    {
144        crate::providers::scan_view::register_scan_view_service(boot.services_mut());
145    }
146
147    // ── Off-keystroke wake ──────────────────────────────────────────────────
148    // `MultibufferExcerptsReady` (published by any provider after appending a
149    // batch) wakes `async_landed` so the actor republishes render state and the
150    // cells worker picks up the new excerpt syntax — without a keypress.
151    // Replaces the host's hand-rolled mpsc→notify forwarder; the wake is baked
152    // into the primitive (can't-forget). Ordering with the
153    // `AsyncRenderStatePublished` → cells bridge is unchanged (that bridge stays
154    // host-side, downstream of this wake).
155    //
156    // PV.1 (2026-08-12): NO LONGER `#[cfg(feature = "search")]`. The event is a
157    // property of multibuffer views, not of searching; gating it meant a
158    // `--no-default-features` build — and any provider living outside this
159    // crate, like magit's project-diff — appended excerpts that only appeared
160    // on the next keypress.
161    boot.wake_on_event::<crate::events::MultibufferExcerptsReady>();
162}