1use std::sync::Arc;
4
5const CHECKPOINT_BYTES: usize = 256;
6
7#[derive(Clone, Copy, Debug)]
8struct Checkpoint {
9 byte: usize,
10 column: usize,
11}
12
13#[derive(Clone, Copy, Debug)]
14struct Line {
15 start: usize,
16 checkpoint: usize,
17}
18
19#[derive(Clone, Debug)]
36pub struct LineIndex {
37 text: Arc<str>,
38 lines: Arc<[Line]>,
39 checkpoints: Arc<[Checkpoint]>,
40}
41
42impl LineIndex {
43 #[must_use]
45 pub fn new(text: impl Into<Arc<str>>) -> Self {
46 let text = text.into();
47 let mut lines = vec![Line {
48 start: 0,
49 checkpoint: 0,
50 }];
51 let mut checkpoints = Vec::new();
52 let mut checkpoint_byte = 0;
53 let mut column = 0;
54 for (byte, character) in text.char_indices() {
55 if byte - checkpoint_byte >= CHECKPOINT_BYTES {
56 checkpoints.push(Checkpoint { byte, column });
57 checkpoint_byte = byte;
58 }
59 if character == '\n' {
60 let start = byte + 1;
61 lines.push(Line {
62 start,
63 checkpoint: checkpoints.len(),
64 });
65 checkpoint_byte = start;
66 column = 0;
67 } else {
68 column += character.len_utf16();
69 }
70 }
71 Self {
72 text,
73 lines: lines.into(),
74 checkpoints: checkpoints.into(),
75 }
76 }
77
78 #[must_use]
80 pub fn text(&self) -> &str {
81 &self.text
82 }
83
84 #[must_use]
87 pub fn utf16_position(&self, byte: usize) -> (usize, usize) {
88 let byte = self.text.floor_char_boundary(byte.min(self.text.len()));
89 let line = self.lines.partition_point(|line| line.start <= byte) - 1;
90 let checkpoints = self.line_checkpoints(line);
91 let checkpoint = checkpoints
92 .partition_point(|point| point.byte <= byte)
93 .checked_sub(1)
94 .map_or(
95 Checkpoint {
96 byte: self.lines[line].start,
97 column: 0,
98 },
99 |index| checkpoints[index],
100 );
101 let column = checkpoint.column + self.text[checkpoint.byte..byte].encode_utf16().count();
102 (line, column)
103 }
104
105 #[must_use]
109 pub fn byte_offset(&self, line: usize, column: usize) -> usize {
110 let Some(_) = self.lines.get(line) else {
111 return self.text.len();
112 };
113 let checkpoints = self.line_checkpoints(line);
114 let checkpoint = checkpoints
115 .partition_point(|point| point.column <= column)
116 .checked_sub(1)
117 .map_or(
118 Checkpoint {
119 byte: self.lines[line].start,
120 column: 0,
121 },
122 |index| checkpoints[index],
123 );
124 let mut current_column = checkpoint.column;
125 for (offset, character) in self.text[checkpoint.byte..].char_indices() {
126 if character == '\n' || current_column >= column {
127 return checkpoint.byte + offset;
128 }
129 current_column += character.len_utf16();
130 }
131 self.text.len()
132 }
133
134 fn line_checkpoints(&self, line: usize) -> &[Checkpoint] {
135 let start = self.lines[line].checkpoint;
136 let end = self
137 .lines
138 .get(line + 1)
139 .map_or(self.checkpoints.len(), |next| next.checkpoint);
140 &self.checkpoints[start..end]
141 }
142}
143
144impl std::ops::Deref for LineIndex {
145 type Target = str;
146 fn deref(&self) -> &str {
147 self.text()
148 }
149}
150
151impl From<String> for LineIndex {
152 fn from(text: String) -> Self {
153 Self::new(text)
154 }
155}
156
157impl From<&str> for LineIndex {
158 fn from(text: &str) -> Self {
159 Self::new(text)
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn positions_preserve_unicode_and_newline_boundaries() {
169 let source = LineIndex::new("a😀\r\nå—\n");
170 assert_eq!(source.utf16_position(3), (0, 1));
171 assert_eq!(source.utf16_position(5), (0, 3));
172 assert_eq!(source.utf16_position(6), (0, 4));
173 assert_eq!(source.utf16_position(7), (1, 0));
174 assert_eq!(source.utf16_position(usize::MAX), (2, 0));
175 assert_eq!(source.byte_offset(0, 2), 5);
176 assert_eq!(source.byte_offset(0, usize::MAX), 6);
177 assert_eq!(source.byte_offset(usize::MAX, 0), source.len());
178 assert_eq!(LineIndex::new("").utf16_position(1), (0, 0));
179 assert_eq!(LineIndex::new("").byte_offset(0, 1), 0);
180 }
181
182 #[test]
183 fn checkpoints_round_trip_long_lines_and_clones_share_storage() {
184 let text = format!("{}\n{}\n", "a😀éå—\t".repeat(300), "x".repeat(1025));
185 let source = LineIndex::new(text);
186 for (byte, _) in source.char_indices() {
187 let (line, column) = source.utf16_position(byte);
188 assert_eq!(source.byte_offset(line, column), byte);
189 }
190 let clone = source.clone();
191 assert!(Arc::ptr_eq(&source.text, &clone.text));
192 assert!(Arc::ptr_eq(&source.lines, &clone.lines));
193 assert!(Arc::ptr_eq(&source.checkpoints, &clone.checkpoints));
194 }
195}