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:
Eric Coissac
2026-04-19 22:10:19 +02:00
parent 41095a40d0
commit 3716eeff7f
+281 -53
View File
@@ -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));
}
} }