use crate::buffer::Cell;
const CHUNK_SIZE: usize = 64;
#[derive(Debug, Clone)]
struct Chunk {
lines: Vec<ChunkedLine>,
}
impl Chunk {
fn new() -> Self {
Self {
lines: Vec::with_capacity(CHUNK_SIZE),
}
}
const fn is_full(&self) -> bool {
self.lines.len() >= CHUNK_SIZE
}
const fn len(&self) -> usize {
self.lines.len()
}
#[allow(dead_code)]
const fn is_empty(&self) -> bool {
self.lines.is_empty()
}
fn push(&mut self, line: ChunkedLine) {
self.lines.push(line);
}
fn get(&self, index: usize) -> Option<&ChunkedLine> {
self.lines.get(index)
}
fn get_mut(&mut self, index: usize) -> Option<&mut ChunkedLine> {
self.lines.get_mut(index)
}
}
#[derive(Debug, Clone)]
pub struct ChunkedLine {
pub content: Vec<Cell>,
pub wrapped: bool,
}
impl ChunkedLine {
pub const fn new(content: Vec<Cell>, wrapped: bool) -> Self {
Self { content, wrapped }
}
pub const fn empty() -> Self {
Self {
content: Vec::new(),
wrapped: false,
}
}
pub const fn len(&self) -> usize {
self.content.len()
}
pub const fn is_empty(&self) -> bool {
self.content.is_empty()
}
}
#[derive(Debug)]
pub struct RopeBuffer {
chunks: Vec<Chunk>,
total_lines: usize,
max_lines: usize,
scroll_offset: usize,
}
impl RopeBuffer {
pub fn new(max_lines: usize) -> Self {
let mut buffer = Self {
chunks: Vec::new(),
total_lines: 0,
max_lines,
scroll_offset: 0,
};
buffer.push_line(ChunkedLine::empty());
buffer
}
pub fn unbounded() -> Self {
Self::new(0)
}
pub const fn len(&self) -> usize {
self.total_lines
}
pub const fn is_empty(&self) -> bool {
self.total_lines == 0
}
pub const fn chunk_count(&self) -> usize {
self.chunks.len()
}
pub fn get_line(&self, index: usize) -> Option<&ChunkedLine> {
if index >= self.total_lines {
return None;
}
let chunk_idx = index / CHUNK_SIZE;
let line_idx = index % CHUNK_SIZE;
self.chunks.get(chunk_idx)?.get(line_idx)
}
pub fn get_line_mut(&mut self, index: usize) -> Option<&mut ChunkedLine> {
if index >= self.total_lines {
return None;
}
let chunk_idx = index / CHUNK_SIZE;
let line_idx = index % CHUNK_SIZE;
self.chunks.get_mut(chunk_idx)?.get_mut(line_idx)
}
pub fn current_line(&self) -> Option<&ChunkedLine> {
if self.total_lines == 0 {
return None;
}
self.get_line(self.total_lines - 1)
}
pub fn current_line_mut(&mut self) -> Option<&mut ChunkedLine> {
if self.total_lines == 0 {
return None;
}
let idx = self.total_lines - 1;
self.get_line_mut(idx)
}
pub fn push_line(&mut self, line: ChunkedLine) {
if self.chunks.is_empty() || self.chunks.last().is_none_or(Chunk::is_full) {
self.chunks.push(Chunk::new());
}
if let Some(chunk) = self.chunks.last_mut() {
chunk.push(line);
self.total_lines += 1;
}
if self.max_lines > 0 && self.total_lines > self.max_lines {
self.trim_front();
}
}
pub fn newline(&mut self) {
self.push_line(ChunkedLine::empty());
}
pub fn append(&mut self, cells: impl Iterator<Item = Cell>) {
if let Some(line) = self.current_line_mut() {
line.content.extend(cells);
}
}
pub fn clear(&mut self) {
self.chunks.clear();
self.total_lines = 0;
self.scroll_offset = 0;
self.push_line(ChunkedLine::empty());
}
pub const fn scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn scroll_up(&mut self, lines: usize) {
let max_offset = self.total_lines.saturating_sub(1);
self.scroll_offset = (self.scroll_offset + lines).min(max_offset);
}
pub const fn scroll_down(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
}
pub const fn scroll_to_bottom(&mut self) {
self.scroll_offset = 0;
}
pub fn visible_lines(&self, viewport_height: usize) -> impl Iterator<Item = (usize, &ChunkedLine)> {
let end = self.total_lines.saturating_sub(self.scroll_offset);
let start = end.saturating_sub(viewport_height);
(start..end).filter_map(move |i| {
self.get_line(i).map(|line| (i, line))
})
}
fn trim_front(&mut self) {
while self.total_lines > self.max_lines && !self.chunks.is_empty() {
let removed_chunk = self.chunks.remove(0);
self.total_lines -= removed_chunk.len();
if self.scroll_offset > removed_chunk.len() {
self.scroll_offset -= removed_chunk.len();
} else {
self.scroll_offset = 0;
}
}
}
pub fn memory_stats(&self) -> RopeMemoryStats {
let mut total_cells = 0;
for chunk in &self.chunks {
for line in &chunk.lines {
total_cells += line.content.len();
}
}
RopeMemoryStats {
chunks: self.chunks.len(),
lines: self.total_lines,
cells: total_cells,
bytes_estimated: self.chunks.len() * std::mem::size_of::<Chunk>()
+ self.total_lines * std::mem::size_of::<ChunkedLine>()
+ total_cells * std::mem::size_of::<Cell>(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RopeMemoryStats {
pub chunks: usize,
pub lines: usize,
pub cells: usize,
pub bytes_estimated: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rope_buffer_basic() {
let mut buffer = RopeBuffer::new(1000);
assert_eq!(buffer.len(), 1);
buffer.append([Cell::new('H'), Cell::new('i')].into_iter());
assert_eq!(buffer.current_line().unwrap().len(), 2);
buffer.newline();
assert_eq!(buffer.len(), 2);
}
#[test]
fn test_rope_buffer_chunks() {
let mut buffer = RopeBuffer::unbounded();
for i in 0..200 {
buffer.newline();
buffer.append([Cell::new(char::from_u32(('a' as u32) + (i % 26)).unwrap())].into_iter());
}
assert!(buffer.chunk_count() > 1);
assert_eq!(buffer.len(), 201); }
#[test]
fn test_rope_buffer_max_lines() {
let mut buffer = RopeBuffer::new(100);
for _ in 0..200 {
buffer.newline();
}
assert!(buffer.len() <= 100 + CHUNK_SIZE);
}
#[test]
fn test_rope_buffer_scroll() {
let mut buffer = RopeBuffer::new(1000);
for _ in 0..50 {
buffer.newline();
}
assert_eq!(buffer.scroll_offset(), 0);
buffer.scroll_up(10);
assert_eq!(buffer.scroll_offset(), 10);
buffer.scroll_down(5);
assert_eq!(buffer.scroll_offset(), 5);
buffer.scroll_to_bottom();
assert_eq!(buffer.scroll_offset(), 0);
}
#[test]
fn test_rope_buffer_visible_lines() {
let mut buffer = RopeBuffer::new(1000);
for i in 0..20 {
buffer.append([Cell::new(char::from_u32('a' as u32 + i).unwrap())].into_iter());
buffer.newline();
}
let visible: Vec<_> = buffer.visible_lines(10).collect();
assert_eq!(visible.len(), 10);
}
#[test]
fn test_rope_buffer_memory_stats() {
let mut buffer = RopeBuffer::new(1000);
for _ in 0..100 {
buffer.append([Cell::new('x'); 80].into_iter());
buffer.newline();
}
let stats = buffer.memory_stats();
assert_eq!(stats.lines, 101);
assert_eq!(stats.cells, 8000);
assert!(stats.bytes_estimated > 0);
}
}