Add modular data structures, parallel pipelines, and system profiling
Establishes foundational infrastructure across multiple crates by introducing unified persistent bit matrix storage with columnar, packed, and implicit variants, alongside De Bruijn graph node encoding and unitig iteration logic. Adds a macro-driven parallel pipeline scheduler featuring NUMA-aware runners, bounded channels, and memory budgets to enforce concurrency limits. Implements streaming nucleotide parsers with pooled page buffers for FASTA, FASTQ, and Genbank formats, complemented by system resource monitoring, progress tracking, and stage profiling utilities. Collectively, these changes provide the core data models, execution frameworks, and I/O pipelines required for downstream k-mer indexing and analysis workloads.
This commit is contained in:
@@ -1,828 +0,0 @@
|
||||
//! Cursors for sequential and random access over a [`Rope`].
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
//! A cursor borrows a `&'a Rope` and keeps a small block cache so that
|
||||
//! consecutive accesses within the same block cost O(1). The first access to a
|
||||
//! new block costs O(log n) (binary search in [`Rope::lookup`]); subsequent
|
||||
//! accesses within that block are free.
|
||||
//!
|
||||
//! All mutable state (current position, cache) is stored in [`Cell`] fields,
|
||||
//! so every cursor method takes `&self` rather than `&mut self`. This means:
|
||||
//!
|
||||
//! - Two cursors can coexist on the same rope without lifetime conflicts.
|
||||
//! - The `iter()` method returns a lightweight wrapper that holds `&Cursor`,
|
||||
//! allowing `cursor.tell()` or `cursor.seek()` to be called **inside a `for`
|
||||
//! loop** over the same cursor.
|
||||
//!
|
||||
//! # Cursors
|
||||
//!
|
||||
//! | Type | Direction | First `read_next` | `seek(Relative, +n)` |
|
||||
//! |------|-----------|-------------------|----------------------|
|
||||
//! | [`ForwardCursor`] | start → end | index 0 | advances (+n) |
|
||||
//! | [`BackwardCursor`] | end → start | index `len-1` | retreats (+n) |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use obikrope::{Rope, RopeCursor};
|
||||
//!
|
||||
//! let mut rope = Rope::new(None);
|
||||
//! rope.push(b"ACGT".to_vec());
|
||||
//!
|
||||
//! let cursor = rope.fw_cursor();
|
||||
//! for byte in cursor.iter() {
|
||||
//! // cursor.tell() is valid here — iter() holds &cursor, not &mut cursor
|
||||
//! let _ = cursor.tell();
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::{Rope, RopeError};
|
||||
|
||||
/// Controls how the `pos` argument of [`RopeCursor::seek`] is interpreted.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SeekMode {
|
||||
/// `pos` is an absolute byte index from the start of the rope.
|
||||
Absolute,
|
||||
/// `pos` is relative to the current position.
|
||||
/// Positive = forward for [`ForwardCursor`], backward for [`BackwardCursor`].
|
||||
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, 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_start: Cell<usize>,
|
||||
block_end: Cell<usize>,
|
||||
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),
|
||||
block_end: Cell::new(0),
|
||||
block: Cell::new(&[]),
|
||||
initialized: Cell::new(false),
|
||||
current: Cell::new(None),
|
||||
offset: Cell::new(offset),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, rope: &'a Rope, i: usize) -> Option<u8> {
|
||||
if !self.initialized.get() || i < self.block_start.get() || i >= self.block_end.get() {
|
||||
let (bi, bs, be) = rope.lookup(i)?;
|
||||
self.block_idx.set(bi);
|
||||
self.block_start.set(bs);
|
||||
self.block_end.set(be);
|
||||
self.block.set(rope.get_block(bi)?);
|
||||
self.initialized.set(true);
|
||||
}
|
||||
Some(self.block.get()[i - self.block_start.get()].get())
|
||||
}
|
||||
|
||||
fn set(&self, rope: &'a Rope, i: usize, value: u8) -> Result<(), RopeError> {
|
||||
if !self.initialized.get() || i < self.block_start.get() || i >= self.block_end.get() {
|
||||
let (bi, bs, be) = rope.lookup(i).ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} > {}",
|
||||
i,
|
||||
rope.len()
|
||||
)))?;
|
||||
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.initialized.set(true);
|
||||
}
|
||||
self.block.get()[i - self.block_start.get()].set(value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── trait ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Common interface for all rope cursors.
|
||||
///
|
||||
/// # Required methods
|
||||
///
|
||||
/// Implementors must provide [`rope`](RopeCursor::rope),
|
||||
/// [`state`](RopeCursor::state), [`read_next`](RopeCursor::read_next) and
|
||||
/// [`seek`](RopeCursor::seek). Everything else has a default implementation.
|
||||
///
|
||||
/// The direction of `read_next` and the sign convention for
|
||||
/// [`SeekMode::Relative`] differ between [`ForwardCursor`] and
|
||||
/// [`BackwardCursor`]; all other methods are identical.
|
||||
pub trait RopeCursor<'a> {
|
||||
/// The rope this cursor is bound to.
|
||||
fn rope(&self) -> &'a Rope;
|
||||
/// Internal cache state — implementation detail exposed for default methods.
|
||||
fn state(&self) -> &CursorState<'a>;
|
||||
|
||||
/// Read the next byte in cursor direction and advance the position.
|
||||
/// Returns `Err` at the exhausted end.
|
||||
fn read_next(&self) -> Result<u8, RopeError>;
|
||||
|
||||
/// Move the cursor to a new position.
|
||||
///
|
||||
/// `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 **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().offset.get())
|
||||
}
|
||||
|
||||
/// 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 + self.state().offset.get(), value)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Unlike [`tell`](RopeCursor::tell), this method **always** returns a
|
||||
/// value: if the cursor has not moved yet, it returns the cursor's offset
|
||||
/// (the rope index of local position 0).
|
||||
///
|
||||
/// Use the returned value with [`SeekMode::Rope`] to restore a position,
|
||||
/// or as a truncation point after a write pass.
|
||||
fn rope_tell(&self) -> usize {
|
||||
self.state()
|
||||
.current
|
||||
.get()
|
||||
.unwrap_or(self.state().offset.get())
|
||||
}
|
||||
|
||||
/// Number of bytes visible through this cursor (`rope.len() - offset`).
|
||||
fn len(&self) -> usize {
|
||||
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.
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.state().get(self.rope(), self.state().current.get()?)
|
||||
}
|
||||
|
||||
/// Write `value` at the current position without advancing.
|
||||
fn poke(&self, value: u8) -> Result<(), RopeError> {
|
||||
let pos = self.state().current.get().ok_or(RopeError::CurrentNotSet)?;
|
||||
self.state().set(self.rope(), pos, value)
|
||||
}
|
||||
|
||||
/// Move backward by `go_back_of` steps (toward lower indices for
|
||||
/// [`ForwardCursor`], toward higher indices for [`BackwardCursor`]).
|
||||
fn rewind(&self, go_back_of: usize) -> Result<(), RopeError> {
|
||||
self.seek(-(go_back_of as isize), SeekMode::Relative)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move forward by `ahead` steps (opposite of [`rewind`](RopeCursor::rewind)).
|
||||
fn forward(&self, ahead: usize) -> Result<(), RopeError> {
|
||||
self.seek(ahead as isize, SeekMode::Relative)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── ForwardCursor ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// A cursor that reads from the start toward the end of the rope.
|
||||
///
|
||||
/// - `read_next`: first call reads index 0, then 1, 2, …
|
||||
/// - `seek(Relative, +n)`: advances by n.
|
||||
/// - `rewind(n)`: steps back by n.
|
||||
///
|
||||
/// Extra methods not in the trait: [`read_ahead`](ForwardCursor::read_ahead),
|
||||
/// [`write`](ForwardCursor::write), [`iter`](ForwardCursor::iter).
|
||||
#[derive(Clone)]
|
||||
pub struct ForwardCursor<'a> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the byte at `current + ahead` without moving the position.
|
||||
pub fn read_ahead(&self, ahead: usize) -> Result<u8, RopeError> {
|
||||
let pos = self.state.current.get().ok_or(RopeError::CurrentNotSet)?;
|
||||
self.state
|
||||
.get(self.rope, pos + ahead)
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} + {} > {}",
|
||||
pos,
|
||||
ahead,
|
||||
self.rope.len()
|
||||
)))
|
||||
}
|
||||
|
||||
/// Write `value` at the current position and advance by one.
|
||||
///
|
||||
/// 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(self.state.offset.get());
|
||||
self.state.set(self.rope, pos, value)?;
|
||||
self.state.current.set(Some(pos + 1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return a shared-borrow iterator that yields bytes forward.
|
||||
///
|
||||
/// Because the iterator holds `&self` rather than `&mut self`, methods
|
||||
/// such as [`tell`](RopeCursor::tell) and [`seek`](RopeCursor::seek) can
|
||||
/// be called on the cursor inside the loop body.
|
||||
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();
|
||||
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 read_next(&self) -> Result<u8, RopeError> {
|
||||
let next_pos = match self.state.current.get() {
|
||||
Some(i) => i + 1,
|
||||
None => self.state.offset.get(),
|
||||
};
|
||||
let value = self
|
||||
.state
|
||||
.get(self.rope, next_pos)
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} > {}",
|
||||
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 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 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared-borrow iterator returned by [`ForwardCursor::iter`].
|
||||
pub struct ForwardIter<'a, 'b> {
|
||||
cursor: &'b ForwardCursor<'a>,
|
||||
}
|
||||
|
||||
impl Iterator for ForwardIter<'_, '_> {
|
||||
type Item = u8;
|
||||
fn next(&mut self) -> Option<u8> {
|
||||
self.cursor.read_next().ok()
|
||||
}
|
||||
}
|
||||
|
||||
// ── BackwardCursor ────────────────────────────────────────────────────────────
|
||||
|
||||
/// A cursor that reads from the end toward the start of the rope.
|
||||
///
|
||||
/// - `read_next`: first call reads index `len-1`, then `len-2`, …
|
||||
/// - `seek(Relative, +n)`: retreats by n (subtracts n from the index).
|
||||
/// - `rewind(n)`: advances toward the end by n.
|
||||
///
|
||||
/// Extra methods not in the trait: [`read_behind`](BackwardCursor::read_behind),
|
||||
/// [`iter`](BackwardCursor::iter).
|
||||
#[derive(Clone)]
|
||||
pub struct BackwardCursor<'a> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the byte at `current + behind` (toward higher indices) without moving.
|
||||
pub fn read_behind(&self, behind: usize) -> Result<u8, RopeError> {
|
||||
let pos = self.state.current.get().ok_or(RopeError::CurrentNotSet)?;
|
||||
let target = pos
|
||||
.checked_add(behind)
|
||||
.filter(|&t| t < self.rope.len())
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} + {} > {}",
|
||||
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()
|
||||
)))
|
||||
}
|
||||
|
||||
/// Return a shared-borrow iterator that yields bytes backward.
|
||||
///
|
||||
/// Because the iterator holds `&self` rather than `&mut self`, methods
|
||||
/// such as [`tell`](RopeCursor::tell) and [`seek`](RopeCursor::seek) can
|
||||
/// be called on the cursor inside the loop body.
|
||||
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();
|
||||
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 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(i) if i <= offset => {
|
||||
return Err(RopeError::OutOfBounds(
|
||||
"BackwardCursor: already at beginning".to_string(),
|
||||
));
|
||||
}
|
||||
Some(i) => i - 1,
|
||||
};
|
||||
let value = self
|
||||
.state
|
||||
.get(self.rope, next_pos)
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"BackwardCursor: index out of bounds at i={}",
|
||||
next_pos
|
||||
)))?;
|
||||
self.state.current.set(Some(next_pos));
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn seek(&self, pos: isize, mode: SeekMode) -> Result<usize, RopeError> {
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared-borrow iterator returned by [`BackwardCursor::iter`].
|
||||
pub struct BackwardIter<'a, 'b> {
|
||||
cursor: &'b BackwardCursor<'a>,
|
||||
}
|
||||
|
||||
impl Iterator for BackwardIter<'_, '_> {
|
||||
type Item = u8;
|
||||
fn next(&mut self) -> Option<u8> {
|
||||
self.cursor.read_next().ok()
|
||||
}
|
||||
}
|
||||
|
||||
// ── tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Rope;
|
||||
|
||||
fn rope(data: &[u8]) -> Rope {
|
||||
let mut r = Rope::new(None);
|
||||
r.push(data.to_vec());
|
||||
r
|
||||
}
|
||||
|
||||
fn rope2(a: &[u8], b: &[u8]) -> Rope {
|
||||
let mut r = Rope::new(None);
|
||||
r.push(a.to_vec());
|
||||
r.push(b.to_vec());
|
||||
r
|
||||
}
|
||||
|
||||
// ── ForwardCursor ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn forward_reads_all_bytes() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"ACGT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_tell_tracks_position() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
assert_eq!(c.tell(), None);
|
||||
c.read_next().unwrap();
|
||||
assert_eq!(c.tell(), Some(0));
|
||||
c.read_next().unwrap();
|
||||
assert_eq!(c.tell(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_iter_with_tell_inside_loop() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
let mut positions = Vec::new();
|
||||
for _ in c.iter() {
|
||||
positions.push(c.tell());
|
||||
}
|
||||
assert_eq!(positions, vec![Some(0), Some(1), Some(2), Some(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_read_ahead() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
c.read_next().unwrap(); // at 0 = 'A'
|
||||
assert_eq!(c.read_ahead(1).unwrap(), b'C');
|
||||
assert_eq!(c.read_ahead(2).unwrap(), b'G');
|
||||
assert_eq!(c.tell(), Some(0)); // position unchanged
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_write_and_read_back() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
c.write(b'X').unwrap();
|
||||
c.write(b'Y').unwrap();
|
||||
let c2 = r.fw_cursor();
|
||||
assert_eq!(c2.read_next().unwrap(), b'X');
|
||||
assert_eq!(c2.read_next().unwrap(), b'Y');
|
||||
assert_eq!(c2.read_next().unwrap(), b'G');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_rewind_and_reread() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
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
|
||||
assert_eq!(c.read_next().unwrap(), b'G');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_seek_absolute() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
c.seek(2, SeekMode::Absolute).unwrap();
|
||||
assert_eq!(c.read_next().unwrap(), b'T');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_seek_relative_to_end() {
|
||||
let r = rope(b"ACGT");
|
||||
// seek(1, RelativeToEnd): current = len-1 = 3; peek() reads index 3 = T.
|
||||
let c = r.fw_cursor();
|
||||
c.seek(1, SeekMode::RelativeToEnd).unwrap();
|
||||
assert_eq!(c.peek().unwrap(), b'T');
|
||||
// seek(2, RelativeToEnd): current = len-2 = 2; read_next reads index 3 = T.
|
||||
let c2 = r.fw_cursor();
|
||||
c2.seek(2, SeekMode::RelativeToEnd).unwrap();
|
||||
assert_eq!(c2.read_next().unwrap(), b'T');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_get_random_access() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
assert_eq!(c.get(0), Some(b'A'));
|
||||
assert_eq!(c.get(3), Some(b'T'));
|
||||
assert_eq!(c.get(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_crosses_block_boundary() {
|
||||
let r = rope2(b"AC", b"GT");
|
||||
let c = r.fw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"ACGT");
|
||||
}
|
||||
|
||||
// ── BackwardCursor ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn backward_reads_all_bytes_in_reverse() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.bw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"TGCA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_tell_tracks_position() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.bw_cursor();
|
||||
assert_eq!(c.tell(), None);
|
||||
c.read_next().unwrap(); // reads index 3
|
||||
assert_eq!(c.tell(), Some(3));
|
||||
c.read_next().unwrap(); // reads index 2
|
||||
assert_eq!(c.tell(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_iter_with_tell_and_seek_inside_loop() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.bw_cursor();
|
||||
let mut restart: usize = 0;
|
||||
for byte in c.iter() {
|
||||
if byte == b'G' {
|
||||
restart = c.tell().unwrap();
|
||||
}
|
||||
if byte == b'A' {
|
||||
// seek back to G and break
|
||||
c.seek(restart as isize, SeekMode::Absolute).ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(c.tell(), Some(restart));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_rewind_moves_toward_end() {
|
||||
let r = rope(b"ACGT");
|
||||
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
|
||||
assert_eq!(c.tell(), Some(3));
|
||||
assert_eq!(c.read_next().unwrap(), b'G'); // reads index 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_crosses_block_boundary() {
|
||||
let r = rope2(b"AC", b"GT");
|
||||
let c = r.bw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"TGCA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_empty_rope_returns_error() {
|
||||
let r = Rope::new(None);
|
||||
let c = r.bw_cursor();
|
||||
assert!(c.read_next().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_empty_rope_returns_error() {
|
||||
let r = Rope::new(None);
|
||||
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(), 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(), 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(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::{Rope, RopeError};
|
||||
|
||||
use super::state::CursorState;
|
||||
use super::traits::{RopeCursor, SeekMode};
|
||||
|
||||
/// A cursor that reads from the end toward the start of the rope.
|
||||
///
|
||||
/// - `read_next`: first call reads index `len-1`, then `len-2`, …
|
||||
/// - `seek(Relative, +n)`: retreats by n (subtracts n from the index).
|
||||
/// - `rewind(n)`: advances toward the end by n.
|
||||
///
|
||||
/// Extra methods not in the trait: [`read_behind`](BackwardCursor::read_behind),
|
||||
/// [`iter`](BackwardCursor::iter).
|
||||
#[derive(Clone)]
|
||||
pub struct BackwardCursor<'a> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the byte at `current + behind` (toward higher indices) without moving.
|
||||
pub fn read_behind(&self, behind: usize) -> Result<u8, RopeError> {
|
||||
let pos = self.state.current.get().ok_or(RopeError::CurrentNotSet)?;
|
||||
let target = pos
|
||||
.checked_add(behind)
|
||||
.filter(|&t| t < self.rope.len())
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} + {} > {}",
|
||||
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()
|
||||
)))
|
||||
}
|
||||
|
||||
/// Return a shared-borrow iterator that yields bytes backward.
|
||||
///
|
||||
/// Because the iterator holds `&self` rather than `&mut self`, methods
|
||||
/// such as [`tell`](RopeCursor::tell) and [`seek`](RopeCursor::seek) can
|
||||
/// be called on the cursor inside the loop body.
|
||||
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();
|
||||
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 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(i) if i <= offset => {
|
||||
return Err(RopeError::OutOfBounds(
|
||||
"BackwardCursor: already at beginning".to_string(),
|
||||
));
|
||||
}
|
||||
Some(i) => i - 1,
|
||||
};
|
||||
let value = self
|
||||
.state
|
||||
.get(self.rope, next_pos)
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"BackwardCursor: index out of bounds at i={}",
|
||||
next_pos
|
||||
)))?;
|
||||
self.state.current.set(Some(next_pos));
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn seek(&self, pos: isize, mode: SeekMode) -> Result<usize, RopeError> {
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared-borrow iterator returned by [`BackwardCursor::iter`].
|
||||
pub struct BackwardIter<'a, 'b> {
|
||||
cursor: &'b BackwardCursor<'a>,
|
||||
}
|
||||
|
||||
impl Iterator for BackwardIter<'_, '_> {
|
||||
type Item = u8;
|
||||
fn next(&mut self) -> Option<u8> {
|
||||
self.cursor.read_next().ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use crate::{Rope, RopeError};
|
||||
|
||||
use super::state::CursorState;
|
||||
use super::traits::{RopeCursor, SeekMode};
|
||||
|
||||
/// A cursor that reads from the start toward the end of the rope.
|
||||
///
|
||||
/// - `read_next`: first call reads index 0, then 1, 2, …
|
||||
/// - `seek(Relative, +n)`: advances by n.
|
||||
/// - `rewind(n)`: steps back by n.
|
||||
///
|
||||
/// Extra methods not in the trait: [`read_ahead`](ForwardCursor::read_ahead),
|
||||
/// [`write`](ForwardCursor::write), [`iter`](ForwardCursor::iter).
|
||||
#[derive(Clone)]
|
||||
pub struct ForwardCursor<'a> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the byte at `current + ahead` without moving the position.
|
||||
pub fn read_ahead(&self, ahead: usize) -> Result<u8, RopeError> {
|
||||
let pos = self.state.current.get().ok_or(RopeError::CurrentNotSet)?;
|
||||
self.state
|
||||
.get(self.rope, pos + ahead)
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} + {} > {}",
|
||||
pos,
|
||||
ahead,
|
||||
self.rope.len()
|
||||
)))
|
||||
}
|
||||
|
||||
/// Write `value` at the current position and advance by one.
|
||||
///
|
||||
/// 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(self.state.offset.get());
|
||||
self.state.set(self.rope, pos, value)?;
|
||||
self.state.current.set(Some(pos + 1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return a shared-borrow iterator that yields bytes forward.
|
||||
///
|
||||
/// Because the iterator holds `&self` rather than `&mut self`, methods
|
||||
/// such as [`tell`](RopeCursor::tell) and [`seek`](RopeCursor::seek) can
|
||||
/// be called on the cursor inside the loop body.
|
||||
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`](std::cell::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();
|
||||
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 read_next(&self) -> Result<u8, RopeError> {
|
||||
let next_pos = match self.state.current.get() {
|
||||
Some(i) => i + 1,
|
||||
None => self.state.offset.get(),
|
||||
};
|
||||
let value = self
|
||||
.state
|
||||
.get(self.rope, next_pos)
|
||||
.ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} > {}",
|
||||
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 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 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared-borrow iterator returned by [`ForwardCursor::iter`].
|
||||
pub struct ForwardIter<'a, 'b> {
|
||||
cursor: &'b ForwardCursor<'a>,
|
||||
}
|
||||
|
||||
impl Iterator for ForwardIter<'_, '_> {
|
||||
type Item = u8;
|
||||
fn next(&mut self) -> Option<u8> {
|
||||
self.cursor.read_next().ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Cursors for sequential and random access over a [`Rope`].
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
//! A cursor borrows a `&'a Rope` and keeps a small block cache so that
|
||||
//! consecutive accesses within the same block cost O(1). The first access to a
|
||||
//! new block costs O(log n) (binary search in [`Rope::lookup`]); subsequent
|
||||
//! accesses within that block are free.
|
||||
//!
|
||||
//! All mutable state (current position, cache) is stored in [`Cell`](std::cell::Cell)
|
||||
//! fields, so every cursor method takes `&self` rather than `&mut self`. This means:
|
||||
//!
|
||||
//! - Two cursors can coexist on the same rope without lifetime conflicts.
|
||||
//! - The `iter()` method returns a lightweight wrapper that holds `&Cursor`,
|
||||
//! allowing `cursor.tell()` or `cursor.seek()` to be called **inside a `for`
|
||||
//! loop** over the same cursor.
|
||||
//!
|
||||
//! # Cursors
|
||||
//!
|
||||
//! | Type | Direction | First `read_next` | `seek(Relative, +n)` |
|
||||
//! |------|-----------|-------------------|----------------------|
|
||||
//! | [`ForwardCursor`] | start → end | index 0 | advances (+n) |
|
||||
//! | [`BackwardCursor`] | end → start | index `len-1` | retreats (+n) |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use obikrope::{Rope, RopeCursor};
|
||||
//!
|
||||
//! let mut rope = Rope::new(None);
|
||||
//! rope.push(b"ACGT".to_vec());
|
||||
//!
|
||||
//! let cursor = rope.fw_cursor();
|
||||
//! for byte in cursor.iter() {
|
||||
//! // cursor.tell() is valid here — iter() holds &cursor, not &mut cursor
|
||||
//! let _ = cursor.tell();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Submodules: [`state`] (shared block-cache state), [`traits`] (`SeekMode`,
|
||||
//! `RopeCursor`), [`forward`]/[`backward`] (the two cursor implementations).
|
||||
|
||||
mod backward;
|
||||
mod forward;
|
||||
mod state;
|
||||
mod traits;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use backward::BackwardCursor;
|
||||
pub use forward::ForwardCursor;
|
||||
pub use traits::{RopeCursor, SeekMode};
|
||||
@@ -0,0 +1,72 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::{Rope, RopeError};
|
||||
|
||||
/// 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_start: Cell<usize>,
|
||||
block_end: Cell<usize>,
|
||||
block: Cell<&'a [Cell<u8>]>,
|
||||
initialized: Cell<bool>,
|
||||
pub(super) current: Cell<Option<usize>>,
|
||||
/// Absolute rope index that maps to local position 0.
|
||||
/// All user-facing coordinates are relative to this value.
|
||||
pub(super) offset: Cell<usize>,
|
||||
}
|
||||
|
||||
impl<'a> CursorState<'a> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::with_offset(0)
|
||||
}
|
||||
|
||||
pub(super) fn with_offset(offset: usize) -> Self {
|
||||
Self {
|
||||
block_idx: Cell::new(0),
|
||||
block_start: Cell::new(0),
|
||||
block_end: Cell::new(0),
|
||||
block: Cell::new(&[]),
|
||||
initialized: Cell::new(false),
|
||||
current: Cell::new(None),
|
||||
offset: Cell::new(offset),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get(&self, rope: &'a Rope, i: usize) -> Option<u8> {
|
||||
if !self.initialized.get() || i < self.block_start.get() || i >= self.block_end.get() {
|
||||
let (bi, bs, be) = rope.lookup(i)?;
|
||||
self.block_idx.set(bi);
|
||||
self.block_start.set(bs);
|
||||
self.block_end.set(be);
|
||||
self.block.set(rope.get_block(bi)?);
|
||||
self.initialized.set(true);
|
||||
}
|
||||
Some(self.block.get()[i - self.block_start.get()].get())
|
||||
}
|
||||
|
||||
pub(super) fn set(&self, rope: &'a Rope, i: usize, value: u8) -> Result<(), RopeError> {
|
||||
if !self.initialized.get() || i < self.block_start.get() || i >= self.block_end.get() {
|
||||
let (bi, bs, be) = rope.lookup(i).ok_or(RopeError::OutOfBounds(format!(
|
||||
"index out of bounds: i={} > {}",
|
||||
i,
|
||||
rope.len()
|
||||
)))?;
|
||||
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.initialized.set(true);
|
||||
}
|
||||
self.block.get()[i - self.block_start.get()].set(value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
use super::*;
|
||||
use crate::Rope;
|
||||
|
||||
fn rope(data: &[u8]) -> Rope {
|
||||
let mut r = Rope::new(None);
|
||||
r.push(data.to_vec());
|
||||
r
|
||||
}
|
||||
|
||||
fn rope2(a: &[u8], b: &[u8]) -> Rope {
|
||||
let mut r = Rope::new(None);
|
||||
r.push(a.to_vec());
|
||||
r.push(b.to_vec());
|
||||
r
|
||||
}
|
||||
|
||||
// ── ForwardCursor ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn forward_reads_all_bytes() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"ACGT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_tell_tracks_position() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
assert_eq!(c.tell(), None);
|
||||
c.read_next().unwrap();
|
||||
assert_eq!(c.tell(), Some(0));
|
||||
c.read_next().unwrap();
|
||||
assert_eq!(c.tell(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_iter_with_tell_inside_loop() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
let mut positions = Vec::new();
|
||||
for _ in c.iter() {
|
||||
positions.push(c.tell());
|
||||
}
|
||||
assert_eq!(positions, vec![Some(0), Some(1), Some(2), Some(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_read_ahead() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
c.read_next().unwrap(); // at 0 = 'A'
|
||||
assert_eq!(c.read_ahead(1).unwrap(), b'C');
|
||||
assert_eq!(c.read_ahead(2).unwrap(), b'G');
|
||||
assert_eq!(c.tell(), Some(0)); // position unchanged
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_write_and_read_back() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
c.write(b'X').unwrap();
|
||||
c.write(b'Y').unwrap();
|
||||
let c2 = r.fw_cursor();
|
||||
assert_eq!(c2.read_next().unwrap(), b'X');
|
||||
assert_eq!(c2.read_next().unwrap(), b'Y');
|
||||
assert_eq!(c2.read_next().unwrap(), b'G');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_rewind_and_reread() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
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
|
||||
assert_eq!(c.read_next().unwrap(), b'G');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_seek_absolute() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
c.seek(2, SeekMode::Absolute).unwrap();
|
||||
assert_eq!(c.read_next().unwrap(), b'T');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_seek_relative_to_end() {
|
||||
let r = rope(b"ACGT");
|
||||
// seek(1, RelativeToEnd): current = len-1 = 3; peek() reads index 3 = T.
|
||||
let c = r.fw_cursor();
|
||||
c.seek(1, SeekMode::RelativeToEnd).unwrap();
|
||||
assert_eq!(c.peek().unwrap(), b'T');
|
||||
// seek(2, RelativeToEnd): current = len-2 = 2; read_next reads index 3 = T.
|
||||
let c2 = r.fw_cursor();
|
||||
c2.seek(2, SeekMode::RelativeToEnd).unwrap();
|
||||
assert_eq!(c2.read_next().unwrap(), b'T');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_get_random_access() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.fw_cursor();
|
||||
assert_eq!(c.get(0), Some(b'A'));
|
||||
assert_eq!(c.get(3), Some(b'T'));
|
||||
assert_eq!(c.get(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_crosses_block_boundary() {
|
||||
let r = rope2(b"AC", b"GT");
|
||||
let c = r.fw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"ACGT");
|
||||
}
|
||||
|
||||
// ── BackwardCursor ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn backward_reads_all_bytes_in_reverse() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.bw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"TGCA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_tell_tracks_position() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.bw_cursor();
|
||||
assert_eq!(c.tell(), None);
|
||||
c.read_next().unwrap(); // reads index 3
|
||||
assert_eq!(c.tell(), Some(3));
|
||||
c.read_next().unwrap(); // reads index 2
|
||||
assert_eq!(c.tell(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_iter_with_tell_and_seek_inside_loop() {
|
||||
let r = rope(b"ACGT");
|
||||
let c = r.bw_cursor();
|
||||
let mut restart: usize = 0;
|
||||
for byte in c.iter() {
|
||||
if byte == b'G' {
|
||||
restart = c.tell().unwrap();
|
||||
}
|
||||
if byte == b'A' {
|
||||
// seek back to G and break
|
||||
c.seek(restart as isize, SeekMode::Absolute).ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(c.tell(), Some(restart));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_rewind_moves_toward_end() {
|
||||
let r = rope(b"ACGT");
|
||||
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
|
||||
assert_eq!(c.tell(), Some(3));
|
||||
assert_eq!(c.read_next().unwrap(), b'G'); // reads index 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_crosses_block_boundary() {
|
||||
let r = rope2(b"AC", b"GT");
|
||||
let c = r.bw_cursor();
|
||||
let out: Vec<u8> = c.collect();
|
||||
assert_eq!(out, b"TGCA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_empty_rope_returns_error() {
|
||||
let r = Rope::new(None);
|
||||
let c = r.bw_cursor();
|
||||
assert!(c.read_next().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_empty_rope_returns_error() {
|
||||
let r = Rope::new(None);
|
||||
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(), 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(), 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(), 1);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::{Rope, RopeError};
|
||||
|
||||
use super::state::CursorState;
|
||||
|
||||
/// Controls how the `pos` argument of [`RopeCursor::seek`] is interpreted.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SeekMode {
|
||||
/// `pos` is an absolute byte index from the start of the rope.
|
||||
Absolute,
|
||||
/// `pos` is relative to the current position.
|
||||
/// Positive = forward for [`ForwardCursor`](super::ForwardCursor), backward for [`BackwardCursor`](super::BackwardCursor).
|
||||
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,
|
||||
}
|
||||
|
||||
/// Common interface for all rope cursors.
|
||||
///
|
||||
/// # Required methods
|
||||
///
|
||||
/// Implementors must provide [`rope`](RopeCursor::rope),
|
||||
/// [`state`](RopeCursor::state), [`read_next`](RopeCursor::read_next) and
|
||||
/// [`seek`](RopeCursor::seek). Everything else has a default implementation.
|
||||
///
|
||||
/// The direction of `read_next` and the sign convention for
|
||||
/// [`SeekMode::Relative`] differ between [`ForwardCursor`](super::ForwardCursor)
|
||||
/// and [`BackwardCursor`](super::BackwardCursor); all other methods are identical.
|
||||
pub trait RopeCursor<'a> {
|
||||
/// The rope this cursor is bound to.
|
||||
fn rope(&self) -> &'a Rope;
|
||||
/// Internal cache state — implementation detail exposed for default methods.
|
||||
fn state(&self) -> &CursorState<'a>;
|
||||
|
||||
/// Read the next byte in cursor direction and advance the position.
|
||||
/// Returns `Err` at the exhausted end.
|
||||
fn read_next(&self) -> Result<u8, RopeError>;
|
||||
|
||||
/// Move the cursor to a new position.
|
||||
///
|
||||
/// `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`](super::ForwardCursor), positive advances toward the end;
|
||||
/// for [`BackwardCursor`](super::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 **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().offset.get())
|
||||
}
|
||||
|
||||
/// 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 + self.state().offset.get(), value)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Unlike [`tell`](RopeCursor::tell), this method **always** returns a
|
||||
/// value: if the cursor has not moved yet, it returns the cursor's offset
|
||||
/// (the rope index of local position 0).
|
||||
///
|
||||
/// Use the returned value with [`SeekMode::Rope`] to restore a position,
|
||||
/// or as a truncation point after a write pass.
|
||||
fn rope_tell(&self) -> usize {
|
||||
self.state()
|
||||
.current
|
||||
.get()
|
||||
.unwrap_or(self.state().offset.get())
|
||||
}
|
||||
|
||||
/// Number of bytes visible through this cursor (`rope.len() - offset`).
|
||||
fn len(&self) -> usize {
|
||||
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.
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.state().get(self.rope(), self.state().current.get()?)
|
||||
}
|
||||
|
||||
/// Write `value` at the current position without advancing.
|
||||
fn poke(&self, value: u8) -> Result<(), RopeError> {
|
||||
let pos = self.state().current.get().ok_or(RopeError::CurrentNotSet)?;
|
||||
self.state().set(self.rope(), pos, value)
|
||||
}
|
||||
|
||||
/// Move backward by `go_back_of` steps (toward lower indices for
|
||||
/// [`ForwardCursor`](super::ForwardCursor), toward higher indices for
|
||||
/// [`BackwardCursor`](super::BackwardCursor)).
|
||||
fn rewind(&self, go_back_of: usize) -> Result<(), RopeError> {
|
||||
self.seek(-(go_back_of as isize), SeekMode::Relative)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move forward by `ahead` steps (opposite of [`rewind`](RopeCursor::rewind)).
|
||||
fn forward(&self, ahead: usize) -> Result<(), RopeError> {
|
||||
self.seek(ahead as isize, SeekMode::Relative)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user