pub struct ByteLocator {
line_starts: Vec<usize>,
line_utf16_starts: Vec<usize>,
}
impl ByteLocator {
pub fn new(source: &str) -> Self {
let mut line_starts = vec![0usize];
let mut line_utf16_starts = vec![0usize];
let mut utf16 = 0usize;
for (byte_idx, ch) in source.char_indices() {
utf16 += ch.len_utf16();
if ch == '\n' {
line_starts.push(byte_idx + 1);
line_utf16_starts.push(utf16);
}
}
Self { line_starts, line_utf16_starts }
}
pub fn byte_offset(&self, line: usize, column: usize) -> usize {
if line >= self.line_starts.len() {
return self.line_starts.last().copied().unwrap_or(0) + column;
}
self.line_starts[line] + column
}
pub fn locate_utf16(&self, source: &str, byte_offset: usize) -> (usize, usize, usize) {
let line_idx = self.line_starts.partition_point(|&start| start <= byte_offset) - 1;
let line_byte_start = self.line_starts[line_idx];
let line_utf16_start = self.line_utf16_starts[line_idx];
let end = byte_offset.min(source.len());
let column: usize = source
.get(line_byte_start..end)
.map(|within_line| within_line.chars().map(char::len_utf16).sum())
.unwrap_or(0);
(line_idx + 1, column, line_utf16_start + column)
}
}
#[cfg(test)]
mod test_locator {
#[test]
fn line_column_to_byte_offset() {
use super::ByteLocator;
let source = "abc\ndef\ncghi";
assert_eq!(ByteLocator::new(source).byte_offset(0, 0), 0);
assert_eq!(ByteLocator::new(source).byte_offset(1, 0), 4);
}
#[test]
fn byte_offset_to_location() {
use super::ByteLocator;
let source = "abc\ndef\ncghi";
let locator = ByteLocator::new(source);
assert_eq!(locator.locate_utf16(source, 0), (1, 0, 0));
assert_eq!(locator.locate_utf16(source, 3), (1, 3, 3)); assert_eq!(locator.locate_utf16(source, 4), (2, 0, 4)); assert_eq!(locator.locate_utf16(source, 5), (2, 1, 5));
let unicode = "ß💣\n💣ß";
let locator = ByteLocator::new(unicode);
assert_eq!(locator.locate_utf16(unicode, 0), (1, 0, 0));
assert_eq!(locator.locate_utf16(unicode, 7), (2, 0, 4)); assert_eq!(locator.locate_utf16(unicode, 11), (2, 2, 6)); }
fn reference_locate(source: &str, byte_offset: usize) -> (usize, usize, usize) {
let end = byte_offset.min(source.len());
let prefix = &source[..end];
let utf16_position: usize = prefix.chars().map(char::len_utf16).sum();
let line0 = prefix.matches('\n').count();
let line_byte_start = prefix.rfind('\n').map_or(0, |i| i + 1);
let column: usize = source[line_byte_start..end].chars().map(char::len_utf16).sum();
(line0 + 1, column, utf16_position)
}
fn assert_matches_reference(source: &str) {
use super::ByteLocator;
let locator = ByteLocator::new(source);
for byte_offset in 0..=source.len() {
if !source.is_char_boundary(byte_offset) {
continue;
}
assert_eq!(
locator.locate_utf16(source, byte_offset),
reference_locate(source, byte_offset),
"mismatch at byte offset {byte_offset} of {source:?}"
);
}
let past_end = source.len() + 5;
assert_eq!(
locator.locate_utf16(source, past_end),
reference_locate(source, past_end),
"mismatch at out-of-range offset {past_end} of {source:?}"
);
}
#[test]
fn locate_utf16_matches_linear_reference() {
for source in [
"", "a\nbc\n\ndef", "café\n😀xy\nz", ] {
assert_matches_reference(source);
}
}
}