lattice_host/buffer_registry.rs
1//! Unified buffer registry (DESIGN.md §5.9).
2//!
3//! Every concrete buffer the App can route input through -- a code
4//! [`Document`] today, a [`FileTreeBuffer`] tomorrow -- lives in
5//! [`BufferRegistry`] keyed by [`BufferId`]. A single registry
6//! gives `:bn` / `:bp` / `:ls` / `:bd` a consistent surface across
7//! buffer kinds; multiple file trees coexist with multiple
8//! documents under the same shape.
9//!
10//! ## Threading model (B'.1b)
11//!
12//! `BufferRegistry` uses interior mutability via
13//! `Arc<Mutex<BufferRegistryInner>>`. Every method takes `&self`
14//! and locks briefly. The registry is `Clone` (cheap atomic
15//! bump) so the App's `BufferStore` service impl can hold the
16//! same state as the App's `buffers` field — modes call into
17//! the shared store from any thread; the App accesses the same
18//! data through its direct field.
19//!
20//! Methods that previously returned `Option<&BufferEntry>` (and
21//! kind-specific equivalents like `document(id)`) are replaced
22//! with two flavours:
23//!
24//! - **Owned-return convenience methods** for common patterns
25//! (`document_handle`, `name_of`, `kind_of`, `flags_of`,
26//! `document_dirty`, `document_path`, `entry_summary`).
27//! - **Callback methods** (`with_entry`, `with_document`, etc.)
28//! for one-off access. The callback runs while the lock is
29//! held; callers MUST NOT re-enter the registry from inside
30//! (it would deadlock on the same `Mutex`).
31//!
32//! Help buffers stay overlay-rendered for v1 (transient popup),
33//! so they're not in the registry yet -- moving them in is a
34//! follow-up that doesn't require structural change.
35//!
36//! Hot-path access: the *active* document's actor handle, syntax
37//! state, and last-parsed-version live on [`crate::app::App`]
38//! directly so motion / dispatch code stays unchanged. Switching
39//! the active document snapshots those fields back into the
40//! matching registry entry and loads from the destination's.
41//!
42//! [`Document`]: lattice_core::Document
43//! [`FileTreeBuffer`]: crate::file_tree::FileTreeBuffer
44
45use std::collections::HashMap;
46use std::sync::{Arc, Mutex, MutexGuard};
47
48use crate::buffers::{BufferFlags, BufferId, BufferKind};
49use crate::help::HelpBuffer;
50use lattice_terminal::buffer::TerminalBuffer;
51
52/// Per-document registry payload. Each entry carries the actor
53/// handle plus per-document tree-sitter `Syntax` state, fold
54/// list, and any other "lives with this buffer until it
55/// closes" state.
56///
57/// **Active vs inactive split.** The currently-active buffer's
58/// `syntax` / `folds` slots are conventionally `None` / empty
59/// because the live state lives on `App.syntax` / `App.folds`
60/// for hot-path access. Switching buffers via
61/// `App::activate_document` snapshots the old buffer's live
62/// state into its entry, then loads the destination's state
63/// from its entry into the App's hot-path fields. The
64/// `App::activate_buffer_state` hook then refreshes anything
65/// that needs recomputing for the newly-active buffer (e.g.
66/// fold recompute when switching into a buffer for the first
67/// time).
68#[derive(Debug)]
69pub struct DocumentEntry {
70 pub id: BufferId,
71 /// M.0 (2026-05-31): typed as `Arc<dyn Document>` so the
72 /// registry can hold either a regular `RopeDocumentHandle`
73 /// (today) or a `MultibufferDocumentHandle` (M.1) without
74 /// kind-branching at retrieval. Consumers read through
75 /// the `Document` trait directly; concrete-type access
76 /// (e.g., `RopeDocumentHandle::replace`-equivalent operations
77 /// that don't apply to multibuffer) is gone — slot
78 /// replacement / membership APIs handle those cases.
79 pub handle: std::sync::Arc<dyn lattice_runtime::Document>,
80 // M.3.2.c.5: `syntax`, `last_parsed_text_version`,
81 // `last_synced_syntax_version`, and `folds` retired off the
82 // entry. They live in `App.buffer_locals[id]` as
83 // [`crate::modes::DocumentSyntax`] /
84 // [`crate::modes::DocumentLastParsedTextVersion`] /
85 // [`crate::modes::DocumentLastSyncedSyntaxVersion`] /
86 // [`crate::modes::DocumentFolds`]. Reads route through
87 // `App::document_syntax_for` and friends; writes go through
88 // `App::seed_document_entry_locals` /
89 // `App::seed_active_document_locals` at the activation
90 // boundary helpers in `app/lifecycle.rs`.
91}
92
93/// One slot in the registry. The kind-specific data lives in
94/// [`BufferData`]; flags + id + name apply uniformly.
95///
96/// `name` is the buffer's synthetic display label when there is
97/// no physical file backing it. For path-backed Documents it
98/// stays `None` and the status line / picker fall back to the
99/// path. For synthetic buffers like `*lsp*`, `*messages*`, or
100/// `*lsp:rust-analyzer:lattice*`, the owning subsystem sets the
101/// name at construction time; the status line + `:ls` + buffer
102/// picker all surface that label uniformly. Help buffers carry
103/// their own `title` field (used by help-mode link / anchor
104/// machinery) and don't use `name`.
105#[derive(Debug)]
106pub struct BufferEntry {
107 pub id: BufferId,
108 pub flags: BufferFlags,
109 pub data: BufferData,
110 pub name: Option<String>,
111}
112
113impl BufferEntry {
114 pub fn kind(&self) -> BufferKind {
115 match &self.data {
116 BufferData::Document(_) => BufferKind::Document,
117 BufferData::FileTree(_) => BufferKind::FileTree,
118 BufferData::Help(_) => BufferKind::Help,
119 BufferData::Oil(_) => BufferKind::Oil,
120 BufferData::Terminal(_) => BufferKind::Terminal,
121 BufferData::Messages(_) => BufferKind::Messages,
122 BufferData::Multibuffer(_) => BufferKind::Multibuffer,
123 BufferData::Dashboard(_) => BufferKind::Dashboard,
124 }
125 }
126
127 /// The Document storage for any document-shaped kind.
128 /// Returns `Some` for [`BufferData::Document`],
129 /// [`BufferData::Messages`], [`BufferData::Multibuffer`], and
130 /// (PU.1a) [`BufferData::Help`] — their storage is identical
131 /// (all carry `Arc<dyn Document>`); `None` for other kinds.
132 /// Code that needs to distinguish branches on [`Self::kind`].
133 pub fn document(&self) -> Option<&DocumentEntry> {
134 match &self.data {
135 BufferData::Document(d)
136 | BufferData::Messages(d)
137 | BufferData::Multibuffer(d)
138 | BufferData::Help(d)
139 | BufferData::FileTree(d)
140 | BufferData::Oil(d)
141 | BufferData::Dashboard(d) => Some(d),
142 _ => None,
143 }
144 }
145
146 pub fn document_mut(&mut self) -> Option<&mut DocumentEntry> {
147 match &mut self.data {
148 BufferData::Document(d)
149 | BufferData::Messages(d)
150 | BufferData::Multibuffer(d)
151 | BufferData::Help(d)
152 | BufferData::FileTree(d)
153 | BufferData::Oil(d)
154 | BufferData::Dashboard(d) => Some(d),
155 _ => None,
156 }
157 }
158
159 pub fn terminal(&self) -> Option<&TerminalBuffer> {
160 match &self.data {
161 BufferData::Terminal(t) => Some(t),
162 _ => None,
163 }
164 }
165
166 pub fn terminal_mut(&mut self) -> Option<&mut TerminalBuffer> {
167 match &mut self.data {
168 BufferData::Terminal(t) => Some(t),
169 _ => None,
170 }
171 }
172}
173
174/// DL.4/DL.5: which listing kind a new listing Document is filed
175/// under. Both are `DocumentEntry`-backed; only the discriminator
176/// differs, which is exactly the PU.1a shape.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum ListingKind {
179 FileTree,
180 Oil,
181}
182
183#[derive(Debug)]
184pub enum BufferData {
185 Document(DocumentEntry),
186 /// Hierarchical filesystem tree.
187 ///
188 /// DL.4: storage is now identical to [`BufferData::Document`] — an
189 /// actor-backed synthetic Document — exactly as `Help` converged at
190 /// PU.1a. The discriminator stays so `:ls`, mode lookup and
191 /// `BufferKind::is_read_only` can still tell a tree from a file.
192 ///
193 /// It was its own struct with its own rope, which is why
194 /// `document_handle()` returned `None` for it and the generic pane
195 /// path could not render it at all — the reason four bespoke paint
196 /// functions existed (CV.5, CV.6).
197 FileTree(DocumentEntry),
198 /// Help / log / picker-listing buffers placed into a pane
199 /// (DESIGN.md §5.9, §5.11). The transient overlay path
200 /// (`App.popup_buffer`) remains for popup-style displays
201 /// (hover, doc lookups, error toasts); persistent help views
202 /// (`:lsp-log`, `:lsp-server-log`, `:lsp-trace-log`,
203 /// `:describe-*`, `:diagnostics`) route here so they live in
204 /// a real pane, can be split, switched, listed via `:ls`,
205 /// and updated live when their backing source emits events.
206 ///
207 /// PU.1a (2026-06-27): storage is now identical to
208 /// [`BufferData::Document`] (a [`DocumentEntry`] carrying
209 /// `Arc<dyn Document>`) — help content is an actor-backed
210 /// synthetic Document, exactly like [`BufferData::Messages`].
211 /// The discriminator stays so help-mode behaviours (links,
212 /// anchors, dismiss-on-Esc, the markdown major) and `:ls` /
213 /// introspection can tell a help view apart from a file. The
214 /// help title lives in [`BufferEntry::name`]; per-buffer
215 /// metadata (links / anchors / highlights) lives in
216 /// `buffer_locals`.
217 Help(DocumentEntry),
218 /// Flat editable directory listing (oil.nvim-style).
219 ///
220 /// DL.5: storage is a `DocumentEntry` like every other kind — the
221 /// last one to converge. Its rope is an actor-backed Document, and
222 /// the directory snapshot `:w` diffs against lives in the
223 /// `OilSnapshotLocal` buffer-local. Oil is **writable**, so it
224 /// needs no owner-write bypass: edits go through the ordinary
225 /// document path.
226 Oil(DocumentEntry),
227 Terminal(TerminalBuffer),
228 /// The editor's `*messages*` audit transcript. Storage is
229 /// identical to [`BufferData::Document`] (same
230 /// [`DocumentEntry`]); the discriminator exists so `:ls`,
231 /// modeline, and introspection paths can tell the transcript
232 /// apart from a user-edited file. The subsystem owns content;
233 /// `messages-mode` contributes `ReadOnly` + `NoFile`.
234 Messages(DocumentEntry),
235 /// **H.1 (2026-05-31):** composed multibuffer view buffer
236 /// (`lattice-multibuffer`). Storage is identical to
237 /// [`BufferData::Document`] (the `DocumentEntry`'s
238 /// `handle: Arc<dyn Document>` is a
239 /// `MultibufferDocumentHandle` impl). The discriminator
240 /// exists so `:ls` / picker display / mode lookup can
241 /// distinguish a composed view from a regular Document.
242 /// `MultibufferMode` (M.2.b.2) is the major mode; H.3's
243 /// `Event::BufferOpened` dispatch activates it.
244 Multibuffer(DocumentEntry),
245 /// The `*dashboard*` launch page (DB.2). Storage is identical to
246 /// [`BufferData::Document`] (same [`DocumentEntry`]); the discriminator
247 /// exists so `:ls` / introspection tell the dashboard apart from a file,
248 /// and so the follow / dismiss gates group it with help-style buffers.
249 /// `dashboard-mode` is the major mode; it contributes `ReadOnly` +
250 /// `NoFile`. See `docs/dev/architecture/dashboard.md` §9.2.
251 Dashboard(DocumentEntry),
252}
253
254#[derive(Debug, Default)]
255struct BufferRegistryInner {
256 by_id: HashMap<BufferId, BufferEntry>,
257}
258
259/// The App's buffer registry. Methods take `&self` and lock
260/// internally; the registry is `Clone` so the App's
261/// `BufferStore` service impl can hold a clone for cross-thread
262/// access.
263///
264/// Perf plan B.4.b: carries an `AtomicU64` version counter
265/// alongside the inner. Every mutating method (`insert`, `remove`,
266/// `set_flags`, `set_name`, and every `with_*_mut` closure
267/// accessor) bumps it. `Versioned<T>`'s `DerefMut` discipline can't
268/// fire here because the registry uses interior mutability —
269/// callers take `&self` rather than `&mut self`, so autoref through
270/// `DerefMut` is impossible. The atomic is shared via the same
271/// `Arc<...>` clone as `inner` so every registry handle sees the
272/// same counter; `Clone` is one Arc bump for the pair.
273///
274/// `version()` is the read API. The `BuffersRenderState` /
275/// `TabsRenderState` caches on `Editor::publish_cache` compare
276/// against the prior captured value to decide cache reuse vs.
277/// rebuild.
278#[derive(Clone, Debug, Default)]
279pub struct BufferRegistry {
280 inner: Arc<Mutex<BufferRegistryInner>>,
281 /// Perf plan B.4.b: monotonic version. Bumped by every
282 /// mutating method on `BufferRegistry`. `AtomicU64` is
283 /// `Default` (=0) so this slot composes with the existing
284 /// `#[derive(Default)]` without an explicit `Default` impl.
285 version: Arc<std::sync::atomic::AtomicU64>,
286}
287
288impl BufferRegistry {
289 /// Perf plan B.4.b: monotonic mutation counter. Read by the
290 /// publish cache (see [`crate::render_state::PublishCache`])
291 /// to decide whether the cached `buffers` / `tabs` Arcs can be
292 /// reused across publishes. `Relaxed` ordering is fine because
293 /// the publish path runs on the actor thread; we only need the
294 /// counter to advance after a mutation, not to synchronise
295 /// memory order with other writers.
296 pub fn version(&self) -> u64 {
297 self.version.load(std::sync::atomic::Ordering::Relaxed)
298 }
299
300 #[inline]
301 fn bump_version(&self) {
302 self.version
303 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
304 }
305}
306
307fn lock_inner(inner: &Arc<Mutex<BufferRegistryInner>>) -> MutexGuard<'_, BufferRegistryInner> {
308 inner.lock().expect("BufferRegistry mutex poisoned")
309}
310
311impl BufferRegistry {
312 pub fn new() -> Self {
313 Self::default()
314 }
315
316 // ---- Mutation ----------------------------------------
317
318 pub fn insert(&self, entry: BufferEntry) {
319 lock_inner(&self.inner).by_id.insert(entry.id, entry);
320 // Perf plan B.4.b: bump after the mutation so the publish
321 // cache sees the new state on its next sub-state check.
322 self.bump_version();
323 }
324
325 pub fn remove(&self, id: BufferId) -> Option<BufferEntry> {
326 let removed = lock_inner(&self.inner).by_id.remove(&id);
327 // Only bump on an actual removal so a no-op `remove` of a
328 // non-existent id doesn't invalidate the cache.
329 if removed.is_some() {
330 self.bump_version();
331 }
332 removed
333 }
334
335 // ---- Owned-return reads ------------------------------
336
337 pub fn contains(&self, id: BufferId) -> bool {
338 lock_inner(&self.inner).by_id.contains_key(&id)
339 }
340
341 pub fn len(&self) -> usize {
342 lock_inner(&self.inner).by_id.len()
343 }
344
345 pub fn is_empty(&self) -> bool {
346 lock_inner(&self.inner).by_id.is_empty()
347 }
348
349 /// Kind of the entry at `id`, or `None` if absent.
350 pub fn kind_of(&self, id: BufferId) -> Option<BufferKind> {
351 lock_inner(&self.inner).by_id.get(&id).map(|e| e.kind())
352 }
353
354 /// Synthetic name of the entry at `id`, or `None` if absent
355 /// or if the entry has no name set.
356 pub fn name_of(&self, id: BufferId) -> Option<String> {
357 lock_inner(&self.inner)
358 .by_id
359 .get(&id)
360 .and_then(|e| e.name.clone())
361 }
362
363 /// Per-buffer flags. `BufferFlags` is `Copy` so this returns
364 /// owned without locking issues.
365 pub fn flags_of(&self, id: BufferId) -> Option<BufferFlags> {
366 lock_inner(&self.inner).by_id.get(&id).map(|e| e.flags)
367 }
368
369 /// Mutate the per-buffer flags via callback (e.g. flip
370 /// `listed` on `:setlocal nobuflisted` once that lands).
371 pub fn set_flags(&self, id: BufferId, flags: BufferFlags) -> bool {
372 let mut inner = lock_inner(&self.inner);
373 let updated = match inner.by_id.get_mut(&id) {
374 Some(e) => {
375 e.flags = flags;
376 true
377 }
378 None => false,
379 };
380 // Drop the lock before bumping the atomic — the version is
381 // a separate Arc and doesn't need the inner lock.
382 drop(inner);
383 if updated {
384 self.bump_version();
385 }
386 updated
387 }
388
389 /// Rename the entry's synthetic `name` slot. Used by the
390 /// supervisor when an actor exits and the per-instance buffer
391 /// gets renamed `*lsp:rust:/path*` → `*lsp:rust:/path (exited)*`.
392 pub fn set_name(&self, id: BufferId, name: Option<String>) -> bool {
393 let mut inner = lock_inner(&self.inner);
394 let updated = match inner.by_id.get_mut(&id) {
395 Some(e) => {
396 e.name = name;
397 true
398 }
399 None => false,
400 };
401 drop(inner);
402 if updated {
403 self.bump_version();
404 }
405 updated
406 }
407
408 /// Kind-specific convenience: clone the `Arc<dyn Document>`
409 /// for `id`. The handle is `Send + Sync` and can be held
410 /// across thread boundaries. M.0: returns the polymorphic
411 /// shape (was `RopeDocumentHandle` pre-M.0) so the registry
412 /// serves multibuffer handles (M.1) through the same path.
413 pub fn document_handle(
414 &self,
415 id: BufferId,
416 ) -> Option<std::sync::Arc<dyn lattice_runtime::Document>> {
417 lock_inner(&self.inner)
418 .by_id
419 .get(&id)
420 .and_then(|e| e.document().map(|d| d.handle.clone()))
421 }
422
423 /// D.3.a.1 (2026-05-29): reverse lookup —
424 /// `DocumentId` → `BufferId`. Used by the
425 /// diff subsystem's `DocumentBufferResolver` impl: bus
426 /// events carry `DocumentId`, host-side state is keyed by
427 /// `BufferId`. Scans the registry's document entries —
428 /// O(N_documents) per call, acceptable at v1 buffer counts
429 /// (~tens; the LSP fan-in and the keymap registry do
430 /// similar walks). Future inverse index lives behind the
431 /// same method signature.
432 pub fn buffer_id_for_document(
433 &self,
434 document_id: lattice_protocol::ids::DocumentId,
435 ) -> Option<BufferId> {
436 let inner = lock_inner(&self.inner);
437 for (buffer_id, entry) in inner.by_id.iter() {
438 if let Some(d) = entry.document()
439 && d.handle.id() == document_id
440 {
441 return Some(*buffer_id);
442 }
443 }
444 None
445 }
446
447 /// Kind-specific convenience: path of the document at `id`.
448 pub fn document_path(&self, id: BufferId) -> Option<std::path::PathBuf> {
449 lock_inner(&self.inner)
450 .by_id
451 .get(&id)
452 .and_then(|e| e.document().and_then(|d| d.handle.path()))
453 }
454
455 /// Kind-specific convenience: dirty flag for the document
456 /// at `id`. Returns `false` for absent / non-document.
457 pub fn document_dirty(&self, id: BufferId) -> bool {
458 lock_inner(&self.inner)
459 .by_id
460 .get(&id)
461 .and_then(|e| e.document().map(|d| d.handle.dirty()))
462 .unwrap_or(false)
463 }
464
465 pub fn contains_document(&self, id: BufferId) -> bool {
466 // H.1 / M.6.X follow-up (2026-06-01): Multibuffer
467 // entries are document-backed (`document()` at line 137
468 // already returns `Some` for them), so the activation
469 // pipeline that gates on this predicate must accept
470 // them. Without this, `:b N` on a multibuffer view
471 // bounces with "buffer #N not a document" even though
472 // the registry can hand back a usable document handle.
473 // Aligned with `feedback_buffers_no_special_case` —
474 // Multibuffer is uniform with Document/Messages at the
475 // activation seam.
476 lock_inner(&self.inner)
477 .by_id
478 .get(&id)
479 // DL.4: DERIVED from `document()` rather than re-listing the
480 // variants. The hand-maintained list had already drifted —
481 // it predated `Multibuffer` being added to `document()` and
482 // then silently excluded `FileTree` when that converged, so
483 // `activate_document` refused a buffer whose handle the
484 // registry would happily hand out. A tree opened, painted
485 // the buffer behind it, and put its caret on row 0.
486 //
487 // Help is the one deliberate exception: it is
488 // document-backed (PU.1a) but activates through the popup /
489 // in-pane help path, not this one. Named here rather than
490 // omitted, so the exclusion is a decision and not a gap.
491 .map(|e| e.document().is_some() && !matches!(e.data, BufferData::Help(_)))
492 .unwrap_or(false)
493 }
494
495 /// True iff the entry at `id` is the `*messages*` transcript.
496 /// Symmetric with [`Self::contains_help`] /
497 /// [`Self::contains_oil`] / [`Self::contains_file_tree`] —
498 /// callers that need to differentiate the Messages identity
499 /// from a regular Document branch on this.
500 pub fn contains_messages(&self, id: BufferId) -> bool {
501 lock_inner(&self.inner)
502 .by_id
503 .get(&id)
504 .map(|e| matches!(e.data, BufferData::Messages(_)))
505 .unwrap_or(false)
506 }
507
508 pub fn contains_file_tree(&self, id: BufferId) -> bool {
509 lock_inner(&self.inner)
510 .by_id
511 .get(&id)
512 .map(|e| matches!(e.data, BufferData::FileTree(_)))
513 .unwrap_or(false)
514 }
515
516 pub fn contains_help(&self, id: BufferId) -> bool {
517 lock_inner(&self.inner)
518 .by_id
519 .get(&id)
520 .map(|e| matches!(e.data, BufferData::Help(_)))
521 .unwrap_or(false)
522 }
523
524 pub fn contains_oil(&self, id: BufferId) -> bool {
525 lock_inner(&self.inner)
526 .by_id
527 .get(&id)
528 .map(|e| matches!(e.data, BufferData::Oil(_)))
529 .unwrap_or(false)
530 }
531
532 /// Compact snapshot of an entry: `(id, kind, flags, name,
533 /// is_document_path_set)`. Used by `:ls` and the buffer picker
534 /// to render rows without holding the lock through complex
535 /// display logic. Returns `None` if the entry is absent.
536 pub fn entry_summary(
537 &self,
538 id: BufferId,
539 ) -> Option<(BufferId, BufferKind, BufferFlags, Option<String>)> {
540 let inner = lock_inner(&self.inner);
541 inner
542 .by_id
543 .get(&id)
544 .map(|e| (e.id, e.kind(), e.flags, e.name.clone()))
545 }
546
547 /// All ids in ascending order. Used by `:bn` / `:bp` for
548 /// deterministic cycling order independent of HashMap
549 /// hash-randomization.
550 pub fn sorted_ids(&self) -> Vec<BufferId> {
551 let inner = lock_inner(&self.inner);
552 let mut ids: Vec<BufferId> = inner.by_id.keys().copied().collect();
553 ids.sort();
554 ids
555 }
556
557 /// All listed ids in ascending order. `:bn` / `:bp` skip
558 /// unlisted buffers (vim semantics); `:ls` shows them under a
559 /// separate header (post-v1 polish).
560 pub fn listed_ids_sorted(&self) -> Vec<BufferId> {
561 let inner = lock_inner(&self.inner);
562 // PU.5: ephemeral popup-backing buffers are never listed (they set
563 // `listed: false`, but gate on `ephemeral` too so the invariant
564 // holds even if a future caller sets `listed: true` by mistake).
565 let mut ids: Vec<BufferId> = inner
566 .by_id
567 .iter()
568 .filter(|(_, e)| e.flags.listed && !e.flags.ephemeral)
569 .map(|(id, _)| *id)
570 .collect();
571 ids.sort();
572 ids
573 }
574
575 /// Document buffers only, sorted by id. The `*messages*`
576 /// transcript stores as [`BufferData::Messages`] (see
577 /// [`Self::messages_ids_sorted`]) so it is **excluded** from
578 /// this list even though storage is identical; callers that
579 /// want every rope-backed-doc kind should walk both.
580 pub fn document_ids_sorted(&self) -> Vec<BufferId> {
581 let inner = lock_inner(&self.inner);
582 let mut ids: Vec<BufferId> = inner
583 .by_id
584 .iter()
585 .filter(|(_, e)| matches!(e.data, BufferData::Document(_)))
586 .map(|(id, _)| *id)
587 .collect();
588 ids.sort();
589 ids
590 }
591
592 /// `*messages*` (and any future Messages-kind) buffers,
593 /// sorted by id. Symmetric with [`Self::document_ids_sorted`].
594 pub fn messages_ids_sorted(&self) -> Vec<BufferId> {
595 let inner = lock_inner(&self.inner);
596 let mut ids: Vec<BufferId> = inner
597 .by_id
598 .iter()
599 .filter(|(_, e)| matches!(e.data, BufferData::Messages(_)))
600 .map(|(id, _)| *id)
601 .collect();
602 ids.sort();
603 ids
604 }
605
606 /// K.4.8 (2026-06-02): Multibuffer-kind buffers, sorted by id.
607 /// Symmetric with [`Self::messages_ids_sorted`] /
608 /// [`Self::document_ids_sorted`]. Used by `:ls` to count the
609 /// Multibuffer entries separately from Messages now that
610 /// Multibuffer has its own listing row.
611 pub fn multibuffer_ids_sorted(&self) -> Vec<BufferId> {
612 let inner = lock_inner(&self.inner);
613 let mut ids: Vec<BufferId> = inner
614 .by_id
615 .iter()
616 .filter(|(_, e)| matches!(e.data, BufferData::Multibuffer(_)))
617 .map(|(id, _)| *id)
618 .collect();
619 ids.sort();
620 ids
621 }
622
623 /// File-tree buffers only, sorted by id.
624 pub fn file_tree_ids_sorted(&self) -> Vec<BufferId> {
625 let inner = lock_inner(&self.inner);
626 let mut ids: Vec<BufferId> = inner
627 .by_id
628 .iter()
629 .filter(|(_, e)| matches!(e.data, BufferData::FileTree(_)))
630 .map(|(id, _)| *id)
631 .collect();
632 ids.sort();
633 ids
634 }
635
636 /// Help buffers only, sorted by id.
637 pub fn help_ids_sorted(&self) -> Vec<BufferId> {
638 let inner = lock_inner(&self.inner);
639 let mut ids: Vec<BufferId> = inner
640 .by_id
641 .iter()
642 .filter(|(_, e)| matches!(e.data, BufferData::Help(_)))
643 .map(|(id, _)| *id)
644 .collect();
645 ids.sort();
646 ids
647 }
648
649 pub fn oil_ids_sorted(&self) -> Vec<BufferId> {
650 let inner = lock_inner(&self.inner);
651 let mut ids: Vec<BufferId> = inner
652 .by_id
653 .iter()
654 .filter(|(_, e)| matches!(e.data, BufferData::Oil(_)))
655 .map(|(id, _)| *id)
656 .collect();
657 ids.sort();
658 ids
659 }
660
661 /// IDs of every registered file-tree buffer, in arbitrary
662 /// order. The App-side `file_tree_with_root` walks these +
663 /// probes each one's `FileTreeRoot` buffer-local for the
664 /// dedup lookup; the registry can't do that walk because it
665 /// doesn't own buffer-locals.
666 pub fn file_tree_ids(&self) -> Vec<BufferId> {
667 lock_inner(&self.inner)
668 .by_id
669 .values()
670 .filter_map(|entry| match &entry.data {
671 BufferData::FileTree(_) => Some(entry.id),
672 _ => None,
673 })
674 .collect()
675 }
676
677 /// IDs of every registered oil buffer, in arbitrary order.
678 pub fn oil_ids(&self) -> Vec<BufferId> {
679 lock_inner(&self.inner)
680 .by_id
681 .values()
682 .filter_map(|entry| match &entry.data {
683 BufferData::Oil(_) => Some(entry.id),
684 _ => None,
685 })
686 .collect()
687 }
688
689 /// First buffer whose `name` matches exactly. Used by
690 /// subsystem-owned synthetic buffers (`*lsp*`, `*messages*`,
691 /// per-instance LSP log buffers) so re-running the
692 /// owner's create-or-activate path surfaces the existing
693 /// entry rather than allocating a duplicate.
694 pub fn by_name(&self, name: &str) -> Option<BufferId> {
695 let inner = lock_inner(&self.inner);
696 for entry in inner.by_id.values() {
697 if entry.name.as_deref() == Some(name) {
698 return Some(entry.id);
699 }
700 }
701 None
702 }
703
704 /// First help buffer with the given title, if any. Used by the
705 /// `:lsp-log` / `:lsp-trace-log` openers so re-running the
706 /// command surfaces the existing buffer rather than allocating
707 /// a duplicate.
708 pub fn help_with_title(&self, title: &str) -> Option<BufferId> {
709 let inner = lock_inner(&self.inner);
710 for entry in inner.by_id.values() {
711 // PU.1a: the help title now lives in `entry.name` (the
712 // synthetic-Document name slot), not on the storage.
713 if matches!(entry.data, BufferData::Help(_)) && entry.name.as_deref() == Some(title) {
714 return Some(entry.id);
715 }
716 }
717 None
718 }
719
720 /// First document buffer with the given path, if any. Used by
721 /// `:e FILE` to detect "already open".
722 pub fn document_with_path(&self, path: &std::path::Path) -> Option<BufferId> {
723 let inner = lock_inner(&self.inner);
724 for entry in inner.by_id.values() {
725 if let BufferData::Document(d) = &entry.data
726 && d.handle.path() == Some(path.to_path_buf())
727 {
728 return Some(entry.id);
729 }
730 }
731 None
732 }
733
734 // ---- Callback access ---------------------------------
735 //
736 // Each `with_*` method locks, looks up the entry, runs the
737 // callback while the lock is held, releases. Callers MUST
738 // NOT re-enter the registry from inside the callback (it
739 // would deadlock on the same `Mutex`). Use the owned-return
740 // helpers above to extract data when re-entry would
741 // otherwise be needed.
742
743 /// Run `f` against the `BufferEntry` at `id` while holding
744 /// the registry lock. Returns `None` if the entry is absent.
745 pub fn with_entry<R>(&self, id: BufferId, f: impl FnOnce(&BufferEntry) -> R) -> Option<R> {
746 let inner = lock_inner(&self.inner);
747 inner.by_id.get(&id).map(f)
748 }
749
750 /// Mutable variant of [`Self::with_entry`].
751 ///
752 /// Perf plan B.4.b: conservatively bumps the version after the
753 /// closure runs, even if the closure didn't actually mutate.
754 /// Over-bumping causes a one-time cache miss on the next
755 /// publish — safe and bounded — versus the alternative of
756 /// missing a real mutation, which would leave stale Arcs
757 /// visible to renderers indefinitely.
758 pub fn with_entry_mut<R>(
759 &self,
760 id: BufferId,
761 f: impl FnOnce(&mut BufferEntry) -> R,
762 ) -> Option<R> {
763 let result = {
764 let mut inner = lock_inner(&self.inner);
765 inner.by_id.get_mut(&id).map(f)
766 };
767 if result.is_some() {
768 self.bump_version();
769 }
770 result
771 }
772
773 pub fn with_document<R>(&self, id: BufferId, f: impl FnOnce(&DocumentEntry) -> R) -> Option<R> {
774 let inner = lock_inner(&self.inner);
775 inner.by_id.get(&id).and_then(|e| e.document()).map(f)
776 }
777
778 pub fn with_document_mut<R>(
779 &self,
780 id: BufferId,
781 f: impl FnOnce(&mut DocumentEntry) -> R,
782 ) -> Option<R> {
783 // Perf plan B.4.b: document-entry mutations don't affect
784 // the published `BuffersRenderState` fields directly (kind /
785 // name / flags). They do invalidate the registry-derived
786 // tabs label only if a name changes — which goes through
787 // the dedicated `set_name`, not through `with_document_mut`.
788 // We still bump because plugin code could conceivably edit
789 // the wrong field through a closure; conservatism is cheaper
790 // than a hard-to-find staleness bug.
791 let result = {
792 let mut inner = lock_inner(&self.inner);
793 inner
794 .by_id
795 .get_mut(&id)
796 .and_then(|e| e.document_mut())
797 .map(f)
798 };
799 if result.is_some() {
800 self.bump_version();
801 }
802 result
803 }
804
805 /// PU.1a: reconstruct a transient [`HelpBuffer`] *view* from the
806 /// registry Document at `id`. Help content is now an actor-backed
807 /// synthetic Document; this builds the slim view value the bespoke
808 /// help renderers still consume (PU.1b deletes both). The view's
809 /// `scroll` / `cursor` default to 0 — callers overlay the live
810 /// popup (`popup_scroll`/`popup_cursor`) or pane scroll+cursor.
811 /// `None` when `id` is not a help buffer.
812 pub fn help_content_view(&self, id: BufferId) -> Option<HelpBuffer> {
813 let inner = lock_inner(&self.inner);
814 let entry = inner.by_id.get(&id)?;
815 if !matches!(entry.data, BufferData::Help(_)) {
816 return None;
817 }
818 let snap = entry.document()?.handle.snapshot();
819 Some(HelpBuffer {
820 id,
821 title: entry.name.clone().unwrap_or_default(),
822 content: snap.buffer.clone(),
823 scroll: 0,
824 cursor: lattice_protocol::position::Position::ZERO,
825 })
826 }
827
828 pub fn with_terminal<R>(
829 &self,
830 id: BufferId,
831 f: impl FnOnce(&TerminalBuffer) -> R,
832 ) -> Option<R> {
833 let inner = lock_inner(&self.inner);
834 inner.by_id.get(&id).and_then(|e| e.terminal()).map(f)
835 }
836
837 pub fn with_terminal_mut<R>(
838 &self,
839 id: BufferId,
840 f: impl FnOnce(&mut TerminalBuffer) -> R,
841 ) -> Option<R> {
842 let result = {
843 let mut inner = lock_inner(&self.inner);
844 inner
845 .by_id
846 .get_mut(&id)
847 .and_then(|e| e.terminal_mut())
848 .map(f)
849 };
850 if result.is_some() {
851 self.bump_version();
852 }
853 result
854 }
855
856 /// Run `f` against every entry under the registry lock.
857 /// Callers must not re-enter the registry from inside.
858 pub fn for_each<F: FnMut(&BufferEntry)>(&self, mut f: F) {
859 let inner = lock_inner(&self.inner);
860 for entry in inner.by_id.values() {
861 f(entry);
862 }
863 }
864}
865
866// ---------------------------------------------------------------
867// BufferStore impl (B'.3)
868// ---------------------------------------------------------------
869//
870// Wraps a clone of `BufferRegistry` so modes can find synthetic
871// buffers by name and pull `RopeDocumentHandle`s from any tokio task.
872// Registered into the App's `ServiceRegistry` at boot; modes pull
873// it via `ctx.service::<lattice_mode::BufferStoreHandle>()`.
874//
875// This handle is read/find + generic document insertion only — buffer
876// *creation* (find-or-create + activate a major) is NOT here, because
877// activating a mode needs `&mut Editor`. Modes provision their own
878// buffers through `ModeActivator::ensure_named_document` (the
879// `&mut`-backed create seam), whose `Editor` impl calls
880// `ensure_named_synthetic_document`.
881
882impl lattice_mode::BufferStore for BufferRegistry {
883 fn find_by_name(&self, name: &str) -> Option<lattice_core::BufferId> {
884 self.by_name(name)
885 }
886
887 fn handle_for(
888 &self,
889 id: lattice_core::BufferId,
890 ) -> Option<std::sync::Arc<dyn lattice_runtime::Document>> {
891 self.document_handle(id)
892 }
893
894 fn name_for(&self, id: lattice_core::BufferId) -> Option<String> {
895 self.name_of(id)
896 }
897
898 /// Answered exactly rather than through the trait's best-effort default:
899 /// this registry holds the map, so it can see a registered buffer that has
900 /// no name, no path and no document handle — which the default reports as
901 /// absent.
902 fn contains_buffer(&self, id: lattice_core::BufferId) -> bool {
903 self.contains(id)
904 }
905
906 fn path_for(&self, id: lattice_core::BufferId) -> Option<std::path::PathBuf> {
907 self.document_path(id)
908 }
909
910 /// H.1 (2026-05-31): generic Document-shaped buffer
911 /// insertion for extension crates (`lattice-multibuffer`
912 /// today; future plugin-defined Document-shaped kinds).
913 /// Maps `kind` to the appropriate `BufferData` variant;
914 /// other kinds (`FileTree`, `Oil`, `Terminal`, `Help`)
915 /// log + skip — their payload is not a Document.
916 fn insert_document_buffer(
917 &self,
918 id: lattice_core::BufferId,
919 kind: lattice_core::BufferKind,
920 handle: std::sync::Arc<dyn lattice_runtime::Document>,
921 flags: lattice_core::BufferFlags,
922 name: Option<String>,
923 ) {
924 // Idempotent: if already registered, return.
925 if self.contains(id) {
926 return;
927 }
928 let entry = DocumentEntry { id, handle };
929 let data = match kind {
930 lattice_core::BufferKind::Document => BufferData::Document(entry),
931 lattice_core::BufferKind::Messages => BufferData::Messages(entry),
932 lattice_core::BufferKind::Multibuffer => BufferData::Multibuffer(entry),
933 other => {
934 // Non-Document-shaped kinds (FileTree, Oil,
935 // Terminal, Help) carry kind-specific payloads;
936 // they're not reachable through this generic
937 // surface. Caller is using the wrong API.
938 tracing::warn!(
939 target: "lattice_host::buffer_registry",
940 ?kind,
941 ?id,
942 "BufferStore::insert_document_buffer called for non-Document kind {:?}; ignoring",
943 other,
944 );
945 return;
946 }
947 };
948 self.insert(BufferEntry {
949 id,
950 flags,
951 data,
952 name,
953 });
954 }
955}
956
957// ---------------------------------------------------------------
958// TerminalStore impl (T-mode-1)
959// ---------------------------------------------------------------
960//
961// Wraps `BufferRegistry` so `TerminalNormalMode`'s on-activate /
962// Guard-Drop can install / clear the SyntheticDoc on a
963// `TerminalBuffer` from inside the mode lifecycle. Registered at
964// boot via `editor_boot.rs` and pulled by the mode via
965// `ctx.service::<lattice_terminal::TerminalStoreHandle>()`.
966
967impl lattice_terminal::TerminalStore for BufferRegistry {
968 fn install_synthetic(&self, id: BufferId) -> bool {
969 self.with_terminal_mut(id, |t| {
970 let doc = t.term.build_normal_snapshot();
971 t.synthetic = Some(std::sync::Arc::new(doc));
972 })
973 .is_some()
974 }
975
976 fn clear_synthetic(&self, id: BufferId) -> bool {
977 self.with_terminal_mut(id, |t| {
978 t.synthetic = None;
979 })
980 .is_some()
981 }
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987
988 fn ft_entry(id: BufferId, listed: bool, name: Option<String>) -> BufferEntry {
989 BufferEntry {
990 id,
991 flags: BufferFlags {
992 listed,
993 hidden: false,
994 ephemeral: false,
995 },
996 // DL.4: a file tree is a `DocumentEntry` now, like Help.
997 data: BufferData::FileTree(DocumentEntry {
998 id,
999 handle: std::sync::Arc::new(lattice_runtime::spawn_document(
1000 id,
1001 lattice_core::Document::empty(),
1002 std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
1003 lattice_grammar::registry::CommandRegistry::default(),
1004 )),
1005 )),
1006 }),
1007 name,
1008 }
1009 }
1010
1011 #[test]
1012 fn fresh_registry_is_empty() {
1013 let r = BufferRegistry::new();
1014 assert!(r.is_empty());
1015 assert_eq!(r.len(), 0);
1016 }
1017
1018 #[test]
1019 fn sorted_ids_returns_ascending_order() {
1020 let r = BufferRegistry::new();
1021 let id_a = BufferId::next();
1022 let id_b = BufferId::next();
1023 let id_c = BufferId::next();
1024 r.insert(ft_entry(id_c, true, None));
1025 r.insert(ft_entry(id_a, true, None));
1026 r.insert(ft_entry(id_b, true, None));
1027 assert_eq!(r.sorted_ids(), vec![id_a, id_b, id_c]);
1028 }
1029
1030 #[test]
1031 fn unlisted_buffers_skip_listed_ids() {
1032 let r = BufferRegistry::new();
1033 let id_a = BufferId::next();
1034 let id_b = BufferId::next();
1035 r.insert(ft_entry(id_a, true, None));
1036 r.insert(ft_entry(id_b, false, None));
1037 assert_eq!(r.listed_ids_sorted(), vec![id_a]);
1038 assert_eq!(r.sorted_ids(), vec![id_a, id_b]);
1039 }
1040
1041 #[test]
1042 fn ephemeral_buffers_excluded_from_listed_ids() {
1043 // PU.5: an ephemeral popup-backing buffer never appears in the
1044 // `:bn` / `:bp` walk (`listed_ids_sorted`), even though it remains
1045 // in the full registry (`sorted_ids`) until its owning popup GCs it.
1046 let r = BufferRegistry::new();
1047 let id_a = BufferId::next();
1048 let id_eph = BufferId::next();
1049 r.insert(ft_entry(id_a, true, None));
1050 let mut eph = ft_entry(id_eph, false, None);
1051 eph.flags.ephemeral = true;
1052 r.insert(eph);
1053 assert_eq!(r.listed_ids_sorted(), vec![id_a]);
1054 assert_eq!(r.sorted_ids(), vec![id_a, id_eph]);
1055 }
1056
1057 #[test]
1058 fn by_name_finds_entry_with_matching_synthetic_name() {
1059 let r = BufferRegistry::new();
1060 let id_lsp = BufferId::next();
1061 let id_other = BufferId::next();
1062 r.insert(ft_entry(id_lsp, true, Some("*lsp*".to_string())));
1063 r.insert(ft_entry(id_other, true, None));
1064 assert_eq!(r.by_name("*lsp*"), Some(id_lsp));
1065 assert_eq!(r.by_name("nope"), None);
1066 }
1067
1068 #[test]
1069 fn file_tree_ids_lists_registered_trees() {
1070 let r = BufferRegistry::new();
1071 let id = BufferId::next();
1072 r.insert(ft_entry(id, true, None));
1073 assert_eq!(r.file_tree_ids(), vec![id]);
1074 }
1075
1076 #[test]
1077 fn clone_shares_state() {
1078 let r1 = BufferRegistry::new();
1079 let r2 = r1.clone();
1080 let id = BufferId::next();
1081 r1.insert(ft_entry(id, true, None));
1082 assert!(r2.contains(id));
1083 assert_eq!(r2.len(), 1);
1084 }
1085
1086 #[test]
1087 fn with_entry_runs_callback_under_lock() {
1088 let r = BufferRegistry::new();
1089 let id = BufferId::next();
1090 r.insert(ft_entry(id, true, Some("name".to_string())));
1091 let kind = r.with_entry(id, |e| e.kind());
1092 assert_eq!(kind, Some(BufferKind::FileTree));
1093 }
1094
1095 #[test]
1096 fn set_name_updates_entry() {
1097 let r = BufferRegistry::new();
1098 let id = BufferId::next();
1099 r.insert(ft_entry(id, true, Some("old".to_string())));
1100 assert!(r.set_name(id, Some("new".to_string())));
1101 assert_eq!(r.name_of(id), Some("new".to_string()));
1102 assert_eq!(r.by_name("new"), Some(id));
1103 assert_eq!(r.by_name("old"), None);
1104 }
1105
1106 // ─────────────────────────────────────────────────────────────
1107 // M.1.b (2026-05-31): `MultibufferDocumentHandle` registers
1108 // through the same `BufferData::Document` slot a regular
1109 // `RopeDocumentHandle` uses. The slot's handle field is
1110 // `Arc<dyn Document>` (M.0 Phase D); multibuffer wraps in via
1111 // the same Arc and is observable through `document_handle(id)`
1112 // / `with_document(id, ...)` / the `BufferStore` trait — no
1113 // kind-branching at the registry boundary.
1114 // ─────────────────────────────────────────────────────────────
1115
1116 #[tokio::test(flavor = "multi_thread")]
1117 async fn multibuffer_registers_through_document_slot() {
1118 use lattice_multibuffer::{Excerpt, MultibufferDocumentHandle};
1119 use lattice_runtime::spawn_document;
1120 use std::collections::HashMap;
1121
1122 // Two source documents.
1123 let registry = std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
1124 lattice_grammar::CommandRegistry::new(),
1125 ));
1126 let s1 = spawn_document(
1127 lattice_core::BufferId(0),
1128 lattice_core::Document::from_text("alpha\nbeta\ngamma\n"),
1129 registry.clone(),
1130 );
1131 let s2 = spawn_document(
1132 lattice_core::BufferId(0),
1133 lattice_core::Document::from_text("delta\nepsilon\nzeta\n"),
1134 registry.clone(),
1135 );
1136
1137 // Reserve BufferIds for the sources; register them so the
1138 // registry round-trips correctly.
1139 let s1_id = BufferId::next();
1140 let s2_id = BufferId::next();
1141 let r = BufferRegistry::new();
1142 let s1_handle: std::sync::Arc<dyn lattice_runtime::Document> = std::sync::Arc::new(s1);
1143 let s2_handle: std::sync::Arc<dyn lattice_runtime::Document> = std::sync::Arc::new(s2);
1144 r.insert(BufferEntry {
1145 id: s1_id,
1146 flags: BufferFlags::default(),
1147 data: BufferData::Document(DocumentEntry {
1148 id: s1_id,
1149 handle: s1_handle.clone(),
1150 }),
1151 name: None,
1152 });
1153 r.insert(BufferEntry {
1154 id: s2_id,
1155 flags: BufferFlags::default(),
1156 data: BufferData::Document(DocumentEntry {
1157 id: s2_id,
1158 handle: s2_handle.clone(),
1159 }),
1160 name: None,
1161 });
1162
1163 // Build a multibuffer composing the two sources.
1164 let mut sources: HashMap<BufferId, std::sync::Arc<dyn lattice_runtime::Document>> =
1165 HashMap::new();
1166 sources.insert(s1_id, s1_handle);
1167 sources.insert(s2_id, s2_handle);
1168 let excerpts = vec![Excerpt::new(s1_id, 0, 1), Excerpt::new(s2_id, 1, 2)];
1169 let mb = MultibufferDocumentHandle::new(
1170 sources,
1171 excerpts,
1172 std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
1173 lattice_grammar::CommandRegistry::new(),
1174 )),
1175 )
1176 .expect("mb constructs");
1177 let mb_buffer_id = mb.buffer_id();
1178 let mb_handle: std::sync::Arc<dyn lattice_runtime::Document> = std::sync::Arc::new(mb);
1179
1180 // H.1 (2026-05-31): register through the generic
1181 // `BufferStore::insert_document_buffer` trait method —
1182 // extension crates' canonical path. Maps
1183 // `BufferKind::Multibuffer` to `BufferData::Multibuffer`
1184 // internally.
1185 use lattice_mode::BufferStore;
1186 r.insert_document_buffer(
1187 mb_buffer_id,
1188 BufferKind::Multibuffer,
1189 mb_handle.clone(),
1190 BufferFlags::default(),
1191 Some("*search-results*".to_string()),
1192 );
1193
1194 // The registry exposes the multibuffer through the same
1195 // public API as any other document. Kind now correctly
1196 // returns Multibuffer (was Document before H.1 because
1197 // M.2.b.1 had no `BufferData::Multibuffer` variant).
1198 assert!(r.contains(mb_buffer_id));
1199 assert_eq!(r.kind_of(mb_buffer_id), Some(BufferKind::Multibuffer));
1200 assert_eq!(
1201 r.name_of(mb_buffer_id),
1202 Some("*search-results*".to_string())
1203 );
1204
1205 // `document_handle` returns the trait-object handle —
1206 // reading through it yields the composed snapshot.
1207 let handle_from_registry = r
1208 .document_handle(mb_buffer_id)
1209 .expect("multibuffer reachable via document_handle");
1210 let composed = handle_from_registry.text();
1211 assert_eq!(composed, "alpha\nbeta\nepsilon\nzeta\n");
1212
1213 // `BufferStore::handle_for` returns the same handle —
1214 // the polymorphic surface that mode-owned tasks
1215 // (lattice-lsp, future plugin modes) consume.
1216 let store_handle = r
1217 .handle_for(mb_buffer_id)
1218 .expect("multibuffer reachable via BufferStore");
1219 assert_eq!(store_handle.text(), composed);
1220 }
1221}