use std::ops::Range;
#[derive(Debug, Clone, Copy)]
struct Line {
start: u32,
end: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Terminator {
Lf,
CrLf,
None,
}
#[derive(Debug, Clone)]
pub struct LineIndex {
lines: Vec<Line>,
len: u32,
}
impl LineIndex {
pub fn new(source: &str) -> Self {
let bytes = source.as_bytes();
let mut lines = Vec::new();
let mut start = 0u32;
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
let mut end = i as u32;
if end > start && bytes[i - 1] == b'\r' {
end -= 1;
}
lines.push(Line { start, end });
start = i as u32 + 1;
}
}
lines.push(Line {
start,
end: bytes.len() as u32,
});
LineIndex {
lines,
len: bytes.len() as u32,
}
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn offset_to_line_col(&self, offset: u32) -> (u32, u32) {
let offset = offset.min(self.len);
let line = self.lines.partition_point(|l| l.start <= offset).max(1);
let start = self.lines[line - 1].start;
(line as u32, offset - start + 1)
}
pub fn line_col_to_offset(&self, line: u32, col: u32) -> u32 {
let idx = (line.max(1) as usize).min(self.lines.len()) - 1;
let line = self.lines[idx];
(line.start + col.saturating_sub(1)).min(line.end)
}
pub fn line_range(&self, n: u32) -> Option<Range<usize>> {
let idx = (n as usize).checked_sub(1)?;
let line = self.lines.get(idx)?;
Some(line.start as usize..line.end as usize)
}
pub fn line_full_range(&self, n: u32) -> Option<Range<usize>> {
let idx = (n as usize).checked_sub(1)?;
let start = self.lines.get(idx)?.start;
let end = self.lines.get(idx + 1).map_or(self.len, |next| next.start);
Some(start as usize..end as usize)
}
pub fn line_terminator(&self, n: u32) -> Option<Terminator> {
let idx = (n as usize).checked_sub(1)?;
let line = *self.lines.get(idx)?;
let full_end = self.lines.get(idx + 1).map_or(self.len, |next| next.start);
Some(match full_end - line.end {
0 => Terminator::None,
1 => Terminator::Lf,
_ => Terminator::CrLf,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_source_has_one_line() {
let idx = LineIndex::new("");
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.offset_to_line_col(0), (1, 1));
assert_eq!(idx.line_range(1), Some(0..0));
}
#[test]
fn lf_breaks() {
let idx = LineIndex::new("ab\ncd");
assert_eq!(idx.line_count(), 2);
assert_eq!(idx.offset_to_line_col(0), (1, 1)); assert_eq!(idx.offset_to_line_col(1), (1, 2)); assert_eq!(idx.offset_to_line_col(2), (1, 3)); assert_eq!(idx.offset_to_line_col(3), (2, 1)); assert_eq!(idx.offset_to_line_col(4), (2, 2)); assert_eq!(idx.line_range(1), Some(0..2)); assert_eq!(idx.line_range(2), Some(3..5)); assert_eq!(idx.line_range(3), None);
}
#[test]
fn crlf_terminator_excluded() {
let idx = LineIndex::new("ab\r\ncd");
assert_eq!(idx.line_count(), 2);
assert_eq!(idx.line_range(1), Some(0..2)); assert_eq!(idx.line_range(2), Some(4..6)); assert_eq!(idx.offset_to_line_col(2), (1, 3)); assert_eq!(idx.offset_to_line_col(4), (2, 1)); }
#[test]
fn lone_cr_is_not_a_break() {
let idx = LineIndex::new("a\rb");
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.line_range(1), Some(0..3)); }
#[test]
fn trailing_newline_yields_final_line() {
let idx = LineIndex::new("ab\n");
assert_eq!(idx.line_count(), 2);
assert_eq!(idx.offset_to_line_col(3), (2, 1)); assert_eq!(idx.line_range(2), Some(3..3)); }
#[test]
fn offset_past_end_clamps() {
let idx = LineIndex::new("ab");
assert_eq!(idx.offset_to_line_col(99), (1, 3));
}
#[test]
fn line_col_round_trips() {
let src = "one\ntwo\r\nthree";
let idx = LineIndex::new(src);
for off in 0..=src.len() as u32 {
let (l, c) = idx.offset_to_line_col(off);
if let Some(range) = idx.line_range(l) {
if (off as usize) <= range.end {
assert_eq!(idx.line_col_to_offset(l, c), off, "offset {off}");
}
}
}
}
#[test]
fn line_col_to_offset_clamps() {
let idx = LineIndex::new("ab\ncd");
assert_eq!(idx.line_col_to_offset(1, 1), 0);
assert_eq!(idx.line_col_to_offset(2, 1), 3);
assert_eq!(idx.line_col_to_offset(99, 99), 5); assert_eq!(idx.line_col_to_offset(0, 0), 0); }
#[test]
fn col_overflow_stays_on_its_line() {
let idx = LineIndex::new("ab\ncd");
let off = idx.line_col_to_offset(1, 10);
assert_eq!(off, 2); assert_eq!(idx.offset_to_line_col(off).0, 1);
let idx = LineIndex::new("ab\r\ncd");
let off = idx.line_col_to_offset(1, 10);
assert_eq!(off, 2); }
#[test]
fn full_ranges_tile_the_source() {
for src in [
"ab\r\ncd",
"one\ntwo\r\nthree",
"x\n",
"",
"no newline",
"\n\n",
] {
let idx = LineIndex::new(src);
let mut rebuilt = String::new();
let mut prev_end = 0;
for n in 1..=idx.line_count() as u32 {
let r = idx.line_full_range(n).unwrap();
assert_eq!(
r.start, prev_end,
"full ranges must be contiguous in {src:?}"
);
rebuilt.push_str(&src[r.clone()]);
prev_end = r.end;
}
assert_eq!(
prev_end,
src.len(),
"full ranges must cover to EOF in {src:?}"
);
assert_eq!(rebuilt, src, "full ranges must reconstruct {src:?}");
}
}
#[test]
fn line_terminator_kinds() {
let idx = LineIndex::new("ab\r\ncd\ne");
assert_eq!(idx.line_terminator(1), Some(Terminator::CrLf)); assert_eq!(idx.line_terminator(2), Some(Terminator::Lf)); assert_eq!(idx.line_terminator(3), Some(Terminator::None)); assert_eq!(idx.line_terminator(4), None);
let idx = LineIndex::new("x\n");
assert_eq!(idx.line_terminator(1), Some(Terminator::Lf));
}
#[test]
fn full_range_includes_terminator_content_excludes_it() {
let idx = LineIndex::new("ab\r\ncd");
assert_eq!(idx.line_range(1), Some(0..2)); assert_eq!(idx.line_full_range(1), Some(0..4)); assert_eq!(idx.line_full_range(3), None); }
}