1use std::ops::Range;
4
5use crate::utils::text::clamp_cursor;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9pub struct TextPosition {
10 pub line: usize,
12 pub column: usize,
14}
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub struct TextRange {
19 pub start: TextPosition,
21 pub end: TextPosition,
23}
24
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub enum TextEncoding {
28 Utf8,
30 Utf16,
32 #[default]
34 UnicodeScalar,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct LineIndex {
46 line_starts: Vec<usize>,
47 text_len: usize,
48}
49
50impl LineIndex {
51 pub fn new(text: &str) -> Self {
53 let mut line_starts = vec![0];
54 for (byte, ch) in text.char_indices() {
55 if ch == '\n' {
56 line_starts.push(byte + ch.len_utf8());
57 }
58 }
59
60 Self {
61 line_starts,
62 text_len: text.len(),
63 }
64 }
65
66 pub fn text_len(&self) -> usize {
68 self.text_len
69 }
70
71 pub fn line_count(&self) -> usize {
73 self.line_starts.len()
74 }
75
76 pub fn line_start(&self, line: usize) -> usize {
78 self.line_starts[self.clamp_line(line)]
79 }
80
81 pub fn line_end(&self, text: &str, line: usize) -> usize {
83 let line = self.clamp_line(line);
84 let end = if line + 1 < self.line_starts.len() {
85 self.line_starts[line + 1].saturating_sub(1)
86 } else {
87 text.len()
88 };
89
90 clamp_cursor(text, end.min(text.len()))
91 }
92
93 pub fn line_end_including_newline(&self, text: &str, line: usize) -> usize {
95 let line = self.clamp_line(line);
96 let end = if line + 1 < self.line_starts.len() {
97 self.line_starts[line + 1]
98 } else {
99 text.len()
100 };
101
102 clamp_cursor(text, end.min(text.len()))
103 }
104
105 pub fn line_range(&self, text: &str, line: usize) -> Range<usize> {
107 self.line_start(line).min(text.len())..self.line_end(text, line)
108 }
109
110 pub fn byte_to_position(&self, text: &str, byte: usize) -> TextPosition {
112 self.byte_to_position_with_encoding(text, byte, TextEncoding::UnicodeScalar)
113 }
114
115 pub fn position_to_byte(&self, text: &str, position: TextPosition) -> usize {
117 self.position_to_byte_with_encoding(text, position, TextEncoding::UnicodeScalar)
118 }
119
120 pub fn byte_to_position_with_encoding(
122 &self,
123 text: &str,
124 byte: usize,
125 encoding: TextEncoding,
126 ) -> TextPosition {
127 let byte = clamp_cursor(text, byte);
128 let line = self.line_for_byte(byte);
129 let start = self.line_start(line).min(byte);
130 let column_text = &text[start..byte];
131 let column = match encoding {
132 TextEncoding::Utf8 => byte - start,
133 TextEncoding::Utf16 => column_text.chars().map(char::len_utf16).sum(),
134 TextEncoding::UnicodeScalar => column_text.chars().count(),
135 };
136
137 TextPosition { line, column }
138 }
139
140 pub fn position_to_byte_with_encoding(
142 &self,
143 text: &str,
144 position: TextPosition,
145 encoding: TextEncoding,
146 ) -> usize {
147 let line = self.clamp_line(position.line);
148 let start = self.line_start(line).min(text.len());
149 let end = self.line_end(text, line).max(start);
150 let line_text = &text[start..end];
151 let offset = match encoding {
152 TextEncoding::Utf8 => clamp_cursor(line_text, position.column),
153 TextEncoding::Utf16 => byte_offset_for_utf16_column(line_text, position.column),
154 TextEncoding::UnicodeScalar => {
155 byte_offset_for_scalar_column(line_text, position.column)
156 }
157 };
158
159 start + offset
160 }
161
162 pub fn byte_range_to_range(&self, text: &str, range: Range<usize>) -> TextRange {
164 TextRange {
165 start: self.byte_to_position(text, range.start),
166 end: self.byte_to_position(text, range.end),
167 }
168 }
169
170 pub fn range_to_byte_range(&self, text: &str, range: TextRange) -> Range<usize> {
172 self.position_to_byte(text, range.start)..self.position_to_byte(text, range.end)
173 }
174
175 fn clamp_line(&self, line: usize) -> usize {
176 line.min(self.line_starts.len().saturating_sub(1))
177 }
178
179 fn line_for_byte(&self, byte: usize) -> usize {
180 self.line_starts
181 .partition_point(|&start| start <= byte)
182 .saturating_sub(1)
183 .min(self.line_starts.len().saturating_sub(1))
184 }
185}
186
187fn byte_offset_for_scalar_column(text: &str, column: usize) -> usize {
188 text.char_indices()
189 .map(|(byte, _)| byte)
190 .nth(column)
191 .unwrap_or(text.len())
192}
193
194fn byte_offset_for_utf16_column(text: &str, column: usize) -> usize {
195 let mut units = 0;
196 for (byte, ch) in text.char_indices() {
197 let next_units = units + ch.len_utf16();
198 if next_units > column {
199 return byte;
200 }
201 units = next_units;
202 if units == column {
203 return byte + ch.len_utf8();
204 }
205 }
206 text.len()
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn indexes_empty_text_as_one_line() {
215 let text = "";
216 let index = LineIndex::new(text);
217
218 assert_eq!(index.text_len(), 0);
219 assert_eq!(index.line_count(), 1);
220 assert_eq!(index.line_start(0), 0);
221 assert_eq!(index.line_end(text, 0), 0);
222 assert_eq!(index.line_end_including_newline(text, 0), 0);
223 assert_eq!(index.line_range(text, 0), 0..0);
224 assert_eq!(
225 index.byte_to_position(text, 0),
226 TextPosition { line: 0, column: 0 }
227 );
228 }
229
230 #[test]
231 fn indexes_single_line() {
232 let text = "hello";
233 let index = LineIndex::new(text);
234
235 assert_eq!(index.line_count(), 1);
236 assert_eq!(index.line_start(0), 0);
237 assert_eq!(index.line_end(text, 0), 5);
238 assert_eq!(
239 index.byte_to_position(text, 3),
240 TextPosition { line: 0, column: 3 }
241 );
242 assert_eq!(
243 index.position_to_byte(text, TextPosition { line: 0, column: 4 }),
244 4
245 );
246 }
247
248 #[test]
249 fn indexes_multiple_lines() {
250 let text = "alpha\nbeta\ngamma";
251 let index = LineIndex::new(text);
252
253 assert_eq!(index.line_count(), 3);
254 assert_eq!(index.line_start(1), 6);
255 assert_eq!(index.line_end(text, 1), 10);
256 assert_eq!(index.line_end_including_newline(text, 1), 11);
257 assert_eq!(
258 index.byte_to_position(text, 8),
259 TextPosition { line: 1, column: 2 }
260 );
261 }
262
263 #[test]
264 fn trailing_newline_creates_final_empty_line() {
265 let text = "alpha\n";
266 let index = LineIndex::new(text);
267
268 assert_eq!(index.line_count(), 2);
269 assert_eq!(index.line_start(1), text.len());
270 assert_eq!(index.line_end(text, 1), text.len());
271 assert_eq!(
272 index.byte_to_position(text, text.len()),
273 TextPosition { line: 1, column: 0 }
274 );
275 }
276
277 #[test]
278 fn indexes_consecutive_empty_lines() {
279 let text = "a\n\nb";
280 let index = LineIndex::new(text);
281
282 assert_eq!(index.line_count(), 3);
283 assert_eq!(index.line_range(text, 1), 2..2);
284 assert_eq!(
285 index.byte_to_position(text, 2),
286 TextPosition { line: 1, column: 0 }
287 );
288 assert_eq!(
289 index.byte_to_position(text, 3),
290 TextPosition { line: 2, column: 0 }
291 );
292 }
293
294 #[test]
295 fn counts_non_ascii_columns_as_scalars_by_default() {
296 let text = "aé你";
297 let index = LineIndex::new(text);
298
299 assert_eq!(
300 index.byte_to_position(text, 3),
301 TextPosition { line: 0, column: 2 }
302 );
303 assert_eq!(
304 index.position_to_byte(text, TextPosition { line: 0, column: 2 }),
305 3
306 );
307 assert_eq!(
308 index.position_to_byte(text, TextPosition { line: 0, column: 3 }),
309 text.len()
310 );
311 }
312
313 #[test]
314 fn clamps_bytes_inside_codepoint_to_previous_boundary() {
315 let text = "aéb";
316 let index = LineIndex::new(text);
317
318 assert_eq!(
319 index.byte_to_position(text, 2),
320 TextPosition { line: 0, column: 1 }
321 );
322 assert_eq!(
323 index.byte_to_position_with_encoding(text, 2, TextEncoding::Utf8),
324 TextPosition { line: 0, column: 1 }
325 );
326 }
327
328 #[test]
329 fn supports_utf16_columns_for_non_bmp_characters() {
330 let text = "a💩b";
331 let index = LineIndex::new(text);
332
333 assert_eq!(
334 index.byte_to_position_with_encoding(text, 5, TextEncoding::Utf16),
335 TextPosition { line: 0, column: 3 }
336 );
337 assert_eq!(
338 index.position_to_byte_with_encoding(
339 text,
340 TextPosition { line: 0, column: 1 },
341 TextEncoding::Utf16
342 ),
343 1
344 );
345 assert_eq!(
346 index.position_to_byte_with_encoding(
347 text,
348 TextPosition { line: 0, column: 2 },
349 TextEncoding::Utf16
350 ),
351 1
352 );
353 assert_eq!(
354 index.position_to_byte_with_encoding(
355 text,
356 TextPosition { line: 0, column: 3 },
357 TextEncoding::Utf16
358 ),
359 5
360 );
361 assert_eq!(
362 index.position_to_byte_with_encoding(
363 text,
364 TextPosition { line: 0, column: 4 },
365 TextEncoding::Utf16
366 ),
367 6
368 );
369 }
370
371 #[test]
372 fn converts_ranges_round_trip() {
373 let text = "ab\nc💩d";
374 let index = LineIndex::new(text);
375 let bytes = 1..8;
376 let range = index.byte_range_to_range(text, bytes.clone());
377
378 assert_eq!(range.start, TextPosition { line: 0, column: 1 });
379 assert_eq!(range.end, TextPosition { line: 1, column: 2 });
380 assert_eq!(index.range_to_byte_range(text, range), bytes);
381 }
382
383 #[test]
384 fn clamps_out_of_range_line_and_column() {
385 let text = "abc\nde";
386 let index = LineIndex::new(text);
387
388 assert_eq!(index.line_start(99), 4);
389 assert_eq!(index.line_end(text, 99), text.len());
390 assert_eq!(
391 index.position_to_byte(
392 text,
393 TextPosition {
394 line: 99,
395 column: 99
396 }
397 ),
398 text.len()
399 );
400 assert_eq!(
401 index.byte_to_position(text, 999),
402 TextPosition { line: 1, column: 2 }
403 );
404 }
405
406 #[test]
407 fn treats_carriage_return_as_content() {
408 let text = "a\rb\nc";
409 let index = LineIndex::new(text);
410
411 assert_eq!(index.line_count(), 2);
412 assert_eq!(
413 index.byte_to_position(text, 3),
414 TextPosition { line: 0, column: 3 }
415 );
416 }
417}