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}