use crate::LineColumn;
#[derive(Debug, Clone)]
pub(crate) struct LineIndex<T: AsRef<str>> {
text: T,
line_starts: Vec<usize>,
leading_boms: Vec<usize>,
continuation_bytes: Vec<usize>,
}
impl<T: AsRef<str>> LineIndex<T> {
pub(crate) fn new(text: T) -> Self {
let bytes = text.as_ref().as_bytes();
let mut line_starts = vec![0];
let mut continuation_bytes = Vec::new();
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' || (b == b'\r' && bytes.get(i + 1) != Some(&b'\n')) {
line_starts.push(i + 1);
} else if b & 0xC0 == 0x80 {
continuation_bytes.push(i);
}
}
let leading_boms = line_starts
.iter()
.map(|&start| {
let line = &text.as_ref()[start..];
(line.len() - line.trim_start_matches('\u{FEFF}').len()) / '\u{FEFF}'.len_utf8()
})
.collect();
Self {
text,
line_starts,
leading_boms,
continuation_bytes,
}
}
pub(crate) fn text(&self) -> &str {
self.text.as_ref()
}
pub(crate) fn line_column(&self, offset: usize) -> LineColumn {
let line = self.line_of(offset);
LineColumn::new(line + 1, self.column_in_line(line, offset) + 1)
}
pub(crate) fn column(&self, offset: usize) -> usize {
self.column_in_line(self.line_of(offset), offset)
}
pub(crate) fn line_start(&self, offset: usize) -> usize {
self.line_starts[self.line_of(offset)]
}
fn line_of(&self, offset: usize) -> usize {
self.line_starts.partition_point(|&s| s <= offset) - 1
}
fn column_in_line(&self, line: usize, offset: usize) -> usize {
let start = self.line_starts[line];
let offset = offset.clamp(start, self.text().len());
let continuations = self.continuation_bytes.partition_point(|&i| i < offset)
- self.continuation_bytes.partition_point(|&i| i < start);
let chars = offset - start - continuations;
chars - chars.min(self.leading_boms[line])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_breaks() {
let index = LineIndex::new("a\nb\rc\r\nd");
let positions: Vec<_> = (0..=9).map(|i| index.line_column(i)).collect();
assert_eq!(
positions,
vec![
LineColumn::new(1, 1),
LineColumn::new(1, 2),
LineColumn::new(2, 1),
LineColumn::new(2, 2),
LineColumn::new(3, 1),
LineColumn::new(3, 2),
LineColumn::new(3, 3),
LineColumn::new(4, 1),
LineColumn::new(4, 2),
LineColumn::new(4, 2),
]
);
}
#[test]
fn test_columns_match_counting_characters() {
let text = "\u{FEFF}\u{FEFF}\u{e9}a\u{FEFF}\r\n\u{1F600}b\rc\u{e9}\n\u{FEFF}";
let index = LineIndex::new(text);
for offset in 0..=text.len() + 2 {
let start = index.line_start(offset);
let expected = text[start..]
.char_indices()
.take_while(|&(i, _)| start + i < offset)
.skip_while(|&(_, c)| c == '\u{FEFF}')
.count();
assert_eq!(index.column(offset), expected, "offset {offset}");
}
}
#[test]
fn test_line_start() {
let index = LineIndex::new("ab\rcd\r\nef");
let starts: Vec<_> = (0..9).map(|i| index.line_start(i)).collect();
assert_eq!(starts, vec![0, 0, 0, 3, 3, 3, 3, 7, 7]);
}
#[test]
fn test_columns_count_chars() {
let index = LineIndex::new("\u{e9}\u{e9}x");
let columns: Vec<_> = (0..=5).map(|i| index.line_column(i).column).collect();
assert_eq!(columns, vec![1, 2, 2, 3, 3, 4]);
}
#[test]
fn test_leading_bom_takes_no_column() {
let index = LineIndex::new("\u{FEFF}a\u{FEFF}");
let columns: Vec<_> = [0, 3, 4, 7].map(|i| index.line_column(i).column).to_vec();
assert_eq!(columns, vec![1, 1, 2, 3]);
}
}