Skip to main content

flywheel/buffer/
rope.rs

1//! Rope Buffer: Chunked storage for efficient large document handling.
2//!
3//! This module provides a rope-based data structure optimized for:
4//! - Large documents (1M+ lines) with minimal allocations
5//! - O(1) append and O(log n) random access
6//! - Good cache locality through chunking
7
8use crate::buffer::Cell;
9
10/// Number of lines per chunk.
11/// Tuned for a balance between overhead and cache utilization.
12const CHUNK_SIZE: usize = 64;
13
14/// A chunk of lines stored contiguously.
15#[derive(Debug, Clone)]
16struct Chunk {
17    /// Lines in this chunk.
18    lines: Vec<ChunkedLine>,
19}
20
21impl Chunk {
22    /// Create a new empty chunk.
23    fn new() -> Self {
24        Self {
25            lines: Vec::with_capacity(CHUNK_SIZE),
26        }
27    }
28
29    /// Check if the chunk is full.
30    const fn is_full(&self) -> bool {
31        self.lines.len() >= CHUNK_SIZE
32    }
33
34    /// Get the number of lines in this chunk.
35    const fn len(&self) -> usize {
36        self.lines.len()
37    }
38
39    /// Check if the chunk is empty.
40    #[allow(dead_code)]
41    const fn is_empty(&self) -> bool {
42        self.lines.is_empty()
43    }
44
45    /// Push a line to this chunk.
46    fn push(&mut self, line: ChunkedLine) {
47        self.lines.push(line);
48    }
49
50    /// Get a line by index within this chunk.
51    fn get(&self, index: usize) -> Option<&ChunkedLine> {
52        self.lines.get(index)
53    }
54
55    /// Get a mutable line by index within this chunk.
56    fn get_mut(&mut self, index: usize) -> Option<&mut ChunkedLine> {
57        self.lines.get_mut(index)
58    }
59}
60
61/// A line stored in the rope buffer.
62#[derive(Debug, Clone)]
63pub struct ChunkedLine {
64    /// The cells in this line.
65    pub content: Vec<Cell>,
66    /// Whether this line was soft-wrapped.
67    pub wrapped: bool,
68}
69
70impl ChunkedLine {
71    /// Create a new line with the given content.
72    pub const fn new(content: Vec<Cell>, wrapped: bool) -> Self {
73        Self { content, wrapped }
74    }
75
76    /// Create an empty line.
77    pub const fn empty() -> Self {
78        Self {
79            content: Vec::new(),
80            wrapped: false,
81        }
82    }
83
84    /// Get the number of cells in this line.
85    pub const fn len(&self) -> usize {
86        self.content.len()
87    }
88
89    /// Check if the line is empty.
90    pub const fn is_empty(&self) -> bool {
91        self.content.is_empty()
92    }
93}
94
95/// A rope-based line buffer for efficient large document storage.
96///
97/// Instead of storing each line as a separate allocation, lines are
98/// grouped into chunks of `CHUNK_SIZE` lines. This reduces:
99/// - Memory fragmentation
100/// - Allocation overhead
101/// - Cache misses during iteration
102#[derive(Debug)]
103pub struct RopeBuffer {
104    /// Chunks of lines.
105    chunks: Vec<Chunk>,
106    /// Total number of lines.
107    total_lines: usize,
108    /// Maximum number of lines to retain (0 = unlimited).
109    max_lines: usize,
110    /// Current scroll offset from bottom.
111    scroll_offset: usize,
112}
113
114impl RopeBuffer {
115    /// Create a new rope buffer with the given maximum capacity.
116    ///
117    /// # Arguments
118    ///
119    /// * `max_lines` - Maximum lines to retain. 0 means unlimited.
120    pub fn new(max_lines: usize) -> Self {
121        let mut buffer = Self {
122            chunks: Vec::new(),
123            total_lines: 0,
124            max_lines,
125            scroll_offset: 0,
126        };
127        // Start with one empty line
128        buffer.push_line(ChunkedLine::empty());
129        buffer
130    }
131
132    /// Create an unbounded rope buffer.
133    pub fn unbounded() -> Self {
134        Self::new(0)
135    }
136
137    /// Get the total number of lines.
138    pub const fn len(&self) -> usize {
139        self.total_lines
140    }
141
142    /// Check if the buffer is empty.
143    pub const fn is_empty(&self) -> bool {
144        self.total_lines == 0
145    }
146
147    /// Get the number of chunks.
148    pub const fn chunk_count(&self) -> usize {
149        self.chunks.len()
150    }
151
152    /// Get a line by global index.
153    pub fn get_line(&self, index: usize) -> Option<&ChunkedLine> {
154        if index >= self.total_lines {
155            return None;
156        }
157        let chunk_idx = index / CHUNK_SIZE;
158        let line_idx = index % CHUNK_SIZE;
159        self.chunks.get(chunk_idx)?.get(line_idx)
160    }
161
162    /// Get a mutable reference to a line by global index.
163    pub fn get_line_mut(&mut self, index: usize) -> Option<&mut ChunkedLine> {
164        if index >= self.total_lines {
165            return None;
166        }
167        let chunk_idx = index / CHUNK_SIZE;
168        let line_idx = index % CHUNK_SIZE;
169        self.chunks.get_mut(chunk_idx)?.get_mut(line_idx)
170    }
171
172    /// Get the current (last) line.
173    pub fn current_line(&self) -> Option<&ChunkedLine> {
174        if self.total_lines == 0 {
175            return None;
176        }
177        self.get_line(self.total_lines - 1)
178    }
179
180    /// Get a mutable reference to the current (last) line.
181    pub fn current_line_mut(&mut self) -> Option<&mut ChunkedLine> {
182        if self.total_lines == 0 {
183            return None;
184        }
185        let idx = self.total_lines - 1;
186        self.get_line_mut(idx)
187    }
188
189    /// Push a new line to the buffer.
190    pub fn push_line(&mut self, line: ChunkedLine) {
191        // Check if we need a new chunk
192        if self.chunks.is_empty() || self.chunks.last().is_none_or(Chunk::is_full) {
193            self.chunks.push(Chunk::new());
194        }
195
196        // Push to the last chunk
197        if let Some(chunk) = self.chunks.last_mut() {
198            chunk.push(line);
199            self.total_lines += 1;
200        }
201
202        // Enforce max_lines if set
203        if self.max_lines > 0 && self.total_lines > self.max_lines {
204            self.trim_front();
205        }
206    }
207
208    /// Add a new empty line.
209    pub fn newline(&mut self) {
210        self.push_line(ChunkedLine::empty());
211    }
212
213    /// Append cells to the current line.
214    pub fn append(&mut self, cells: impl Iterator<Item = Cell>) {
215        if let Some(line) = self.current_line_mut() {
216            line.content.extend(cells);
217        }
218    }
219
220    /// Clear all content.
221    pub fn clear(&mut self) {
222        self.chunks.clear();
223        self.total_lines = 0;
224        self.scroll_offset = 0;
225        self.push_line(ChunkedLine::empty());
226    }
227
228    /// Get the current scroll offset.
229    pub const fn scroll_offset(&self) -> usize {
230        self.scroll_offset
231    }
232
233    /// Scroll up by the given number of lines.
234    pub fn scroll_up(&mut self, lines: usize) {
235        let max_offset = self.total_lines.saturating_sub(1);
236        self.scroll_offset = (self.scroll_offset + lines).min(max_offset);
237    }
238
239    /// Scroll down by the given number of lines.
240    pub const fn scroll_down(&mut self, lines: usize) {
241        self.scroll_offset = self.scroll_offset.saturating_sub(lines);
242    }
243
244    /// Scroll to the bottom (most recent content).
245    pub const fn scroll_to_bottom(&mut self) {
246        self.scroll_offset = 0;
247    }
248
249    /// Iterate over lines in the visible range.
250    ///
251    /// Returns an iterator over (`line_index`, &`ChunkedLine`) for lines
252    /// that should be visible given the viewport height.
253    pub fn visible_lines(&self, viewport_height: usize) -> impl Iterator<Item = (usize, &ChunkedLine)> {
254        let end = self.total_lines.saturating_sub(self.scroll_offset);
255        let start = end.saturating_sub(viewport_height);
256        
257        (start..end).filter_map(move |i| {
258            self.get_line(i).map(|line| (i, line))
259        })
260    }
261
262    /// Trim lines from the front to stay within `max_lines`.
263    fn trim_front(&mut self) {
264        while self.total_lines > self.max_lines && !self.chunks.is_empty() {
265            // Remove the first chunk
266            let removed_chunk = self.chunks.remove(0);
267            self.total_lines -= removed_chunk.len();
268            
269            // Adjust scroll offset
270            if self.scroll_offset > removed_chunk.len() {
271                self.scroll_offset -= removed_chunk.len();
272            } else {
273                self.scroll_offset = 0;
274            }
275        }
276    }
277
278    /// Get memory usage statistics.
279    pub fn memory_stats(&self) -> RopeMemoryStats {
280        let mut total_cells = 0;
281        for chunk in &self.chunks {
282            for line in &chunk.lines {
283                total_cells += line.content.len();
284            }
285        }
286
287        RopeMemoryStats {
288            chunks: self.chunks.len(),
289            lines: self.total_lines,
290            cells: total_cells,
291            bytes_estimated: self.chunks.len() * std::mem::size_of::<Chunk>()
292                + self.total_lines * std::mem::size_of::<ChunkedLine>()
293                + total_cells * std::mem::size_of::<Cell>(),
294        }
295    }
296}
297
298/// Memory usage statistics for a rope buffer.
299#[derive(Debug, Clone, Copy)]
300pub struct RopeMemoryStats {
301    /// Number of chunks.
302    pub chunks: usize,
303    /// Number of lines.
304    pub lines: usize,
305    /// Number of cells.
306    pub cells: usize,
307    /// Estimated memory usage in bytes.
308    pub bytes_estimated: usize,
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_rope_buffer_basic() {
317        let mut buffer = RopeBuffer::new(1000);
318        
319        assert_eq!(buffer.len(), 1); // Starts with empty line
320        
321        buffer.append([Cell::new('H'), Cell::new('i')].into_iter());
322        assert_eq!(buffer.current_line().unwrap().len(), 2);
323        
324        buffer.newline();
325        assert_eq!(buffer.len(), 2);
326    }
327
328    #[test]
329    fn test_rope_buffer_chunks() {
330        let mut buffer = RopeBuffer::unbounded();
331        
332        // Add enough lines to create multiple chunks
333        for i in 0..200 {
334            buffer.newline();
335            buffer.append([Cell::new(char::from_u32(('a' as u32) + (i % 26)).unwrap())].into_iter());
336        }
337        
338        // Should have multiple chunks
339        assert!(buffer.chunk_count() > 1);
340        assert_eq!(buffer.len(), 201); // 1 initial + 200 new
341    }
342
343    #[test]
344    fn test_rope_buffer_max_lines() {
345        let mut buffer = RopeBuffer::new(100);
346        
347        for _ in 0..200 {
348            buffer.newline();
349        }
350        
351        // Should be trimmed to around max_lines
352        // Note: trimming is chunk-based, so might be slightly over
353        assert!(buffer.len() <= 100 + CHUNK_SIZE);
354    }
355
356    #[test]
357    fn test_rope_buffer_scroll() {
358        let mut buffer = RopeBuffer::new(1000);
359        
360        for _ in 0..50 {
361            buffer.newline();
362        }
363        
364        assert_eq!(buffer.scroll_offset(), 0);
365        
366        buffer.scroll_up(10);
367        assert_eq!(buffer.scroll_offset(), 10);
368        
369        buffer.scroll_down(5);
370        assert_eq!(buffer.scroll_offset(), 5);
371        
372        buffer.scroll_to_bottom();
373        assert_eq!(buffer.scroll_offset(), 0);
374    }
375
376    #[test]
377    fn test_rope_buffer_visible_lines() {
378        let mut buffer = RopeBuffer::new(1000);
379        
380        for i in 0..20 {
381            buffer.append([Cell::new(char::from_u32('a' as u32 + i).unwrap())].into_iter());
382            buffer.newline();
383        }
384        
385        let visible: Vec<_> = buffer.visible_lines(10).collect();
386        assert_eq!(visible.len(), 10);
387    }
388
389    #[test]
390    fn test_rope_buffer_memory_stats() {
391        let mut buffer = RopeBuffer::new(1000);
392        
393        for _ in 0..100 {
394            buffer.append([Cell::new('x'); 80].into_iter());
395            buffer.newline();
396        }
397        
398        let stats = buffer.memory_stats();
399        assert_eq!(stats.lines, 101);
400        assert_eq!(stats.cells, 8000);
401        assert!(stats.bytes_estimated > 0);
402    }
403}