🔧 refactor cursor API and normalize chunking logic

- Change `rope_tell()` return type from Option<usize> to usize, always returning cursor's absolute position (offset if unmoved).
- Update all call sites to remove `.unwrap_or(...)` around `rope_tell()`.
- Add new method `<Rope>::truncate(pos)`, replacing `split_off(...).map(|_| ())`.
- Refactor FASTA/FASTQ normalizers to use a single mutable write cursor (`wc`) and document the protocol.
- Simplify `end_segment()` logic: commit segment with 0x00 if length ≥ k, else reset.
- Improve documentation for write-cursor protocol and rope truncation semantics.
This commit is contained in:
Eric Coissac
2026-04-19 22:47:35 +02:00
parent 3716eeff7f
commit 097f7f0695
3 changed files with 122 additions and 59 deletions
+15 -10
View File
@@ -185,11 +185,16 @@ pub trait RopeCursor<'a> {
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()
/// 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`).
@@ -295,7 +300,7 @@ impl<'a> ForwardCursor<'a> {
/// 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());
let new_offset = self.rope_tell();
ForwardCursor {
rope: self.rope,
state: CursorState::with_offset(new_offset),
@@ -431,7 +436,7 @@ impl<'a> BackwardCursor<'a> {
/// 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());
let new_offset = self.rope_tell();
BackwardCursor {
rope: self.rope,
state: CursorState::with_offset(new_offset),
@@ -723,7 +728,7 @@ mod tests {
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.rope_tell(), 2);
assert_eq!(sub.read_next().unwrap(), b'D');
assert_eq!(sub.tell(), Some(1)); // relative: 3-2=1
}
@@ -810,11 +815,11 @@ mod tests {
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));
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(), Some(1));
assert_eq!(sub2.rope_tell(), 1);
}
}
+8
View File
@@ -155,6 +155,14 @@ impl Rope {
})
}
/// Discard all bytes at and after `pos`, keeping `[0, pos)`.
///
/// Equivalent to `split_off(pos)` with the tail dropped.
/// Returns `Err` if `pos > self.length`.
pub fn truncate(&mut self, pos: usize) -> Result<(), RopeError> {
self.split_off(pos).map(|_| ())
}
/// Create a forward cursor positioned before the first byte.
pub fn fw_cursor(&self) -> ForwardCursor<'_> {
ForwardCursor::new(self)