1pub struct OffsetTable {
15 breakpoints: Vec<(usize, i64)>,
21}
22
23impl OffsetTable {
24 pub fn build(src: &str) -> Self {
26 let mut breakpoints = Vec::new();
27 let mut divergence: i64 = 0;
28 for (byte_start, ch) in src.char_indices() {
29 let utf8_len = ch.len_utf8() as i64;
30 let utf16_len = ch.len_utf16() as i64;
31 if utf8_len != utf16_len {
32 divergence += utf8_len - utf16_len;
33 breakpoints.push((byte_start + ch.len_utf8(), divergence));
34 }
35 }
36 OffsetTable { breakpoints }
37 }
38
39 fn divergence_at(&self, byte_offset: usize) -> i64 {
42 match self
43 .breakpoints
44 .binary_search_by(|(pos, _)| pos.cmp(&byte_offset))
45 {
46 Ok(idx) => self.breakpoints[idx].1,
47 Err(0) => 0,
48 Err(idx) => self.breakpoints[idx - 1].1,
49 }
50 }
51
52 pub fn byte_to_utf16(&self, byte_offset: usize) -> usize {
56 (byte_offset as i64 - self.divergence_at(byte_offset)) as usize
57 }
58
59 pub fn translate_point(&self, byte_offset: usize, column_bytes: usize) -> (usize, usize) {
64 let line_start = byte_offset - column_bytes;
65 let offset_u16 = self.byte_to_utf16(byte_offset);
66 let line_start_u16 = self.byte_to_utf16(line_start);
67 (offset_u16, offset_u16 - line_start_u16)
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn ascii_has_no_divergence() {
77 let src = "class Widget {}\n";
78 let table = OffsetTable::build(src);
79 for byte in 0..=src.len() {
80 assert_eq!(table.byte_to_utf16(byte), byte);
81 }
82 }
83
84 #[test]
85 fn bom_diverges_by_two() {
86 let src = "\u{FEFF}class Widget {}";
88 let table = OffsetTable::build(src);
89 assert_eq!(table.byte_to_utf16(0), 0);
90 assert_eq!(table.byte_to_utf16(3), 1); assert_eq!(table.byte_to_utf16(3 + 5), 1 + 5); }
93
94 #[test]
95 fn accented_char_diverges_by_one() {
96 let src = "// caf\u{E9}\n";
98 let table = OffsetTable::build(src);
99 let accent_byte_start = 6; assert_eq!(table.byte_to_utf16(accent_byte_start), 6);
101 assert_eq!(table.byte_to_utf16(accent_byte_start + 2), 7);
102 }
103
104 #[test]
105 fn cjk_char_diverges_by_two() {
106 let src = "// \u{4E2D}\n";
108 let table = OffsetTable::build(src);
109 assert_eq!(table.byte_to_utf16(3), 3); assert_eq!(table.byte_to_utf16(3 + 3), 4);
111 }
112
113 #[test]
114 fn astral_emoji_diverges_by_two_via_surrogate_pair() {
115 let src = "// \u{1F600}\n";
117 let table = OffsetTable::build(src);
118 assert_eq!(table.byte_to_utf16(3), 3);
119 assert_eq!(table.byte_to_utf16(3 + 4), 3 + 2);
120 }
121
122 #[test]
123 fn multiple_multibyte_chars_accumulate() {
124 let src = "\u{00E9}\u{00E9}"; let table = OffsetTable::build(src);
126 assert_eq!(table.byte_to_utf16(0), 0);
127 assert_eq!(table.byte_to_utf16(2), 1);
128 assert_eq!(table.byte_to_utf16(4), 2);
129 }
130
131 #[test]
132 fn column_ignores_divergence_from_earlier_lines() {
133 let src = "// \u{00E9}\u{00E9}\nclass Widget {}\n";
137 let table = OffsetTable::build(src);
138 let widget_byte = src.find("Widget").unwrap();
139 let line_start_byte = src.find("class").unwrap();
140 let column_bytes = widget_byte - line_start_byte;
141 assert_eq!(column_bytes, 6);
142
143 let (offset_u16, column_u16) = table.translate_point(widget_byte, column_bytes);
144 assert_eq!(
145 column_u16, 6,
146 "line 2 is ASCII; column must equal raw byte column"
147 );
148 assert_eq!(
149 offset_u16,
150 widget_byte - 2,
151 "global offset reflects line 1's -2 divergence"
152 );
153 }
154}