Skip to main content

lattice_mode/
buffer_store.rs

1//! `BufferStore`: host primitive for mode-owned buffer lifecycles.
2//!
3//! Modes that synthesize their own buffers (LSP log family,
4//! `*messages*`, future `*scratch*`, plugin-installed log streams)
5//! need a thread-safe way to:
6//!
7//! - Find a buffer by its synthetic name (idempotency check).
8//! - Create a new Document with a given major mode active on it.
9//! - Look up a Document's handle so the mode can append to it from
10//!   a background tokio task.
11//!
12//! Why a trait + handle pair: the concrete buffer registry lives
13//! in the renderer crate (`lattice-ui-tui::buffer_registry`), but
14//! mode crates (`lattice-lsp`, eventually plugin modes) shouldn't
15//! depend on the renderer. The trait carries the host-side
16//! contract; the renderer registers an `Arc<dyn BufferStore>` into
17//! [`crate::ServiceRegistry`] at App boot; modes pull
18//! `Arc<BufferStoreHandle>` via `ctx.service::<BufferStoreHandle>()`
19//! and call through it.
20//!
21//! ## Threading model
22//!
23//! Operations on the trait are `&self`; the implementation is
24//! responsible for whatever synchronisation it needs (the
25//! `lattice-ui-tui` impl wraps the relevant App state in
26//! `Arc<Mutex<...>>`). Modes can call from any thread — the App
27//! thread inside `on_activate`, a tokio task draining an event
28//! subscription, etc.
29//!
30//! ## Read / write surface
31//!
32//! - [`BufferStore::find_by_name`] — read-only registry probe.
33//! - [`BufferStore::handle_for`] — clone of the actor handle so a
34//!   mode can write to the buffer from outside the App's borrow.
35//! - [`BufferStore::insert_document_buffer`] — generic Document-shaped
36//!   insertion (multibuffer kinds).
37//!
38//! **Buffer *creation* (find-or-create + activate a major) is NOT on
39//! this trait.** Activating a mode mutates the mode registry /
40//! active-modes / options cache, which needs `&mut Editor`;
41//! `BufferStore` is `&self` (thread-safe). The reliable, mode-owned
42//! creation seam is [`crate::ModeActivator::ensure_named_document`]
43//! (`&mut`-backed) — a mode / provider provisions its own buffer there.
44//!
45//! Mode authors call these from their lifecycle hooks. The
46//! mode-ownership contract: a mode that creates a synthetic
47//! buffer is the only thing that writes to it (subsystem writes
48//! via the handle; user writes are gated by `ReadOnly`).
49
50use std::sync::Arc;
51
52use lattice_core::{BufferFlags, BufferId, BufferKind};
53
54/// Host-implemented trait. The renderer crate provides an impl
55/// that wraps the App's buffer registry + mode-activation state.
56///
57/// Methods take `&self` and must be safe to call from any thread
58/// (the impl is responsible for synchronisation).
59pub trait BufferStore: Send + Sync {
60    /// Look up the buffer id whose `name` matches exactly.
61    /// Returns `None` when no buffer with that synthetic name is
62    /// registered yet.
63    fn find_by_name(&self, name: &str) -> Option<BufferId>;
64
65    // NOTE: buffer *creation* (find-or-create + activate a major) is
66    // deliberately NOT on this trait. Activating a mode mutates the mode
67    // registry / active-modes / options cache, which needs `&mut Editor`;
68    // `BufferStore` methods are `&self` (callable from any thread). The
69    // create seam is `ModeActivator::ensure_named_document`
70    // (`&mut`-backed) — the mode owns the creation of its buffers there.
71
72    /// Get a clone of the `Arc<dyn Document>` for `id`,
73    /// suitable for holding across thread boundaries. M.0: the
74    /// return type is the polymorphic shape so the same
75    /// surface serves both regular-document handles and (M.1+)
76    /// multibuffer handles uniformly. The handle is the only
77    /// way for a mode-owned background task to write into the
78    /// buffer (`handle.apply_edit_batch(...)`).
79    ///
80    /// `None` when `id` is not a Document in the registry.
81    fn handle_for(&self, id: BufferId) -> Option<Arc<dyn lattice_runtime::Document>>;
82
83    /// Read the buffer's synthetic name. `None` when the buffer is
84    /// unnamed (the default for path-less scratch documents) or
85    /// when `id` is not registered. B'.7: lets a mode derive its
86    /// own identity from the buffer it's attached to without the
87    /// host having to seed a buffer-local first.
88    fn name_for(&self, id: BufferId) -> Option<String>;
89
90    /// Read the buffer's on-disk file path, if it has one. `None`
91    /// for path-less/synthetic buffers or when `id` is not
92    /// registered. Default `None` so existing implementors (e.g.
93    /// `NullBufferStore`) don't need updating for this addition.
94    fn path_for(&self, _id: BufferId) -> Option<std::path::PathBuf> {
95        None
96    }
97
98    /// Is `id` a buffer this store knows about at all?
99    ///
100    /// **The existence oracle**, and the reason it is a method rather than a
101    /// composition of the three above: every one of them returns `None` for
102    /// *two* different reasons — "no such buffer" and "that buffer has no
103    /// name / no path / no document handle" — so none of them can answer this
104    /// question, and a caller that picks one is really asking something else.
105    ///
106    /// `name_for` is the specific trap. It is the SYNTHETIC-name slot, so it
107    /// answers `None` for every ordinary file-backed buffer; a caller using it
108    /// as an existence check refuses exactly the buffers a user actually edits.
109    /// The plugin `project` seam did that, and the `project` plugin was dead in
110    /// the real editor as a result — every `document-opened` resolved to `none`
111    /// and `:project-switch` reported "no projects remembered yet" forever,
112    /// while its tests passed against a stub that answered `Some` for a
113    /// file-backed buffer.
114    ///
115    /// The default composes what the trait already offers, so no implementor
116    /// breaks; it is best-effort and a store that can answer exactly — the host
117    /// registry can, it has the map — should override. What the default cannot
118    /// see is a registered buffer with no name, no path and no document handle,
119    /// which it reports as absent.
120    fn contains_buffer(&self, id: BufferId) -> bool {
121        self.name_for(id).is_some() || self.path_for(id).is_some() || self.handle_for(id).is_some()
122    }
123
124    /// **H.1 (2026-05-31): generic Document-shaped buffer
125    /// insertion.** Used by extension crates (`lattice-multibuffer`
126    /// today; future plugin-defined Document-shaped kinds) to
127    /// push a `BufferEntry` into the registry without host
128    /// knowing the kind exists.
129    ///
130    /// `kind` must be a Document-shaped kind whose payload is an
131    /// `Arc<dyn Document>`: today `BufferKind::Document` /
132    /// `BufferKind::Messages` / `BufferKind::Multibuffer`. For
133    /// other kinds (`FileTree`, `Oil`, `Terminal`, `Help`) the
134    /// payload is structurally different and they keep their
135    /// host-internal insertion paths until the v2 plugin-
136    /// architecture work designs the generic extension point
137    /// (per `docs/dev/architecture/kind-agnostic-buffers.md`
138    /// §8 Q2).
139    ///
140    /// Idempotent: if `id` is already registered the call is a
141    /// no-op (consistent with the named-singleton create seam,
142    /// [`crate::ModeActivator::ensure_named_document`]). The caller is
143    /// responsible for allocating `id` via `BufferId::next()` first.
144    ///
145    /// Errors fold into the impl's logging path (e.g. the
146    /// `lattice-host` impl logs through `tracing` and emits a
147    /// host-side message); the trait surface is infallible
148    /// because every error here is a programmer error (wrong
149    /// kind, duplicate id) and there's no recovery path the
150    /// caller could enact.
151    fn insert_document_buffer(
152        &self,
153        id: BufferId,
154        kind: BufferKind,
155        handle: Arc<dyn lattice_runtime::Document>,
156        flags: BufferFlags,
157        name: Option<String>,
158    );
159}
160
161/// Concrete service-registry-friendly wrapper around
162/// `Arc<dyn BufferStore>`. Modes register interest by pulling
163/// `Arc<BufferStoreHandle>` from
164/// [`crate::ServiceRegistry::get::<BufferStoreHandle>()`].
165///
166/// Cheap to clone — internal `Arc<dyn BufferStore>` is one atomic
167/// bump.
168#[derive(Clone)]
169pub struct BufferStoreHandle {
170    inner: Arc<dyn BufferStore>,
171}
172
173impl BufferStoreHandle {
174    /// Wrap the host's store. Called once by the host at boot (the
175    /// concrete store lives in the host); tests wrap a stub.
176    pub fn new(inner: Arc<dyn BufferStore>) -> Self {
177        Self { inner }
178    }
179
180    /// Forwards to [`BufferStore::find_by_name`].
181    pub fn find_by_name(&self, name: &str) -> Option<BufferId> {
182        self.inner.find_by_name(name)
183    }
184
185    // Buffer *creation* is not on this `&self` handle — it must activate
186    // a mode, which needs `&mut Editor`. Use
187    // `ModeActivator::ensure_named_document` (the mode-owned creation
188    // seam). `BufferStore` is read/find + generic document insertion only.
189
190    /// Forwards to [`BufferStore::handle_for`].
191    pub fn handle_for(&self, id: BufferId) -> Option<Arc<dyn lattice_runtime::Document>> {
192        self.inner.handle_for(id)
193    }
194
195    /// Forwards to [`BufferStore::name_for`] (the synthetic-name slot —
196    /// `None` for ordinary file-backed buffers).
197    pub fn name_for(&self, id: BufferId) -> Option<String> {
198        self.inner.name_for(id)
199    }
200
201    /// Forwards to [`BufferStore::path_for`].
202    pub fn path_for(&self, id: BufferId) -> Option<std::path::PathBuf> {
203        self.inner.path_for(id)
204    }
205
206    /// The existence oracle. See [`BufferStore::contains_buffer`] for why this
207    /// is not `name_for(id).is_some()`.
208    pub fn contains_buffer(&self, id: BufferId) -> bool {
209        self.inner.contains_buffer(id)
210    }
211
212    /// H.1 pass-through wrapper. See
213    /// [`BufferStore::insert_document_buffer`].
214    pub fn insert_document_buffer(
215        &self,
216        id: BufferId,
217        kind: BufferKind,
218        handle: Arc<dyn lattice_runtime::Document>,
219        flags: BufferFlags,
220        name: Option<String>,
221    ) {
222        self.inner
223            .insert_document_buffer(id, kind, handle, flags, name)
224    }
225}
226
227impl std::fmt::Debug for BufferStoreHandle {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        f.debug_struct("BufferStoreHandle").finish_non_exhaustive()
230    }
231}