use alloc::boxed::Box;
use alloc::vec::Vec;
use span_lang::{BytePos, LineCol};
pub(crate) fn line_starts(text: &str) -> Box<[u32]> {
let bytes = text.as_bytes();
let breaks = bytes.iter().filter(|&&b| b == b'\n').count();
let mut starts = Vec::with_capacity(breaks + 1);
starts.push(0u32);
for (i, &byte) in bytes.iter().enumerate() {
if byte == b'\n' {
starts.push(i as u32 + 1);
}
}
starts.into_boxed_slice()
}
pub(crate) fn line_col(text: &str, starts: &[u32], local: BytePos) -> LineCol {
let mut at = local.to_usize().min(text.len());
while at > 0 && !text.is_char_boundary(at) {
at -= 1;
}
let line_idx = starts.partition_point(|&start| start as usize <= at).max(1) - 1;
let line_start = starts.get(line_idx).map_or(0, |&s| s as usize).min(at);
let segment = &text[line_start..at];
let col = if segment.is_ascii() {
segment.len()
} else {
segment.chars().count()
};
LineCol::new(
(line_idx as u32).saturating_add(1),
(col as u32).saturating_add(1),
)
}
#[cfg(test)]
mod tests {
use super::*;
use span_lang::LineIndex;
#[test]
fn test_line_starts_follow_the_line_index_model() {
assert_eq!(&*line_starts(""), &[0]);
assert_eq!(&*line_starts("a"), &[0]);
assert_eq!(&*line_starts("a\n"), &[0, 2]);
assert_eq!(&*line_starts("a\r\nb\rc\n\n"), &[0, 3, 7, 8]);
}
#[test]
fn test_cached_line_col_matches_line_index() {
for text in ["", "abc", "αβ\n\tx\r\nlast", "\n\n", "a\rb\n世界\n"] {
let starts = line_starts(text);
let index = LineIndex::new(text);
for at in 0..=text.len() as u32 + 3 {
let pos = BytePos::new(at);
assert_eq!(
line_col(text, &starts, pos),
index.line_col(pos),
"{text:?} at {at}"
);
}
}
}
}