lattice_multibuffer/view.rs
1//! M.2.b.2 (2026-06-01): `create_multibuffer_view` — the atomic
2//! "make me a multibuffer view" entry point that providers (and
3//! tests) call.
4//!
5//! Composes the five steps so providers can't forget any:
6//!
7//! 1. Allocate a `BufferId`.
8//! 2. Build the typed `MultibufferDocumentHandle` from `sources` +
9//! `excerpts` (empty inputs are valid — async providers
10//! open empty views and stream content via
11//! [`MultibufferDocumentHandle::append_excerpts`]).
12//! 3. Register the typed handle in `MultibufferRegistry` (pulled
13//! from `activator.services()`).
14//! 4. Insert the upcast handle into `BufferStore` via H.1's
15//! `insert_document_buffer(id, BufferKind::Multibuffer, ...)`.
16//! 5. Activate `multibuffer-mode` on the buffer via
17//! `activator.activate_major_for_kind(id, Multibuffer)` —
18//! H.2's `ModeRegistry::find_major_for_kind` resolves the
19//! mode id from the kind.
20//!
21//! Failures (missing `BufferStore` / `MultibufferRegistry`
22//! service) log + return early. The activation cascade's own
23//! failure path publishes through `ModeEvent::ModeActivationFailed`.
24//! Return type is `BufferId` (not `Result`) to match
25//! `Editor::activate_major_for_buffer_kind`'s existing shape.
26//!
27//! See `docs/dev/architecture/multibuffer-views.md` §3.7.
28
29use std::collections::HashMap;
30use std::sync::Arc;
31
32use lattice_core::{BufferFlags, BufferId, BufferKind};
33use lattice_mode::{BufferStoreHandle, ModeActivator};
34use lattice_runtime::{Document, EventBus};
35use lattice_syntax::LangRegistry;
36
37use crate::registry::MultibufferRegistryHandle;
38use crate::{
39 Excerpt, MultibufferDocumentHandle, MultibufferExcerptHeaderProvider, MultibufferStatusProvider,
40};
41
42/// Atomic insert + activate-major for a multibuffer view. Returns
43/// the freshly-allocated view `BufferId`. After this call:
44///
45/// - The view buffer exists in `BufferRegistry` (as a
46/// `BufferData::Multibuffer` entry).
47/// - `MultibufferRegistry::handle(buffer_id)` returns the typed
48/// handle.
49/// - `multibuffer-mode` is the active major on the buffer.
50///
51/// The caller (typically a provider's public trigger function,
52/// e.g. `project_search`) is responsible for activating its own
53/// provider-minor mode after this returns (see §3.7 worked
54/// example).
55///
56/// **Empty `sources` + empty `excerpts` are valid.** Async
57/// providers (project-search, lsp-references, etc.) call this
58/// with empty inputs to open the view immediately, then stream
59/// content in via [`MultibufferDocumentHandle::append_excerpts`]
60/// as their scan progresses.
61///
62/// **Missing services log + return a fresh `BufferId`** — the
63/// caller can detect by checking
64/// `activator.services().get::<MultibufferRegistryHandle>()
65/// .and_then(|r| r.handle(id))` returning `Some`. Production
66/// boot wires both services unconditionally; failure here means
67/// boot order is broken.
68pub fn create_multibuffer_view(
69 activator: &mut dyn ModeActivator,
70 sources: HashMap<BufferId, Arc<dyn Document>>,
71 excerpts: Vec<Excerpt>,
72 name: Option<String>,
73 flags: BufferFlags,
74 registry: lattice_grammar::CommandRegistryHandle,
75 // K.4.7 (2026-06-07): when `Some`, wired into the handle so
76 // `add_source` creates a `SyntaxHandle` per source file.
77 lang_registry: Option<Arc<LangRegistry>>,
78 // AF.1: how this view's rows group for folding. A PARAMETER rather than a
79 // setter on the returned handle because `MultibufferMode::on_activate`
80 // reads it while activating the mode below — by the time the caller has a
81 // BufferId back, the fold source has already been chosen.
82 fold_grouping: crate::FoldGrouping,
83) -> BufferId {
84 let services = activator.services();
85
86 let buffer_store = match services.get::<BufferStoreHandle>() {
87 Some(h) => h,
88 None => {
89 tracing::warn!(
90 "create_multibuffer_view: BufferStoreHandle service not registered; \
91 returning sentinel BufferId — boot order is broken"
92 );
93 return BufferId::next();
94 }
95 };
96
97 let mb_registry = match services.get::<MultibufferRegistryHandle>() {
98 Some(h) => h,
99 None => {
100 tracing::warn!(
101 "create_multibuffer_view: MultibufferRegistryHandle service not \
102 registered; returning sentinel BufferId — boot order is broken"
103 );
104 return BufferId::next();
105 }
106 };
107
108 let handle = match MultibufferDocumentHandle::new(sources, excerpts, registry) {
109 Ok(h) => h,
110 Err(e) => {
111 tracing::warn!(
112 error = ?e,
113 "create_multibuffer_view: MultibufferDocumentHandle::new failed; \
114 returning sentinel BufferId"
115 );
116 return BufferId::next();
117 }
118 };
119 // K.4.7: wire lang_registry so subsequent add_source calls can
120 // create SyntaxHandles for per-excerpt syntax highlighting.
121 if let Some(lr) = lang_registry {
122 handle.set_lang_registry(lr);
123 }
124 handle.set_fold_grouping(fold_grouping);
125 let buffer_id = handle.buffer_id();
126
127 // Step 2.5 (M.4 2026-06-01): auto-subscribe to source events
128 // so the view recomposes when a source publishes
129 // DocumentChanged and prunes + publishes
130 // MultibufferSourceClosed when a source closes. No-op if the
131 // EventBus service isn't registered (test paths without the
132 // bus wired up).
133 // EventBus is registered as `Arc<EventBus>` at boot
134 // (`editor_boot.rs` -> `s.register(event_bus.clone())` where
135 // `event_bus: Arc<EventBus>`); lookup must query the same
136 // shape and unwrap one Arc layer. Earlier
137 // `services.get::<EventBus>()` returned None silently —
138 // tests still pass because they wire the bus directly via
139 // `attach_event_subscriptions`, but production paths missed
140 // their subscriptions.
141 if let Some(events_outer) = services.get::<Arc<EventBus>>() {
142 let events: Arc<EventBus> = (*events_outer).clone();
143 handle.attach_event_subscriptions(&events);
144 }
145
146 let typed_handle = Arc::new(handle);
147
148 // Step 3: typed-handle registry insert (providers reach
149 // through this via service lookup).
150 mb_registry.insert(buffer_id, typed_handle.clone());
151
152 // Step 4: buffer-registry insert via H.1's primitive method.
153 // Upcast to `Arc<dyn Document>`.
154 let dyn_handle: Arc<dyn Document> = typed_handle.clone();
155 buffer_store.insert_document_buffer(
156 buffer_id,
157 BufferKind::Multibuffer,
158 dyn_handle,
159 flags,
160 name,
161 );
162
163 // K.4.6 (2026-06-02): excerpt-header provider — one VirtualRow per
164 // excerpt (anchored Above the excerpt's first composed row).
165 // M.6.5 (2026-06-08): renamed to MultibufferExcerptHeaderProvider.
166 //
167 // T.7 (2026-06-18): register the mode-owned excerpt-header theme
168 // elements + capture their ids, then wire the resolved theme
169 // handle into the provider so `collect()` bakes the registered
170 // backdrop / path colors into the header rows. Registration is
171 // idempotent by name; `MultibufferMode::on_activate` registers
172 // the same elements (mode-ownership contract) — both hit the same
173 // interned ids. Build the provider with theme only when the
174 // service is wired (production); test activators that don't
175 // register the service fall back to the no-theme provider.
176 // MH.A4: register the mode's theme elements ONCE (idempotent by
177 // name) and share the interned ids between the excerpt-header and
178 // status providers — both bake their colors from the same table.
179 let theme_handle = services
180 .get::<lattice_theme::ThemeRegistryHandle>()
181 .map(|outer| (*outer).clone());
182 let theme_elements = theme_handle.as_ref().map(|theme| {
183 let owner = lattice_theme::ElementOwner::Mode(
184 crate::MultibufferMode::mode_id()
185 .as_str()
186 .to_string()
187 .into(),
188 );
189 crate::register_multibuffer_theme_elements(theme.as_ref(), owner)
190 });
191 let excerpt_header_provider: Arc<MultibufferExcerptHeaderProvider> =
192 match (theme_handle.clone(), theme_elements) {
193 (Some(theme), Some(elements)) => {
194 // MH.A3 (2026-06-19): read the GLOBAL `ui.nerd_fonts`
195 // default via the ConfigRegistry service (registered at
196 // boot; read by name so we needn't import host's
197 // `UiNerdFonts` decl) to pick the icon palette. Defaults
198 // to `false` (BMP fallback) when the service or option is
199 // absent — matching the icon-palette rule's safe default.
200 //
201 // MH.A3 follow-on: live per-buffer `ui.nerd_fonts` toggle
202 // — captured once here, so a runtime toggle does not yet
203 // re-render the header. Needs the `FrameView::for_buffer`
204 // seam threaded into the provider.
205 let nerd_fonts = services
206 .get::<Arc<lattice_config::ConfigRegistry>>()
207 .and_then(|cfg| cfg.get_bool_by_name("ui.nerd_fonts"))
208 .unwrap_or(false);
209 Arc::new(
210 MultibufferExcerptHeaderProvider::with_theme(
211 (*typed_handle).clone(),
212 theme,
213 elements,
214 nerd_fonts,
215 )
216 // OA.2: headers group the way this view folds. The two
217 // must agree, so they read one declaration.
218 .with_grouping(fold_grouping),
219 )
220 }
221 _ => Arc::new(
222 MultibufferExcerptHeaderProvider::new((*typed_handle).clone())
223 .with_grouping(fold_grouping),
224 ),
225 };
226 let registered = activator.register_virtual_row_provider(buffer_id, excerpt_header_provider);
227 if !registered {
228 tracing::debug!(
229 buffer = ?buffer_id,
230 "create_multibuffer_view: excerpt header provider not registered \
231 (test activator with default impl, or duplicate ProviderId)"
232 );
233 }
234
235 // M.6.5 (2026-06-08): view-status sticky headerline — surfaces
236 // HeaderlineStatus (Idle / InProgress / Complete / Failed) as a
237 // pinned row above line 0. Hidden when status is Idle.
238 // MH.A4: thread the resolved theme + the shared status element ids
239 // into the status provider so its `render()` resolves the
240 // `multibuffer.status.*` foregrounds (falls back to the no-theme
241 // provider for test activators with no theme service).
242 let status_provider = match (theme_handle, theme_elements) {
243 (Some(theme), Some(elements)) => {
244 MultibufferStatusProvider::with_theme((*typed_handle).clone(), theme, elements)
245 }
246 _ => MultibufferStatusProvider::new((*typed_handle).clone()),
247 }
248 .into_provider(buffer_id);
249 let registered = activator.register_virtual_row_provider(buffer_id, Arc::new(status_provider));
250 if !registered {
251 tracing::debug!(
252 buffer = ?buffer_id,
253 "create_multibuffer_view: status provider not registered \
254 (test activator with default impl, or duplicate ProviderId)"
255 );
256 }
257
258 // Step 5: activate the major via H.2's kind dispatch. The
259 // activator's impl runs the full cascade (default minor +
260 // auto minors + options recompute + completion sources +
261 // maybe-auto-LSP) and queues any renderer signals into the
262 // implementor's pending-signals queue.
263 activator.activate_major_for_kind(buffer_id, BufferKind::Multibuffer);
264
265 buffer_id
266}