facet_diff_core/layout/
arena.rs

1//! Format arena for string storage.
2
3use std::fmt;
4use unicode_width::UnicodeWidthStr;
5
6/// A span into the format arena's buffer.
7#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
8pub struct Span {
9    /// Start byte offset
10    pub start: u32,
11    /// End byte offset (exclusive)
12    pub end: u32,
13}
14
15impl Span {
16    /// Byte length of the span
17    #[inline]
18    pub fn len(self) -> usize {
19        (self.end - self.start) as usize
20    }
21
22    /// Check if span is empty
23    #[inline]
24    pub fn is_empty(self) -> bool {
25        self.start == self.end
26    }
27}
28
29/// Arena for formatted strings.
30///
31/// All scalar values are formatted once into this buffer and referenced by [`Span`].
32/// This avoids per-value allocations and allows measuring display width at format time.
33pub struct FormatArena {
34    buf: String,
35}
36
37impl FormatArena {
38    /// Create a new arena with pre-allocated capacity.
39    pub fn with_capacity(cap: usize) -> Self {
40        Self {
41            buf: String::with_capacity(cap),
42        }
43    }
44
45    /// Create a new arena with default capacity.
46    pub fn new() -> Self {
47        // 4KB default - enough for most diffs
48        Self::with_capacity(4096)
49    }
50
51    /// Format something into the arena, returning span and display width.
52    ///
53    /// The closure receives a `&mut String` to write into. The span returned
54    /// covers exactly what was written. Display width is calculated using
55    /// `unicode-width` for proper handling of CJK characters, emoji, etc.
56    pub fn format<F>(&mut self, f: F) -> (Span, usize)
57    where
58        F: FnOnce(&mut String) -> fmt::Result,
59    {
60        let start = self.buf.len();
61        f(&mut self.buf).expect("formatting to String cannot fail");
62        let end = self.buf.len();
63        let span = Span {
64            start: start as u32,
65            end: end as u32,
66        };
67        let width = self.buf[start..end].width();
68        (span, width)
69    }
70
71    /// Push a string directly, returning span and display width.
72    pub fn push_str(&mut self, s: &str) -> (Span, usize) {
73        let start = self.buf.len();
74        self.buf.push_str(s);
75        let span = Span {
76            start: start as u32,
77            end: self.buf.len() as u32,
78        };
79        let width = s.width();
80        (span, width)
81    }
82
83    /// Retrieve the string for a span.
84    #[inline]
85    pub fn get(&self, span: Span) -> &str {
86        &self.buf[span.start as usize..span.end as usize]
87    }
88
89    /// Current size of the buffer in bytes.
90    pub fn len(&self) -> usize {
91        self.buf.len()
92    }
93
94    /// Check if the arena is empty.
95    pub fn is_empty(&self) -> bool {
96        self.buf.is_empty()
97    }
98}
99
100impl Default for FormatArena {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::fmt::Write;
109
110    use super::*;
111
112    #[test]
113    fn test_format_simple() {
114        let mut arena = FormatArena::new();
115        let (span, width) = arena.format(|w| write!(w, "hello"));
116        assert_eq!(arena.get(span), "hello");
117        assert_eq!(width, 5);
118    }
119
120    #[test]
121    fn test_format_multiple() {
122        let mut arena = FormatArena::new();
123
124        let (s1, w1) = arena.format(|w| write!(w, "hello"));
125        let (s2, w2) = arena.format(|w| write!(w, "world"));
126
127        assert_eq!(arena.get(s1), "hello");
128        assert_eq!(arena.get(s2), "world");
129        assert_eq!(w1, 5);
130        assert_eq!(w2, 5);
131
132        // Spans don't overlap
133        assert_eq!(s1.end, s2.start);
134    }
135
136    #[test]
137    fn test_format_unicode() {
138        let mut arena = FormatArena::new();
139
140        // CJK characters are typically 2 columns wide
141        let (span, width) = arena.format(|w| write!(w, "日本語"));
142        assert_eq!(arena.get(span), "日本語");
143        assert_eq!(width, 6); // 3 chars * 2 columns each
144
145        // Emoji
146        let (span, width) = arena.format(|w| write!(w, "🦀"));
147        assert_eq!(arena.get(span), "🦀");
148        assert_eq!(width, 2); // emoji is typically 2 columns
149    }
150
151    #[test]
152    fn test_push_str() {
153        let mut arena = FormatArena::new();
154        let (span, width) = arena.push_str("test");
155        assert_eq!(arena.get(span), "test");
156        assert_eq!(width, 4);
157    }
158
159    #[test]
160    fn test_span_len() {
161        let span = Span { start: 10, end: 20 };
162        assert_eq!(span.len(), 10);
163        assert!(!span.is_empty());
164
165        let empty = Span { start: 5, end: 5 };
166        assert_eq!(empty.len(), 0);
167        assert!(empty.is_empty());
168    }
169}