use crate::pos::{Segment, Span};
#[derive(Clone, Copy)]
pub struct Cursor<'s> {
line: &'s str,
line_start: u32,
trimmed: usize,
pub pos: usize,
pub col: usize,
pub partial: u8,
}
impl<'s> Cursor<'s> {
pub fn new(line: &'s str, line_start: u32) -> Self {
Self {
line,
line_start,
trimmed: line.trim_end_matches([' ', '\t']).len(),
pos: 0,
col: 0,
partial: 0,
}
}
pub fn is_blank(&self) -> bool {
self.pos >= self.trimmed
}
pub fn tail(&self) -> &'s str {
&self.line[self.pos..]
}
#[expect(clippy::cast_possible_truncation)] pub fn offset(&self) -> u32 {
self.line_start + self.pos as u32
}
pub fn trimmed_span(&self) -> Span {
Span::at(self.offset(), 0..self.trimmed.saturating_sub(self.pos))
}
pub fn segment(&self, line_content_end: u32) -> Segment {
Segment::new(Span::new(self.offset().min(line_content_end), line_content_end), self.partial)
}
pub fn indent_cols(&self) -> usize {
let mut cols = usize::from(self.partial);
let mut col = self.col + cols;
for b in self.line[self.pos..].bytes() {
match b {
b' ' => {
cols += 1;
col += 1;
}
b'\t' => {
let width = 4 - col % 4;
cols += width;
col += width;
}
_ => break,
}
}
cols
}
#[expect(clippy::cast_possible_truncation)] pub fn advance_cols(&mut self, mut n: usize) {
while n > 0 {
if self.partial > 0 {
let take = usize::from(self.partial).min(n);
self.partial -= take as u8;
self.col += take;
n -= take;
continue;
}
match self.line.as_bytes().get(self.pos) {
Some(b' ') => {
self.pos += 1;
self.col += 1;
n -= 1;
}
Some(b'\t') => {
let width = 4 - self.col % 4;
self.pos += 1;
if width > n {
self.partial = (width - n) as u8;
self.col += n;
n = 0;
} else {
self.col += width;
n -= width;
}
}
_ => break,
}
}
}
pub fn skip_indent_up_to(&mut self, max: usize) {
let available = self.indent_cols().min(max);
self.advance_cols(available);
}
pub fn flow_start(mut self) -> Option<Self> {
if self.indent_cols() >= 4 {
return None;
}
self.skip_indent_up_to(3);
(self.partial == 0).then_some(self)
}
pub fn skip_all_ws(&mut self) {
let available = self.indent_cols();
self.advance_cols(available);
self.partial = 0;
}
pub fn bump(&mut self, n: usize) {
debug_assert_eq!(self.partial, 0);
self.pos += n;
self.col += n;
}
pub fn eat_one_space(&mut self) {
if self.partial > 0 || matches!(self.line.as_bytes().get(self.pos), Some(b' ' | b'\t')) {
self.advance_cols(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tab_columns() {
let cur = Cursor::new("\tx", 0);
assert_eq!(cur.indent_cols(), 4);
let cur = Cursor::new(" \tx", 0);
assert_eq!(cur.indent_cols(), 4);
let cur = Cursor::new(" \tx", 0);
assert_eq!(cur.indent_cols(), 8);
}
#[test]
fn partial_tab() {
let mut cur = Cursor::new("\tx", 0);
cur.advance_cols(1);
assert_eq!((cur.pos, cur.col, cur.partial), (1, 1, 3));
assert_eq!(cur.indent_cols(), 3);
assert_eq!(cur.segment(2), Segment::new(Span::new(1, 2), 3));
cur.advance_cols(3);
assert_eq!((cur.pos, cur.col, cur.partial), (1, 4, 0));
assert_eq!(cur.tail(), "x");
}
#[test]
fn flow_start() {
assert_eq!(Cursor::new(" x", 0).flow_start().map(|c| c.pos), Some(3));
assert!(Cursor::new(" x", 0).flow_start().is_none());
assert!(Cursor::new("\tx", 0).flow_start().is_none());
}
#[test]
fn spans_and_offsets() {
let mut cur = Cursor::new(" # h ", 100);
cur.skip_indent_up_to(3);
assert_eq!(cur.offset(), 102);
assert_eq!(cur.trimmed_span(), Span::new(102, 105));
cur.eat_one_space();
assert_eq!(cur.pos, 2, "eat_one_space only eats whitespace");
cur.bump(1);
cur.eat_one_space();
assert_eq!(cur.tail(), "h ");
}
}