272 lines
9.4 KiB
Rust
272 lines
9.4 KiB
Rust
// buffer_registry.rs --- Buffer registry: opaque IDs resolved through Rust.
|
|
|
|
//! Owns [`Buffer`]s behind opaque [`BufferId`] handles.
|
|
//!
|
|
//! # Why a registry
|
|
//!
|
|
//! Spec §3 Checkpoint 6 and the Lua boundary rules (R52, R53) keep raw
|
|
//! `Arc<Mutex<Buffer>>` away from Lua. Lua holds `BufferId` values
|
|
//! (cheap, `Copy`, no GC entanglement); every method call resolves the
|
|
//! ID through this registry. A removed buffer leaves all live IDs as
|
|
//! *stale* handles, and the next resolution returns
|
|
//! [`RegistryError::Missing`] --- a typed error, not a use-after-free.
|
|
//!
|
|
//! # Threading
|
|
//!
|
|
//! Single-threaded by construction. The registry lives next to the
|
|
//! [`crate::lua::LuaHost`] on the main thread; workers (M3) communicate
|
|
//! via typed messages and snapshots, never by sharing the registry.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use thiserror::Error;
|
|
|
|
use crate::buffer::{Buffer, BufferId};
|
|
|
|
/// Errors raised by the registry.
|
|
#[derive(Debug, Error)]
|
|
pub enum RegistryError {
|
|
/// The supplied [`BufferId`] is not currently registered. The handle
|
|
/// is stale: it referred to a buffer that has since been removed (or
|
|
/// was never registered with this instance).
|
|
#[error("no buffer registered with id {id:?}")]
|
|
Missing {
|
|
/// The offending ID, preserved for structured surfacing across
|
|
/// the Lua boundary (R52).
|
|
id: BufferId,
|
|
},
|
|
/// Removal was requested for a buffer that is currently mid-edit
|
|
/// (T M7.4). The most common path: a Lua intercept body on buffer
|
|
/// `A` called `pmacs.buffer.remove(A)`. Mirrors
|
|
/// [`crate::buffer::BufferError::ConcurrentEdit`] in spirit; the
|
|
/// message names the workaround per project convention.
|
|
#[error(
|
|
"buffer `{name}` (id {id:?}) is already being edited; \
|
|
it cannot be removed while an intercept is running on it. \
|
|
To remove this buffer, return from the intercept first \
|
|
(the registry borrow will release at that point), or remove \
|
|
a different buffer."
|
|
)]
|
|
ConcurrentEdit {
|
|
/// The buffer's identifier.
|
|
id: BufferId,
|
|
/// The buffer's name, for diagnostics.
|
|
name: String,
|
|
},
|
|
}
|
|
|
|
/// Owns [`Buffer`]s behind their [`BufferId`]s.
|
|
///
|
|
/// Construction goes through [`Self::new`] or [`Self::default`]. Buffers
|
|
/// are inserted by [`Self::create`], [`Self::create_from_bytes`], or
|
|
/// [`Self::insert`] (which takes a pre-built [`Buffer`] --- useful when a
|
|
/// buffer is loaded from disk before being handed off to the registry).
|
|
#[derive(Default)]
|
|
pub struct BufferRegistry {
|
|
buffers: HashMap<BufferId, Buffer>,
|
|
/// Insertion-order list of IDs. Used for stable iteration / listing
|
|
/// (`HashMap` iteration order is unspecified, which would make
|
|
/// `pmacs.buffer.list()` non-deterministic).
|
|
order: Vec<BufferId>,
|
|
}
|
|
|
|
impl BufferRegistry {
|
|
/// Construct an empty registry.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Insert an already-constructed [`Buffer`]. Returns its [`BufferId`].
|
|
///
|
|
/// If a buffer with the same ID is already present (which would
|
|
/// indicate a programming error --- IDs are allocated by the global
|
|
/// counter and unique), the old buffer is overwritten and removed
|
|
/// from the order list before the new one is appended.
|
|
pub fn insert(&mut self, buffer: Buffer) -> BufferId {
|
|
let id = buffer.id();
|
|
if self.buffers.insert(id, buffer).is_some() {
|
|
// Defensive: keep `order` consistent if someone re-inserted.
|
|
self.order.retain(|x| *x != id);
|
|
}
|
|
self.order.push(id);
|
|
id
|
|
}
|
|
|
|
/// Create a fresh empty [`Buffer`] under a new [`BufferId`].
|
|
pub fn create(&mut self, name: impl Into<String>) -> BufferId {
|
|
self.insert(Buffer::new(BufferId::next(), name))
|
|
}
|
|
|
|
/// Create a [`Buffer`] seeded with `bytes` under a new [`BufferId`].
|
|
pub fn create_from_bytes(&mut self, name: impl Into<String>, bytes: &[u8]) -> BufferId {
|
|
self.insert(Buffer::from_bytes(BufferId::next(), name, bytes))
|
|
}
|
|
|
|
/// Resolve an ID to a shared reference. Returns
|
|
/// [`RegistryError::Missing`] for stale handles (R53).
|
|
pub fn get(&self, id: BufferId) -> Result<&Buffer, RegistryError> {
|
|
self.buffers.get(&id).ok_or(RegistryError::Missing { id })
|
|
}
|
|
|
|
/// Resolve an ID to an exclusive reference.
|
|
pub fn get_mut(&mut self, id: BufferId) -> Result<&mut Buffer, RegistryError> {
|
|
self.buffers
|
|
.get_mut(&id)
|
|
.ok_or(RegistryError::Missing { id })
|
|
}
|
|
|
|
/// Remove and return the buffer behind `id`. Subsequent lookups of
|
|
/// `id` produce [`RegistryError::Missing`].
|
|
///
|
|
/// Refuses to remove a buffer that is currently mid-edit (T M7.4):
|
|
/// an intercept body running on buffer `A` cannot drop `A` out
|
|
/// from under itself. Returns
|
|
/// [`RegistryError::ConcurrentEdit`] in that case, leaving the
|
|
/// buffer in place.
|
|
pub fn remove(&mut self, id: BufferId) -> Result<Buffer, RegistryError> {
|
|
// Peek without taking ownership: if the buffer is mid-edit we
|
|
// surface a typed error and leave the registry untouched.
|
|
if let Some(buf) = self.buffers.get(&id)
|
|
&& buf.editing_in_progress()
|
|
{
|
|
return Err(RegistryError::ConcurrentEdit {
|
|
id,
|
|
name: buf.name().to_string(),
|
|
});
|
|
}
|
|
let buf = self
|
|
.buffers
|
|
.remove(&id)
|
|
.ok_or(RegistryError::Missing { id })?;
|
|
self.order.retain(|x| *x != id);
|
|
Ok(buf)
|
|
}
|
|
|
|
/// True iff `id` currently resolves.
|
|
#[must_use]
|
|
pub fn contains(&self, id: BufferId) -> bool {
|
|
self.buffers.contains_key(&id)
|
|
}
|
|
|
|
/// First buffer whose name matches `name` exactly, in insertion
|
|
/// order. Used by host code that addresses well-known buffers
|
|
/// (e.g. `*errors*`) without holding their `BufferId`.
|
|
#[must_use]
|
|
pub fn find_by_name(&self, name: &str) -> Option<BufferId> {
|
|
self.order
|
|
.iter()
|
|
.copied()
|
|
.find(|id| self.buffers.get(id).is_some_and(|b| b.name() == name))
|
|
}
|
|
|
|
/// First buffer bound to `path` on disk, in insertion order.
|
|
/// T M4.5 L1: matches on the per-buffer `file_path` (set at
|
|
/// load), so cross-file navigation reuses an already-open file
|
|
/// instead of creating a duplicate buffer.
|
|
#[must_use]
|
|
pub fn find_by_path(&self, path: &std::path::Path) -> Option<BufferId> {
|
|
self.order.iter().copied().find(|id| {
|
|
self.buffers
|
|
.get(id)
|
|
.is_some_and(|b| b.file_path() == Some(path))
|
|
})
|
|
}
|
|
|
|
/// IDs in insertion order. Stable: preserved across non-removing
|
|
/// operations.
|
|
#[must_use]
|
|
pub fn ids(&self) -> &[BufferId] {
|
|
&self.order
|
|
}
|
|
|
|
/// Number of registered buffers.
|
|
#[must_use]
|
|
pub fn len(&self) -> usize {
|
|
self.buffers.len()
|
|
}
|
|
|
|
/// True iff no buffers are registered.
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.buffers.is_empty()
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn create_then_get_round_trips() {
|
|
let mut r = BufferRegistry::new();
|
|
let id = r.create("alpha");
|
|
assert_eq!(r.len(), 1);
|
|
let b = r.get(id).unwrap();
|
|
assert_eq!(b.name(), "alpha");
|
|
assert_eq!(b.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn create_from_bytes_seeds_content() {
|
|
let mut r = BufferRegistry::new();
|
|
let id = r.create_from_bytes("seed", b"hello");
|
|
assert_eq!(r.get(id).unwrap().len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_id_returns_typed_error() {
|
|
// Avoids requiring Debug on Buffer (Buffer has trait-object views
|
|
// that don't carry Debug), so we destructure the error directly.
|
|
let r = BufferRegistry::new();
|
|
let stale = BufferId::next();
|
|
let err = r.get(stale).err().expect("expected stale-handle error");
|
|
match err {
|
|
RegistryError::Missing { id } => assert_eq!(id, stale),
|
|
other @ RegistryError::ConcurrentEdit { .. } => {
|
|
panic!("expected Missing, got {other:?}")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn remove_invalidates_handle() {
|
|
let mut r = BufferRegistry::new();
|
|
let id = r.create("doomed");
|
|
r.remove(id).unwrap();
|
|
assert!(!r.contains(id));
|
|
assert!(r.get(id).is_err());
|
|
assert!(r.remove(id).is_err());
|
|
assert_eq!(r.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn find_by_name_returns_first_match() {
|
|
let mut r = BufferRegistry::new();
|
|
let a = r.create("alpha");
|
|
let _b = r.create("beta");
|
|
let alpha2 = r.create("alpha");
|
|
// Insertion order: returns the first.
|
|
assert_eq!(r.find_by_name("alpha"), Some(a));
|
|
// Once removed, the second alpha takes the slot.
|
|
r.remove(a).unwrap();
|
|
assert_eq!(r.find_by_name("alpha"), Some(alpha2));
|
|
assert_eq!(r.find_by_name("nope"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn ids_preserved_in_insertion_order() {
|
|
let mut r = BufferRegistry::new();
|
|
let a = r.create("a");
|
|
let b = r.create("b");
|
|
let c = r.create("c");
|
|
assert_eq!(r.ids(), &[a, b, c]);
|
|
r.remove(b).unwrap();
|
|
assert_eq!(r.ids(), &[a, c]);
|
|
}
|
|
}
|