Skip to main content

strop_core/
id.rs

1//! Stable identities and typed coordinates (0014 wave 2).
2//!
3//! Two families:
4//! - **IDs**: generational arena keys (`DocumentId`, `ViewId`, …). A
5//!   closed document's id fails lookup instead of silently resolving to
6//!   whatever moved into its old vector slot.
7//! - **Coordinates**: newtypes so byte offsets, line indexes, and the
8//!   three column kinds (bytes / UTF-16 / display) can't mix silently.
9
10mod seed;
11pub use seed::{ArenaSeed, ArenaSeedError};
12
13/// A generational-arena key: the index names the slot, the generation
14/// names the occupant. Stale keys fail lookup.
15#[derive(
16    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
17)]
18#[serde(bound = "")]
19pub struct Id<K> {
20    #[serde(rename = "slot", alias = "index")]
21    index: u32,
22    generation: u32,
23    #[serde(skip)]
24    _kind: std::marker::PhantomData<K>,
25}
26
27impl<K> Id<K> {
28    pub fn index(self) -> usize {
29        self.index as usize
30    }
31
32    pub fn generation(self) -> u32 {
33        self.generation
34    }
35}
36
37/// Marker kinds for the arena's identities.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct DocumentKind;
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct ViewKind;
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub struct PaneKind;
44/// Registry keys for bound workspace contexts (0042 slice 2).
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub struct WorkspaceKind;
47
48pub type DocumentId = Id<DocumentKind>;
49pub type ViewId = Id<ViewKind>;
50pub type PaneId = Id<PaneKind>;
51pub type WorkspaceId = Id<WorkspaceKind>;
52
53/// A minimal generational arena (house rule: 40 boring lines beat a
54/// dependency). Slots are reused; each reuse bumps the generation.
55pub struct Arena<K, T> {
56    slots: Vec<Slot<T>>,
57    free: Vec<u32>,
58    _kind: std::marker::PhantomData<K>,
59}
60
61#[derive(Debug)]
62struct Slot<T> {
63    generation: u32,
64    value: Option<T>,
65}
66
67impl<K, T> Default for Arena<K, T> {
68    fn default() -> Self {
69        Self {
70            slots: Vec::new(),
71            free: Vec::new(),
72            _kind: std::marker::PhantomData,
73        }
74    }
75}
76
77impl<K, T> Arena<K, T> {
78    pub fn insert(&mut self, value: T) -> Id<K> {
79        if let Some(index) = self.free.pop() {
80            let slot = &mut self.slots[index as usize];
81            slot.generation += 1;
82            slot.value = Some(value);
83            return Id {
84                index,
85                generation: slot.generation,
86                _kind: std::marker::PhantomData,
87            };
88        }
89        let index = self.slots.len() as u32;
90        self.slots.push(Slot {
91            generation: 0,
92            value: Some(value),
93        });
94        Id {
95            index,
96            generation: 0,
97            _kind: std::marker::PhantomData,
98        }
99    }
100
101    /// None for a stale id — never the wrong document.
102    pub fn get(&self, id: Id<K>) -> Option<&T> {
103        self.slots
104            .get(id.index as usize)
105            .filter(|s| s.generation == id.generation)
106            .and_then(|s| s.value.as_ref())
107    }
108
109    pub fn get_mut(&mut self, id: Id<K>) -> Option<&mut T> {
110        self.slots
111            .get_mut(id.index as usize)
112            .filter(|s| s.generation == id.generation)
113            .and_then(|s| s.value.as_mut())
114    }
115
116    /// Remove and return the value; stale ids get None.
117    pub fn remove(&mut self, id: Id<K>) -> Option<T> {
118        let slot = self.slots.get_mut(id.index as usize)?;
119        if slot.generation != id.generation {
120            return None;
121        }
122        let value = slot.value.take()?;
123        self.free.push(id.index);
124        Some(value)
125    }
126
127    pub fn iter(&self) -> impl Iterator<Item = (Id<K>, &T)> {
128        self.slots.iter().enumerate().filter_map(|(i, s)| {
129            s.value.as_ref().map(|v| {
130                (
131                    Id {
132                        index: i as u32,
133                        generation: s.generation,
134                        _kind: std::marker::PhantomData,
135                    },
136                    v,
137                )
138            })
139        })
140    }
141
142    /// Mutable values without copying keys or exposing vacant slots.
143    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
144        self.slots.iter_mut().filter_map(|slot| slot.value.as_mut())
145    }
146
147    pub fn len(&self) -> usize {
148        self.slots.iter().filter(|s| s.value.is_some()).count()
149    }
150
151    /// Drop every live value (slot reuse still bumps generations).
152    pub fn clear(&mut self) {
153        let live: Vec<u32> = self
154            .slots
155            .iter()
156            .enumerate()
157            .filter(|(_, s)| s.value.is_some())
158            .map(|(i, _)| i as u32)
159            .collect();
160        for s in &mut self.slots {
161            s.value = None;
162        }
163        self.free.extend(live);
164    }
165    pub fn is_empty(&self) -> bool {
166        self.len() == 0
167    }
168}
169
170/// A coordinate newtype: the inner value is bytes (ByteOffset), lines
171/// (LineIndex), or columns in a named unit (ByteColumn/Utf16Column/
172/// DisplayColumn). Copy, ordered, hashable; arithmetic is explicit.
173macro_rules! coordinate {
174    ($name:ident, $unit:literal) => {
175        #[derive(
176            Debug,
177            Clone,
178            Copy,
179            PartialEq,
180            Eq,
181            PartialOrd,
182            Ord,
183            Hash,
184            Default,
185            serde::Serialize,
186            serde::Deserialize,
187        )]
188        #[repr(transparent)]
189        #[serde(transparent)]
190        pub struct $name(usize);
191
192        impl $name {
193            #[inline]
194            pub fn new(v: usize) -> Self {
195                Self(v)
196            }
197            /// Raw units (bytes / lines / columns per the type) — escape
198            /// hatch for arithmetic; naming the unit is the point.
199            #[inline]
200            pub fn get(self) -> usize {
201                self.0
202            }
203            #[inline]
204            pub fn saturating_sub(self, n: usize) -> Self {
205                Self(self.0.saturating_sub(n))
206            }
207        }
208
209        impl std::ops::AddAssign<usize> for $name {
210            #[inline]
211            fn add_assign(&mut self, n: usize) {
212                self.0 += n;
213            }
214        }
215        impl std::ops::SubAssign<usize> for $name {
216            #[inline]
217            fn sub_assign(&mut self, n: usize) {
218                self.0 -= n;
219            }
220        }
221        /// Raw-unit comparison: `offset > 0` reads naturally; the type
222        /// system still stops offset-vs-line mixes (the bug class).
223        impl PartialEq<usize> for $name {
224            #[inline]
225            fn eq(&self, other: &usize) -> bool {
226                self.0 == *other
227            }
228        }
229        impl PartialOrd<usize> for $name {
230            #[inline]
231            fn partial_cmp(&self, other: &usize) -> Option<std::cmp::Ordering> {
232                self.0.partial_cmp(other)
233            }
234        }
235        impl std::ops::Add<usize> for $name {
236            type Output = $name;
237            #[inline]
238            fn add(self, n: usize) -> $name {
239                $name(self.0 + n)
240            }
241        }
242        impl std::ops::Sub<usize> for $name {
243            type Output = $name;
244            #[inline]
245            fn sub(self, n: usize) -> $name {
246                $name(self.0 - n)
247            }
248        }
249        impl std::ops::Sub<$name> for $name {
250            type Output = usize; // a length
251            #[inline]
252            fn sub(self, other: $name) -> usize {
253                self.0 - other.0
254            }
255        }
256        impl std::fmt::Display for $name {
257            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258                write!(f, "{} {}", self.0, $unit)
259            }
260        }
261        impl From<$name> for usize {
262            #[inline]
263            fn from(v: $name) -> usize {
264                v.0
265            }
266        }
267        impl From<usize> for $name {
268            #[inline]
269            fn from(v: usize) -> $name {
270                $name(v)
271            }
272        }
273    };
274}
275
276coordinate!(ByteOffset, "B");
277coordinate!(LineIndex, "L");
278coordinate!(ByteColumn, "col:B");
279coordinate!(Utf16Column, "col:u16");
280coordinate!(DisplayColumn, "col:dsp");
281
282/// A document's content clock, not an LSP version, request ID or history node.
283#[derive(
284    Debug,
285    Clone,
286    Copy,
287    PartialEq,
288    Eq,
289    PartialOrd,
290    Ord,
291    Hash,
292    Default,
293    serde::Serialize,
294    serde::Deserialize,
295)]
296#[serde(transparent)]
297pub struct BufferRevision(u64);
298
299impl BufferRevision {
300    pub const fn new(value: u64) -> Self {
301        Self(value)
302    }
303    pub const fn get(self) -> u64 {
304        self.0
305    }
306    pub fn checked_next(self) -> Option<Self> {
307        self.0.checked_add(1).map(Self)
308    }
309}
310
311impl std::fmt::Display for BufferRevision {
312    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        self.0.fmt(formatter)
314    }
315}
316
317impl From<u64> for BufferRevision {
318    fn from(value: u64) -> Self {
319        Self(value)
320    }
321}
322
323// NOTE: full newtype wrappers (ByteOffset(usize), Utf16Column(u32), …)
324// are the target; the pragmatic cutover is to name the domains first
325// (this module + the conversion functions) and tighten the Buffer API
326// per call-site cluster as waves 2–4 touch them. A big-bang usize→newtype
327// rewrite of every arithmetic site would be unreviewable.
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn stale_ids_fail_lookup() {
335        let mut a: Arena<DocumentKind, String> = Arena::default();
336        let one = a.insert("one".into());
337        let two = a.insert("two".into());
338        assert_eq!(a.get(one).map(String::as_str), Some("one"));
339        a.remove(one);
340        assert_eq!(a.get(one), None, "removed");
341        let three = a.insert("three".into()); // reuses the slot
342        assert_eq!(a.get(one), None, "stale generation must not resolve");
343        assert_eq!(a.get(three).map(String::as_str), Some("three"));
344        assert_eq!(a.get(two).map(String::as_str), Some("two"));
345        assert_eq!(a.len(), 2);
346    }
347}