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}