#[cfg(feature = "parser-ruff")]
use ruff_text_size::{Ranged, TextRange};
use crate::common::SourceSpan;
pub(crate) struct LineIndex {
line_starts: Vec<u32>,
total_len: u32,
}
impl LineIndex {
pub(crate) fn new(src: &str) -> Self {
let mut line_starts = Vec::with_capacity(src.len() / 40);
line_starts.push(0u32);
for (idx, b) in src.as_bytes().iter().enumerate() {
if *b == b'\n' {
let next = u32::try_from(idx + 1).unwrap_or(u32::MAX);
line_starts.push(next);
}
}
Self {
line_starts,
total_len: u32::try_from(src.len()).unwrap_or(u32::MAX),
}
}
pub(crate) fn line_col(&self, offset: u32) -> (u32, u32) {
let clamped = offset.min(self.total_len);
let line_idx = match self.line_starts.binary_search(&clamped) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let line_start = self.line_starts[line_idx];
let col = clamped.saturating_sub(line_start) + 1;
(u32::try_from(line_idx).unwrap_or(u32::MAX) + 1, col)
}
#[cfg(feature = "parser-ruff")]
pub(crate) fn span_of(&self, range: TextRange) -> SourceSpan {
let (start_line, _) = self.line_col(range.start().to_u32());
let end_offset = range.end().to_u32().saturating_sub(1);
let (end_line, _) = self.line_col(end_offset);
SourceSpan {
start_line,
end_line: end_line.max(start_line),
}
}
}
#[allow(dead_code)]
#[cfg(feature = "parser-ruff")]
pub(crate) fn ruff_line_col(idx: &LineIndex, node: &impl Ranged) -> (u32, u32) {
idx.line_col(node.range().start().to_u32())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_byte_is_line_one_col_one() {
let idx = LineIndex::new("abc\ndef\n");
assert_eq!(idx.line_col(0), (1, 1));
}
#[test]
fn newline_advances_line() {
let idx = LineIndex::new("abc\ndef\n");
assert_eq!(idx.line_col(4), (2, 1));
assert_eq!(idx.line_col(5), (2, 2));
}
#[test]
fn offset_past_eof_clamps_to_last_line() {
let idx = LineIndex::new("abc\ndef");
assert_eq!(idx.line_col(99).0, 2);
}
#[test]
fn empty_source_collapses_to_one_one() {
let idx = LineIndex::new("");
assert_eq!(idx.line_col(0), (1, 1));
assert_eq!(idx.line_col(7), (1, 1));
}
}