Skip to main content

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}