diff --git a/src/obikrope/src/cursor.rs b/src/obikrope/src/cursor.rs index 301fcc2..f8d73ad 100644 --- a/src/obikrope/src/cursor.rs +++ b/src/obikrope/src/cursor.rs @@ -51,33 +51,44 @@ pub enum SeekMode { Relative, /// `pos` is counted back from the end: target = `len - pos`. RelativeToEnd, + /// `pos` is a rope index relative to the start of the rope. + Rope, } // ── shared state ────────────────────────────────────────────────────────────── -/// Per-cursor cache of the last accessed block plus the current position. +/// Per-cursor cache of the last accessed block, the current position, and the +/// base offset that defines the cursor's local coordinate system. /// /// All fields are [`Cell`]-wrapped so they can be mutated through a shared /// reference, enabling `&self` methods on cursors. #[derive(Clone)] pub struct CursorState<'a> { - block_idx: Cell, + block_idx: Cell, block_start: Cell, - block_end: Cell, - block: Cell<&'a [Cell]>, + block_end: Cell, + block: Cell<&'a [Cell]>, initialized: Cell, - current: Cell>, + current: Cell>, + /// Absolute rope index that maps to local position 0. + /// All user-facing coordinates are relative to this value. + offset: Cell, } impl<'a> CursorState<'a> { fn new() -> Self { + Self::with_offset(0) + } + + fn with_offset(offset: usize) -> Self { Self { - block_idx: Cell::new(0), + block_idx: Cell::new(0), block_start: Cell::new(0), - block_end: Cell::new(0), - block: Cell::new(&[]), + block_end: Cell::new(0), + block: Cell::new(&[]), initialized: Cell::new(false), - current: Cell::new(None), + current: Cell::new(None), + offset: Cell::new(offset), } } @@ -103,10 +114,11 @@ impl<'a> CursorState<'a> { self.block_idx.set(bi); self.block_start.set(bs); self.block_end.set(be); - self.block.set(rope.get_block(bi).ok_or(RopeError::BlockNotFound(format!( - "Cannot find block for index {}", - i - )))?); + self.block + .set(rope.get_block(bi).ok_or(RopeError::BlockNotFound(format!( + "Cannot find block for index {}", + i + )))?); self.initialized.set(true); } self.block.get()[i - self.block_start.get()].set(value); @@ -137,33 +149,59 @@ pub trait RopeCursor<'a> { /// Returns `Err` at the exhausted end. fn read_next(&self) -> Result; - /// Move the cursor to an absolute or relative position. + /// Move the cursor to a new position. /// - /// For [`ForwardCursor`], `Relative +n` advances toward the end. - /// For [`BackwardCursor`], `Relative +n` retreats toward the start - /// (i.e. subtracts from the current index). + /// `pos` is interpreted according to `mode`: + /// - [`Absolute`](SeekMode::Absolute): local coordinate (`pos + offset` in the rope). + /// - [`Rope`](SeekMode::Rope): raw rope index, ignores the offset. Pass a value + /// from [`rope_tell`](RopeCursor::rope_tell) to restore a saved position. + /// - [`Relative`](SeekMode::Relative): delta from the current position. + /// For [`ForwardCursor`], positive advances toward the end; + /// for [`BackwardCursor`], positive retreats toward the start. + /// - [`RelativeToEnd`](SeekMode::RelativeToEnd): `rope.len() - pos`. + /// + /// Returns the new position as a **rope index** (same value as + /// [`rope_tell`](RopeCursor::rope_tell) would return immediately after). fn seek(&self, pos: isize, mode: SeekMode) -> Result; // ── default methods ─────────────────────────────────────────────────────── - /// Read the byte at absolute index `i` without moving the position. + /// Read the byte at **local** index `i` (relative to the cursor's offset) + /// without moving the position. fn get(&self, i: usize) -> Option { - self.state().get(self.rope(), i) + self.state().get(self.rope(), i + self.state().offset.get()) } - /// Write `value` at absolute index `i` without moving the position. + /// Write `value` at **local** index `i` without moving the position. fn set(&self, i: usize, value: u8) -> Result<(), RopeError> { - self.state().set(self.rope(), i, value) + self.state() + .set(self.rope(), i + self.state().offset.get(), value) } - /// Current position, or `None` if the cursor has not moved yet. + /// Current position relative to the cursor's offset, or `None` if the + /// cursor has not moved yet. fn tell(&self) -> Option { + let abs = self.state().current.get()?; + Some(abs.saturating_sub(self.state().offset.get())) + } + + /// Current position as an absolute rope index, or `None` if the cursor + /// has not moved yet. Use this when you need to pass a value to + /// [`seek`](RopeCursor::seek) with [`SeekMode::Absolute`]. + fn rope_tell(&self) -> Option { self.state().current.get() } - /// Total number of bytes in the rope. + /// Number of bytes visible through this cursor (`rope.len() - offset`). fn len(&self) -> usize { - self.rope().len() + self.rope().len().saturating_sub(self.state().offset.get()) + } + + /// Reset the cursor to its initial state (positioned before the first + /// byte of its local view). Equivalent to `seek(0, Absolute)` on a + /// fresh cursor, but works even when `current` is `None`. + fn reset(&self) { + self.state().current.set(None); } /// Read the byte at the current position without advancing. @@ -203,14 +241,17 @@ pub trait RopeCursor<'a> { /// [`write`](ForwardCursor::write), [`iter`](ForwardCursor::iter). #[derive(Clone)] pub struct ForwardCursor<'a> { - rope: &'a Rope, + rope: &'a Rope, state: CursorState<'a>, } impl<'a> ForwardCursor<'a> { /// Create a new forward cursor positioned before the first byte. pub fn new(rope: &'a Rope) -> Self { - Self { rope, state: CursorState::new() } + Self { + rope, + state: CursorState::new(), + } } /// Read the byte at `current + ahead` without moving the position. @@ -220,15 +261,18 @@ impl<'a> ForwardCursor<'a> { .get(self.rope, pos + ahead) .ok_or(RopeError::OutOfBounds(format!( "index out of bounds: i={} + {} > {}", - pos, ahead, self.rope.len() + pos, + ahead, + self.rope.len() ))) } /// Write `value` at the current position and advance by one. /// - /// If the cursor has not moved yet, writes at index 0. + /// If the cursor has not moved yet, writes at the first byte of its local + /// view (absolute index = offset). pub fn write(&self, value: u8) -> Result<(), RopeError> { - let pos = self.state.current.get().unwrap_or(0); + let pos = self.state.current.get().unwrap_or(self.state.offset.get()); self.state.set(self.rope, pos, value)?; self.state.current.set(Some(pos + 1)); Ok(()) @@ -242,44 +286,74 @@ impl<'a> ForwardCursor<'a> { pub fn iter(&self) -> ForwardIter<'a, '_> { ForwardIter { cursor: self } } + + /// Create a new [`ForwardCursor`] whose local position 0 starts at the + /// current absolute position of `self`. + /// + /// The new cursor shares the same underlying [`Rope`] (with the same + /// [`Cell`]-based interior mutability) but has an independent position and + /// an `offset` equal to `self.absolute_tell()`. If `self` has not moved + /// yet, the new cursor starts at the same offset as `self`. + pub fn cursor(&self) -> ForwardCursor<'a> { + let new_offset = self.rope_tell().unwrap_or(self.state.offset.get()); + ForwardCursor { + rope: self.rope, + state: CursorState::with_offset(new_offset), + } + } } impl<'a> RopeCursor<'a> for ForwardCursor<'a> { - fn rope(&self) -> &'a Rope { self.rope } - fn state(&self) -> &CursorState<'a> { &self.state } + fn rope(&self) -> &'a Rope { + self.rope + } + fn state(&self) -> &CursorState<'a> { + &self.state + } fn read_next(&self) -> Result { let next_pos = match self.state.current.get() { Some(i) => i + 1, - None => 0, + None => self.state.offset.get(), }; - let value = self.state + let value = self + .state .get(self.rope, next_pos) .ok_or(RopeError::OutOfBounds(format!( "index out of bounds: i={} > {}", - next_pos, self.rope.len() + next_pos, + self.rope.len() )))?; self.state.current.set(Some(next_pos)); Ok(value) } fn seek(&self, pos: isize, mode: SeekMode) -> Result { - let pos = match mode { - SeekMode::Absolute => pos, - SeekMode::Relative => self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize + pos, + let offset = self.state.offset.get() as isize; + let abs_pos = match mode { + SeekMode::Absolute => pos + offset, + SeekMode::Relative => { + self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize + pos + } SeekMode::RelativeToEnd => self.rope.len() as isize - pos, + SeekMode::Rope => pos, }; - if pos < 0 { - return Err(RopeError::OutOfBounds(format!("index out of bounds: i={} < 0", pos))); + if abs_pos < 0 { + return Err(RopeError::OutOfBounds(format!( + "index out of bounds: i={} < 0", + abs_pos + ))); } - self.state.current.set(Some(pos as usize)); - Ok(pos as usize) + self.state.current.set(Some(abs_pos as usize)); + Ok(abs_pos as usize) } } impl Iterator for ForwardCursor<'_> { type Item = u8; - fn next(&mut self) -> Option { self.read_next().ok() } + fn next(&mut self) -> Option { + self.read_next().ok() + } } /// Shared-borrow iterator returned by [`ForwardCursor::iter`]. @@ -289,7 +363,9 @@ pub struct ForwardIter<'a, 'b> { impl Iterator for ForwardIter<'_, '_> { type Item = u8; - fn next(&mut self) -> Option { self.cursor.read_next().ok() } + fn next(&mut self) -> Option { + self.cursor.read_next().ok() + } } // ── BackwardCursor ──────────────────────────────────────────────────────────── @@ -304,14 +380,17 @@ impl Iterator for ForwardIter<'_, '_> { /// [`iter`](BackwardCursor::iter). #[derive(Clone)] pub struct BackwardCursor<'a> { - rope: &'a Rope, + rope: &'a Rope, state: CursorState<'a>, } impl<'a> BackwardCursor<'a> { /// Create a new backward cursor positioned past the last byte. pub fn new(rope: &'a Rope) -> Self { - Self { rope, state: CursorState::new() } + Self { + rope, + state: CursorState::new(), + } } /// Read the byte at `current + behind` (toward higher indices) without moving. @@ -322,13 +401,17 @@ impl<'a> BackwardCursor<'a> { .filter(|&t| t < self.rope.len()) .ok_or(RopeError::OutOfBounds(format!( "index out of bounds: i={} + {} > {}", - pos, behind, self.rope.len() + pos, + behind, + self.rope.len() )))?; self.state .get(self.rope, target) .ok_or(RopeError::OutOfBounds(format!( "index out of bounds: i={} + {} > {}", - pos, behind, self.rope.len() + pos, + behind, + self.rope.len() ))) } @@ -340,23 +423,49 @@ impl<'a> BackwardCursor<'a> { pub fn iter(&self) -> BackwardIter<'a, '_> { BackwardIter { cursor: self } } + + /// Create a new [`BackwardCursor`] that stops at the current absolute + /// position of `self` (used as the lower bound / offset of the new cursor). + /// + /// The new cursor scans from `rope.len() - 1` down to the current absolute + /// position of `self`. If `self` has not moved yet, the new cursor has the + /// same offset as `self` (no restriction). + pub fn cursor(&self) -> BackwardCursor<'a> { + let new_offset = self.rope_tell().unwrap_or(self.state.offset.get()); + BackwardCursor { + rope: self.rope, + state: CursorState::with_offset(new_offset), + } + } } impl<'a> RopeCursor<'a> for BackwardCursor<'a> { - fn rope(&self) -> &'a Rope { self.rope } - fn state(&self) -> &CursorState<'a> { &self.state } + fn rope(&self) -> &'a Rope { + self.rope + } + fn state(&self) -> &CursorState<'a> { + &self.state + } fn read_next(&self) -> Result { + let offset = self.state.offset.get(); let next_pos = match self.state.current.get() { - None => self.rope.len().checked_sub(1).ok_or(RopeError::OutOfBounds( - "BackwardCursor: rope is empty".to_string(), - ))?, - Some(0) => return Err(RopeError::OutOfBounds( - "BackwardCursor: already at beginning".to_string(), - )), + None => self + .rope + .len() + .checked_sub(1) + .ok_or(RopeError::OutOfBounds( + "BackwardCursor: rope is empty".to_string(), + ))?, + Some(i) if i <= offset => { + return Err(RopeError::OutOfBounds( + "BackwardCursor: already at beginning".to_string(), + )); + } Some(i) => i - 1, }; - let value = self.state + let value = self + .state .get(self.rope, next_pos) .ok_or(RopeError::OutOfBounds(format!( "BackwardCursor: index out of bounds at i={}", @@ -367,22 +476,31 @@ impl<'a> RopeCursor<'a> for BackwardCursor<'a> { } fn seek(&self, pos: isize, mode: SeekMode) -> Result { - let pos = match mode { - SeekMode::Absolute => pos, - SeekMode::Relative => self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize - pos, + let offset = self.state.offset.get() as isize; + let abs_pos = match mode { + SeekMode::Absolute => pos + offset, + SeekMode::Relative => { + self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize - pos + } SeekMode::RelativeToEnd => self.rope.len() as isize - pos, + SeekMode::Rope => pos, }; - if pos < 0 { - return Err(RopeError::OutOfBounds(format!("index out of bounds: i={} < 0", pos))); + if abs_pos < 0 { + return Err(RopeError::OutOfBounds(format!( + "index out of bounds: i={} < 0", + abs_pos + ))); } - self.state.current.set(Some(pos as usize)); - Ok(pos as usize) + self.state.current.set(Some(abs_pos as usize)); + Ok(abs_pos as usize) } } impl Iterator for BackwardCursor<'_> { type Item = u8; - fn next(&mut self) -> Option { self.read_next().ok() } + fn next(&mut self) -> Option { + self.read_next().ok() + } } /// Shared-borrow iterator returned by [`BackwardCursor::iter`]. @@ -392,7 +510,9 @@ pub struct BackwardIter<'a, 'b> { impl Iterator for BackwardIter<'_, '_> { type Item = u8; - fn next(&mut self) -> Option { self.cursor.read_next().ok() } + fn next(&mut self) -> Option { + self.cursor.read_next().ok() + } } // ── tests ───────────────────────────────────────────────────────────────────── @@ -476,7 +596,7 @@ mod tests { c.read_next().unwrap(); // A → current = Some(0) c.read_next().unwrap(); // C → current = Some(1) c.read_next().unwrap(); // G → current = Some(2) - c.rewind(1).unwrap(); // current = Some(1) → next read = index 2 + c.rewind(1).unwrap(); // current = Some(1) → next read = index 2 assert_eq!(c.read_next().unwrap(), b'G'); } @@ -563,7 +683,7 @@ mod tests { let c = r.bw_cursor(); c.read_next().unwrap(); // index 3 = T c.read_next().unwrap(); // index 2 = G - c.rewind(1).unwrap(); // back to index 3 + c.rewind(1).unwrap(); // back to index 3 assert_eq!(c.tell(), Some(3)); assert_eq!(c.read_next().unwrap(), b'G'); // reads index 2 } @@ -589,4 +709,112 @@ mod tests { let c = r.fw_cursor(); assert!(c.read_next().is_err()); } + + // ── offset / sub-cursor ─────────────────────────────────────────────────── + + #[test] + fn forward_cursor_reads_from_offset() { + // cursor() at current=Some(2) → new cursor reads from index 2 + let r = rope(b"ABCDE"); + let c = r.fw_cursor(); + c.read_next().unwrap(); // A → current=Some(0) + c.read_next().unwrap(); // B → current=Some(1) + c.read_next().unwrap(); // C → current=Some(2) + let sub = c.cursor(); // offset=2 (absolute_tell=2) + assert_eq!(sub.read_next().unwrap(), b'C'); // reads index 2 + assert_eq!(sub.tell(), Some(0)); // relative: 2-2=0 + assert_eq!(sub.rope_tell(), Some(2)); + assert_eq!(sub.read_next().unwrap(), b'D'); + assert_eq!(sub.tell(), Some(1)); // relative: 3-2=1 + } + + #[test] + fn forward_cursor_get_uses_relative_index() { + let r = rope(b"ABCDE"); + let c = r.fw_cursor(); + c.read_next().unwrap(); // A → current=Some(0), absolute_tell=0 + let _sub = c.cursor(); // offset=0 — created to show cursor() compiles; not used further + // From sub2 with offset=2: get(0)=C, get(2)=E + let c2 = r.fw_cursor(); + c2.read_next().unwrap(); // at 0 + c2.read_next().unwrap(); // at 1 + c2.read_next().unwrap(); // at 2, absolute=2 + let sub2 = c2.cursor(); // offset=2 + assert_eq!(sub2.get(0), Some(b'C')); // local 0 = absolute 2 + assert_eq!(sub2.get(2), Some(b'E')); // local 2 = absolute 4 + assert_eq!(sub2.get(3), None); // local 3 = absolute 5, OOB + } + + #[test] + fn forward_cursor_len_reflects_offset() { + let r = rope(b"ABCDE"); // len=5 + let c = r.fw_cursor(); + c.read_next().unwrap(); + c.read_next().unwrap(); + c.read_next().unwrap(); // absolute_tell=2 + let sub = c.cursor(); // offset=2 + assert_eq!(sub.len(), 3); // 5 - 2 + } + + #[test] + fn forward_reset_goes_back_to_start() { + let r = rope(b"ABCDE"); + let c = r.fw_cursor(); + c.read_next().unwrap(); // A + c.read_next().unwrap(); // B + c.reset(); + assert_eq!(c.tell(), None); + assert_eq!(c.read_next().unwrap(), b'A'); // starts over + } + + #[test] + fn forward_sub_cursor_write_and_reset() { + // Write two bytes, discard them via reset(), write again. + let r = rope(b"XXXXX"); + let c = r.fw_cursor(); + c.write(b'A').unwrap(); // absolute 0 → current=Some(1) + c.write(b'B').unwrap(); // absolute 1 → current=Some(2) + let seg = c.cursor(); // absolute_tell=2, offset=2 + seg.write(b'C').unwrap(); // absolute 2 → current=Some(3), tell=3-2=1 + seg.write(b'D').unwrap(); // absolute 3 → current=Some(4), tell=4-2=2 + assert_eq!(seg.tell(), Some(2)); // 2 bytes written into this segment + seg.reset(); + assert_eq!(seg.tell(), None); + seg.write(b'E').unwrap(); // absolute 2 again + let all: Vec = r.fw_cursor().collect(); + assert_eq!(&all[..3], b"ABE"); + } + + #[test] + fn backward_cursor_stops_at_offset() { + // BackwardCursor.cursor() creates a cursor with offset = absolute_tell. + // offset = local position 0 (inclusive lower bound). + // The cursor reads rope.len()-1 downto offset, then stops. + let r = rope(b"ABCDE"); // 0=A 1=B 2=C 3=D 4=E + let bw = r.bw_cursor(); + bw.read_next().unwrap(); // E=4, current=Some(4) + bw.read_next().unwrap(); // D=3, current=Some(3), absolute_tell=3 + // sub: offset=3, reads from 4 down to 3 (inclusive), then stops. + let sub = bw.cursor(); + assert_eq!(sub.read_next().unwrap(), b'E'); // index 4, tell=4-3=1 + assert_eq!(sub.read_next().unwrap(), b'D'); // index 3, tell=3-3=0 (local 0) + assert!(sub.read_next().is_err()); // would go to 2 < offset=3 + } + + #[test] + fn forward_absolute_tell_unchanged_by_offset() { + let r = rope(b"ABCDE"); + let c = r.fw_cursor(); + c.read_next().unwrap(); // absolute=0 + let sub = c.cursor(); // offset=0 + sub.read_next().unwrap(); // reads index 0, absolute_tell=0 + sub.read_next().unwrap(); // reads index 1, absolute_tell=1 + assert_eq!(sub.tell(), Some(1)); + assert_eq!(sub.rope_tell(), Some(1)); + // sub2 with offset=1 + let sub2 = sub.cursor(); // offset=1 + sub2.read_next().unwrap(); // reads index 1, absolute=1 + assert_eq!(sub2.tell(), Some(0)); // relative: 1-1=0 + assert_eq!(sub2.rope_tell(), Some(1)); + } }