use std::ops::Range;
#[derive(Debug, Clone, Copy)]
struct Line {
start: u32,
end: u32,
}
#[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)
}
}
#[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); }
}