✨ Add offset-based sub-cursors and Rope seek mode
- Introduce SeekMode::Rope for absolute rope-index positioning - Add CursorState.offset field to support local coordinate systems per cursor - Implement ForwardCursor.cursor() and BackwardCursor(cursor()) to create sub-cursors with independent offsets - Update tell(), get(i), set i, len() to use local coordinates (relative offset) - Add rope_tell(), reset()—deprecate old absolute behavior in favor of offset-aware API - Add comprehensive tests for sub-cursor semantics, including write/reset and bounds checking
This commit is contained in:
+281
-53
@@ -51,11 +51,14 @@ pub enum SeekMode {
|
|||||||
Relative,
|
Relative,
|
||||||
/// `pos` is counted back from the end: target = `len - pos`.
|
/// `pos` is counted back from the end: target = `len - pos`.
|
||||||
RelativeToEnd,
|
RelativeToEnd,
|
||||||
|
/// `pos` is a rope index relative to the start of the rope.
|
||||||
|
Rope,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── shared state ──────────────────────────────────────────────────────────────
|
// ── 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
|
/// All fields are [`Cell`]-wrapped so they can be mutated through a shared
|
||||||
/// reference, enabling `&self` methods on cursors.
|
/// reference, enabling `&self` methods on cursors.
|
||||||
@@ -67,10 +70,17 @@ pub struct CursorState<'a> {
|
|||||||
block: Cell<&'a [Cell<u8>]>,
|
block: Cell<&'a [Cell<u8>]>,
|
||||||
initialized: Cell<bool>,
|
initialized: Cell<bool>,
|
||||||
current: Cell<Option<usize>>,
|
current: Cell<Option<usize>>,
|
||||||
|
/// Absolute rope index that maps to local position 0.
|
||||||
|
/// All user-facing coordinates are relative to this value.
|
||||||
|
offset: Cell<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CursorState<'a> {
|
impl<'a> CursorState<'a> {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
Self::with_offset(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_offset(offset: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
block_idx: Cell::new(0),
|
block_idx: Cell::new(0),
|
||||||
block_start: Cell::new(0),
|
block_start: Cell::new(0),
|
||||||
@@ -78,6 +88,7 @@ impl<'a> CursorState<'a> {
|
|||||||
block: Cell::new(&[]),
|
block: Cell::new(&[]),
|
||||||
initialized: Cell::new(false),
|
initialized: Cell::new(false),
|
||||||
current: Cell::new(None),
|
current: Cell::new(None),
|
||||||
|
offset: Cell::new(offset),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +114,8 @@ impl<'a> CursorState<'a> {
|
|||||||
self.block_idx.set(bi);
|
self.block_idx.set(bi);
|
||||||
self.block_start.set(bs);
|
self.block_start.set(bs);
|
||||||
self.block_end.set(be);
|
self.block_end.set(be);
|
||||||
self.block.set(rope.get_block(bi).ok_or(RopeError::BlockNotFound(format!(
|
self.block
|
||||||
|
.set(rope.get_block(bi).ok_or(RopeError::BlockNotFound(format!(
|
||||||
"Cannot find block for index {}",
|
"Cannot find block for index {}",
|
||||||
i
|
i
|
||||||
)))?);
|
)))?);
|
||||||
@@ -137,33 +149,59 @@ pub trait RopeCursor<'a> {
|
|||||||
/// Returns `Err` at the exhausted end.
|
/// Returns `Err` at the exhausted end.
|
||||||
fn read_next(&self) -> Result<u8, RopeError>;
|
fn read_next(&self) -> Result<u8, RopeError>;
|
||||||
|
|
||||||
/// Move the cursor to an absolute or relative position.
|
/// Move the cursor to a new position.
|
||||||
///
|
///
|
||||||
/// For [`ForwardCursor`], `Relative +n` advances toward the end.
|
/// `pos` is interpreted according to `mode`:
|
||||||
/// For [`BackwardCursor`], `Relative +n` retreats toward the start
|
/// - [`Absolute`](SeekMode::Absolute): local coordinate (`pos + offset` in the rope).
|
||||||
/// (i.e. subtracts from the current index).
|
/// - [`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<usize, RopeError>;
|
fn seek(&self, pos: isize, mode: SeekMode) -> Result<usize, RopeError>;
|
||||||
|
|
||||||
// ── default methods ───────────────────────────────────────────────────────
|
// ── 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<u8> {
|
fn get(&self, i: usize) -> Option<u8> {
|
||||||
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> {
|
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<usize> {
|
fn tell(&self) -> Option<usize> {
|
||||||
|
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<usize> {
|
||||||
self.state().current.get()
|
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 {
|
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.
|
/// Read the byte at the current position without advancing.
|
||||||
@@ -210,7 +248,10 @@ pub struct ForwardCursor<'a> {
|
|||||||
impl<'a> ForwardCursor<'a> {
|
impl<'a> ForwardCursor<'a> {
|
||||||
/// Create a new forward cursor positioned before the first byte.
|
/// Create a new forward cursor positioned before the first byte.
|
||||||
pub fn new(rope: &'a Rope) -> Self {
|
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.
|
/// Read the byte at `current + ahead` without moving the position.
|
||||||
@@ -220,15 +261,18 @@ impl<'a> ForwardCursor<'a> {
|
|||||||
.get(self.rope, pos + ahead)
|
.get(self.rope, pos + ahead)
|
||||||
.ok_or(RopeError::OutOfBounds(format!(
|
.ok_or(RopeError::OutOfBounds(format!(
|
||||||
"index out of bounds: i={} + {} > {}",
|
"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.
|
/// 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> {
|
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.set(self.rope, pos, value)?;
|
||||||
self.state.current.set(Some(pos + 1));
|
self.state.current.set(Some(pos + 1));
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -242,44 +286,74 @@ impl<'a> ForwardCursor<'a> {
|
|||||||
pub fn iter(&self) -> ForwardIter<'a, '_> {
|
pub fn iter(&self) -> ForwardIter<'a, '_> {
|
||||||
ForwardIter { cursor: self }
|
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> {
|
impl<'a> RopeCursor<'a> for ForwardCursor<'a> {
|
||||||
fn rope(&self) -> &'a Rope { self.rope }
|
fn rope(&self) -> &'a Rope {
|
||||||
fn state(&self) -> &CursorState<'a> { &self.state }
|
self.rope
|
||||||
|
}
|
||||||
|
fn state(&self) -> &CursorState<'a> {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
fn read_next(&self) -> Result<u8, RopeError> {
|
fn read_next(&self) -> Result<u8, RopeError> {
|
||||||
let next_pos = match self.state.current.get() {
|
let next_pos = match self.state.current.get() {
|
||||||
Some(i) => i + 1,
|
Some(i) => i + 1,
|
||||||
None => 0,
|
None => self.state.offset.get(),
|
||||||
};
|
};
|
||||||
let value = self.state
|
let value = self
|
||||||
|
.state
|
||||||
.get(self.rope, next_pos)
|
.get(self.rope, next_pos)
|
||||||
.ok_or(RopeError::OutOfBounds(format!(
|
.ok_or(RopeError::OutOfBounds(format!(
|
||||||
"index out of bounds: i={} > {}",
|
"index out of bounds: i={} > {}",
|
||||||
next_pos, self.rope.len()
|
next_pos,
|
||||||
|
self.rope.len()
|
||||||
)))?;
|
)))?;
|
||||||
self.state.current.set(Some(next_pos));
|
self.state.current.set(Some(next_pos));
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seek(&self, pos: isize, mode: SeekMode) -> Result<usize, RopeError> {
|
fn seek(&self, pos: isize, mode: SeekMode) -> Result<usize, RopeError> {
|
||||||
let pos = match mode {
|
let offset = self.state.offset.get() as isize;
|
||||||
SeekMode::Absolute => pos,
|
let abs_pos = match mode {
|
||||||
SeekMode::Relative => self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize + pos,
|
SeekMode::Absolute => pos + offset,
|
||||||
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
SeekMode::Relative => {
|
||||||
};
|
self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize + pos
|
||||||
if pos < 0 {
|
|
||||||
return Err(RopeError::OutOfBounds(format!("index out of bounds: i={} < 0", pos)));
|
|
||||||
}
|
}
|
||||||
self.state.current.set(Some(pos as usize));
|
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
||||||
Ok(pos as usize)
|
SeekMode::Rope => pos,
|
||||||
|
};
|
||||||
|
if abs_pos < 0 {
|
||||||
|
return Err(RopeError::OutOfBounds(format!(
|
||||||
|
"index out of bounds: i={} < 0",
|
||||||
|
abs_pos
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.state.current.set(Some(abs_pos as usize));
|
||||||
|
Ok(abs_pos as usize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for ForwardCursor<'_> {
|
impl Iterator for ForwardCursor<'_> {
|
||||||
type Item = u8;
|
type Item = u8;
|
||||||
fn next(&mut self) -> Option<Self::Item> { self.read_next().ok() }
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.read_next().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared-borrow iterator returned by [`ForwardCursor::iter`].
|
/// Shared-borrow iterator returned by [`ForwardCursor::iter`].
|
||||||
@@ -289,7 +363,9 @@ pub struct ForwardIter<'a, 'b> {
|
|||||||
|
|
||||||
impl Iterator for ForwardIter<'_, '_> {
|
impl Iterator for ForwardIter<'_, '_> {
|
||||||
type Item = u8;
|
type Item = u8;
|
||||||
fn next(&mut self) -> Option<u8> { self.cursor.read_next().ok() }
|
fn next(&mut self) -> Option<u8> {
|
||||||
|
self.cursor.read_next().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BackwardCursor ────────────────────────────────────────────────────────────
|
// ── BackwardCursor ────────────────────────────────────────────────────────────
|
||||||
@@ -311,7 +387,10 @@ pub struct BackwardCursor<'a> {
|
|||||||
impl<'a> BackwardCursor<'a> {
|
impl<'a> BackwardCursor<'a> {
|
||||||
/// Create a new backward cursor positioned past the last byte.
|
/// Create a new backward cursor positioned past the last byte.
|
||||||
pub fn new(rope: &'a Rope) -> Self {
|
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.
|
/// 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())
|
.filter(|&t| t < self.rope.len())
|
||||||
.ok_or(RopeError::OutOfBounds(format!(
|
.ok_or(RopeError::OutOfBounds(format!(
|
||||||
"index out of bounds: i={} + {} > {}",
|
"index out of bounds: i={} + {} > {}",
|
||||||
pos, behind, self.rope.len()
|
pos,
|
||||||
|
behind,
|
||||||
|
self.rope.len()
|
||||||
)))?;
|
)))?;
|
||||||
self.state
|
self.state
|
||||||
.get(self.rope, target)
|
.get(self.rope, target)
|
||||||
.ok_or(RopeError::OutOfBounds(format!(
|
.ok_or(RopeError::OutOfBounds(format!(
|
||||||
"index out of bounds: i={} + {} > {}",
|
"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, '_> {
|
pub fn iter(&self) -> BackwardIter<'a, '_> {
|
||||||
BackwardIter { cursor: self }
|
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> {
|
impl<'a> RopeCursor<'a> for BackwardCursor<'a> {
|
||||||
fn rope(&self) -> &'a Rope { self.rope }
|
fn rope(&self) -> &'a Rope {
|
||||||
fn state(&self) -> &CursorState<'a> { &self.state }
|
self.rope
|
||||||
|
}
|
||||||
|
fn state(&self) -> &CursorState<'a> {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
fn read_next(&self) -> Result<u8, RopeError> {
|
fn read_next(&self) -> Result<u8, RopeError> {
|
||||||
|
let offset = self.state.offset.get();
|
||||||
let next_pos = match self.state.current.get() {
|
let next_pos = match self.state.current.get() {
|
||||||
None => self.rope.len().checked_sub(1).ok_or(RopeError::OutOfBounds(
|
None => self
|
||||||
|
.rope
|
||||||
|
.len()
|
||||||
|
.checked_sub(1)
|
||||||
|
.ok_or(RopeError::OutOfBounds(
|
||||||
"BackwardCursor: rope is empty".to_string(),
|
"BackwardCursor: rope is empty".to_string(),
|
||||||
))?,
|
))?,
|
||||||
Some(0) => return Err(RopeError::OutOfBounds(
|
Some(i) if i <= offset => {
|
||||||
|
return Err(RopeError::OutOfBounds(
|
||||||
"BackwardCursor: already at beginning".to_string(),
|
"BackwardCursor: already at beginning".to_string(),
|
||||||
)),
|
));
|
||||||
|
}
|
||||||
Some(i) => i - 1,
|
Some(i) => i - 1,
|
||||||
};
|
};
|
||||||
let value = self.state
|
let value = self
|
||||||
|
.state
|
||||||
.get(self.rope, next_pos)
|
.get(self.rope, next_pos)
|
||||||
.ok_or(RopeError::OutOfBounds(format!(
|
.ok_or(RopeError::OutOfBounds(format!(
|
||||||
"BackwardCursor: index out of bounds at i={}",
|
"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<usize, RopeError> {
|
fn seek(&self, pos: isize, mode: SeekMode) -> Result<usize, RopeError> {
|
||||||
let pos = match mode {
|
let offset = self.state.offset.get() as isize;
|
||||||
SeekMode::Absolute => pos,
|
let abs_pos = match mode {
|
||||||
SeekMode::Relative => self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize - pos,
|
SeekMode::Absolute => pos + offset,
|
||||||
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
SeekMode::Relative => {
|
||||||
};
|
self.state.current.get().ok_or(RopeError::CurrentNotSet)? as isize - pos
|
||||||
if pos < 0 {
|
|
||||||
return Err(RopeError::OutOfBounds(format!("index out of bounds: i={} < 0", pos)));
|
|
||||||
}
|
}
|
||||||
self.state.current.set(Some(pos as usize));
|
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
||||||
Ok(pos as usize)
|
SeekMode::Rope => pos,
|
||||||
|
};
|
||||||
|
if abs_pos < 0 {
|
||||||
|
return Err(RopeError::OutOfBounds(format!(
|
||||||
|
"index out of bounds: i={} < 0",
|
||||||
|
abs_pos
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.state.current.set(Some(abs_pos as usize));
|
||||||
|
Ok(abs_pos as usize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for BackwardCursor<'_> {
|
impl Iterator for BackwardCursor<'_> {
|
||||||
type Item = u8;
|
type Item = u8;
|
||||||
fn next(&mut self) -> Option<Self::Item> { self.read_next().ok() }
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.read_next().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared-borrow iterator returned by [`BackwardCursor::iter`].
|
/// Shared-borrow iterator returned by [`BackwardCursor::iter`].
|
||||||
@@ -392,7 +510,9 @@ pub struct BackwardIter<'a, 'b> {
|
|||||||
|
|
||||||
impl Iterator for BackwardIter<'_, '_> {
|
impl Iterator for BackwardIter<'_, '_> {
|
||||||
type Item = u8;
|
type Item = u8;
|
||||||
fn next(&mut self) -> Option<u8> { self.cursor.read_next().ok() }
|
fn next(&mut self) -> Option<u8> {
|
||||||
|
self.cursor.read_next().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── tests ─────────────────────────────────────────────────────────────────────
|
// ── tests ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -589,4 +709,112 @@ mod tests {
|
|||||||
let c = r.fw_cursor();
|
let c = r.fw_cursor();
|
||||||
assert!(c.read_next().is_err());
|
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<u8> = 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user