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
+295 -67
View File
@@ -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<usize>,
block_idx: Cell<usize>,
block_start: Cell<usize>,
block_end: Cell<usize>,
block: Cell<&'a [Cell<u8>]>,
block_end: Cell<usize>,
block: Cell<&'a [Cell<u8>]>,
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> {
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<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.
/// 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<usize, RopeError>;
// ── 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> {
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<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()
}
/// 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<u8, RopeError> {
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<usize, RopeError> {
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::Item> { self.read_next().ok() }
fn next(&mut self) -> Option<Self::Item> {
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<u8> { self.cursor.read_next().ok() }
fn next(&mut self) -> Option<u8> {
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<u8, RopeError> {
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<usize, RopeError> {
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::Item> { self.read_next().ok() }
fn next(&mut self) -> Option<Self::Item> {
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<u8> { self.cursor.read_next().ok() }
fn next(&mut self) -> Option<u8> {
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<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));
}
}