pub struct OffsetTable {
breakpoints: Vec<(usize, i64)>,
}
impl OffsetTable {
pub fn build(src: &str) -> Self {
let mut breakpoints = Vec::new();
let mut divergence: i64 = 0;
for (byte_start, ch) in src.char_indices() {
let utf8_len = ch.len_utf8() as i64;
let utf16_len = ch.len_utf16() as i64;
if utf8_len != utf16_len {
divergence += utf8_len - utf16_len;
breakpoints.push((byte_start + ch.len_utf8(), divergence));
}
}
OffsetTable { breakpoints }
}
fn divergence_at(&self, byte_offset: usize) -> i64 {
match self
.breakpoints
.binary_search_by(|(pos, _)| pos.cmp(&byte_offset))
{
Ok(idx) => self.breakpoints[idx].1,
Err(0) => 0,
Err(idx) => self.breakpoints[idx - 1].1,
}
}
pub fn byte_to_utf16(&self, byte_offset: usize) -> usize {
(byte_offset as i64 - self.divergence_at(byte_offset)) as usize
}
pub fn translate_point(&self, byte_offset: usize, column_bytes: usize) -> (usize, usize) {
let line_start = byte_offset - column_bytes;
let offset_u16 = self.byte_to_utf16(byte_offset);
let line_start_u16 = self.byte_to_utf16(line_start);
(offset_u16, offset_u16 - line_start_u16)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_has_no_divergence() {
let src = "class Widget {}\n";
let table = OffsetTable::build(src);
for byte in 0..=src.len() {
assert_eq!(table.byte_to_utf16(byte), byte);
}
}
#[test]
fn bom_diverges_by_two() {
let src = "\u{FEFF}class Widget {}";
let table = OffsetTable::build(src);
assert_eq!(table.byte_to_utf16(0), 0);
assert_eq!(table.byte_to_utf16(3), 1); assert_eq!(table.byte_to_utf16(3 + 5), 1 + 5); }
#[test]
fn accented_char_diverges_by_one() {
let src = "// caf\u{E9}\n";
let table = OffsetTable::build(src);
let accent_byte_start = 6; assert_eq!(table.byte_to_utf16(accent_byte_start), 6);
assert_eq!(table.byte_to_utf16(accent_byte_start + 2), 7);
}
#[test]
fn cjk_char_diverges_by_two() {
let src = "// \u{4E2D}\n";
let table = OffsetTable::build(src);
assert_eq!(table.byte_to_utf16(3), 3); assert_eq!(table.byte_to_utf16(3 + 3), 4);
}
#[test]
fn astral_emoji_diverges_by_two_via_surrogate_pair() {
let src = "// \u{1F600}\n";
let table = OffsetTable::build(src);
assert_eq!(table.byte_to_utf16(3), 3);
assert_eq!(table.byte_to_utf16(3 + 4), 3 + 2);
}
#[test]
fn multiple_multibyte_chars_accumulate() {
let src = "\u{00E9}\u{00E9}"; let table = OffsetTable::build(src);
assert_eq!(table.byte_to_utf16(0), 0);
assert_eq!(table.byte_to_utf16(2), 1);
assert_eq!(table.byte_to_utf16(4), 2);
}
#[test]
fn column_ignores_divergence_from_earlier_lines() {
let src = "// \u{00E9}\u{00E9}\nclass Widget {}\n";
let table = OffsetTable::build(src);
let widget_byte = src.find("Widget").unwrap();
let line_start_byte = src.find("class").unwrap();
let column_bytes = widget_byte - line_start_byte;
assert_eq!(column_bytes, 6);
let (offset_u16, column_u16) = table.translate_point(widget_byte, column_bytes);
assert_eq!(
column_u16, 6,
"line 2 is ASCII; column must equal raw byte column"
);
assert_eq!(
offset_u16,
widget_byte - 2,
"global offset reflects line 1's -2 divergence"
);
}
}