✨ 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,
|
||||
/// `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.
|
||||
@@ -67,10 +70,17 @@ pub struct CursorState<'a> {
|
||||
block: Cell<&'a [Cell<u8>]>,
|
||||
initialized: Cell<bool>,
|
||||
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_start: Cell::new(0),
|
||||
@@ -78,6 +88,7 @@ impl<'a> CursorState<'a> {
|
||||
block: Cell::new(&[]),
|
||||
initialized: Cell::new(false),
|
||||
current: Cell::new(None),
|
||||
offset: Cell::new(offset),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +114,8 @@ 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!(
|
||||
self.block
|
||||
.set(rope.get_block(bi).ok_or(RopeError::BlockNotFound(format!(
|
||||
"Cannot find block for index {}",
|
||||
i
|
||||
)))?);
|
||||
@@ -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.
|
||||
@@ -210,7 +248,10 @@ pub struct ForwardCursor<'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,
|
||||
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
||||
};
|
||||
if pos < 0 {
|
||||
return Err(RopeError::OutOfBounds(format!("index out of bounds: i={} < 0", 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
|
||||
}
|
||||
self.state.current.set(Some(pos as usize));
|
||||
Ok(pos as usize)
|
||||
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
||||
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<'_> {
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
@@ -311,7 +387,10 @@ pub struct BackwardCursor<'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(
|
||||
None => self
|
||||
.rope
|
||||
.len()
|
||||
.checked_sub(1)
|
||||
.ok_or(RopeError::OutOfBounds(
|
||||
"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(),
|
||||
)),
|
||||
));
|
||||
}
|
||||
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,
|
||||
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
||||
};
|
||||
if pos < 0 {
|
||||
return Err(RopeError::OutOfBounds(format!("index out of bounds: i={} < 0", 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
|
||||
}
|
||||
self.state.current.set(Some(pos as usize));
|
||||
Ok(pos as usize)
|
||||
SeekMode::RelativeToEnd => self.rope.len() as isize - pos,
|
||||
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<'_> {
|
||||
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 ─────────────────────────────────────────────────────────────────────
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user