#[derive(Clone, Copy)]
struct NonAsciiSpan {
byte_start: u32,
byte_end: u32,
utf16_start: u32,
utf16_end: u32,
}
pub struct SourceIndex {
line_starts_utf16: Vec<u32>,
line_ends_utf16: Vec<u32>,
non_ascii_spans: Vec<NonAsciiSpan>,
byte_len: u32,
utf16_len: u32,
}
impl SourceIndex {
#[expect(
clippy::cast_possible_truncation,
reason = "`char::len_utf8` and `char::len_utf16` both return a value in `1..=4`"
)]
pub fn new(source: &str) -> Self {
let mut line_starts_utf16 = vec![0];
let mut line_ends_utf16 = Vec::new();
let mut non_ascii_spans = Vec::new();
let mut utf16_offset = 0u32;
let mut chars = source.char_indices().peekable();
while let Some((byte_start, ch)) = chars.next() {
let byte_start = u32::try_from(byte_start).unwrap_or(u32::MAX);
let byte_width = ch.len_utf8() as u32;
let utf16_width = ch.len_utf16() as u32;
if byte_width != utf16_width {
non_ascii_spans.push(NonAsciiSpan {
byte_start,
byte_end: byte_start.saturating_add(byte_width),
utf16_start: utf16_offset,
utf16_end: utf16_offset.saturating_add(utf16_width),
});
}
match ch {
'\r' => {
line_ends_utf16.push(utf16_offset);
utf16_offset = utf16_offset.saturating_add(1);
if chars.peek().is_some_and(|(_, next)| *next == '\n') {
chars.next();
utf16_offset = utf16_offset.saturating_add(1);
}
line_starts_utf16.push(utf16_offset);
}
'\n' | '\u{2028}' | '\u{2029}' => {
line_ends_utf16.push(utf16_offset);
utf16_offset = utf16_offset.saturating_add(utf16_width);
line_starts_utf16.push(utf16_offset);
}
_ => utf16_offset = utf16_offset.saturating_add(utf16_width),
}
}
line_ends_utf16.push(utf16_offset);
Self {
line_starts_utf16,
line_ends_utf16,
non_ascii_spans,
byte_len: u32::try_from(source.len()).unwrap_or(u32::MAX),
utf16_len: utf16_offset,
}
}
pub fn byte_to_utf16(&self, byte_offset: u32) -> u32 {
let byte_offset = byte_offset.min(self.byte_len);
let index = self.non_ascii_spans.partition_point(|span| span.byte_end <= byte_offset);
if let Some(span) = self.non_ascii_spans.get(index)
&& byte_offset > span.byte_start
&& byte_offset < span.byte_end
{
return span.utf16_start;
}
let byte_utf16_delta = index
.checked_sub(1)
.and_then(|previous| self.non_ascii_spans.get(previous))
.map_or(0, |span| span.byte_end.saturating_sub(span.utf16_end));
byte_offset.saturating_sub(byte_utf16_delta).min(self.utf16_len)
}
pub fn position_to_utf16(&self, line_1based: u32, column_utf16: u32) -> u32 {
if line_1based == 0 {
return 0;
}
let line_index = (line_1based - 1) as usize;
let Some(line_start) = self.line_starts_utf16.get(line_index).copied() else {
return self.utf16_len;
};
let line_end = self.line_ends_utf16.get(line_index).copied().unwrap_or(self.utf16_len);
line_start.saturating_add(column_utf16.min(line_end.saturating_sub(line_start)))
}
}