1use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum TextViewError {
9 InvalidRange { start: usize, end: usize },
10 OffsetOutOfBounds { offset: usize, len: usize },
11 InvalidUtf8Boundary { offset: usize },
12 LineOutOfBounds { line: u32, line_count: u32 },
13 Utf16OffsetOutOfBounds { offset: usize, total: usize },
14}
15
16impl fmt::Display for TextViewError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::InvalidRange { start, end } => {
20 write!(f, "invalid range: start {start} > end {end}")
21 }
22 Self::OffsetOutOfBounds { offset, len } => {
23 write!(f, "offset {offset} out of bounds (len {len})")
24 }
25 Self::InvalidUtf8Boundary { offset } => {
26 write!(f, "offset {offset} is not on a UTF-8 char boundary")
27 }
28 Self::LineOutOfBounds { line, line_count } => {
29 write!(f, "line {line} out of bounds (line_count {line_count})")
30 }
31 Self::Utf16OffsetOutOfBounds { offset, total } => {
32 write!(f, "UTF-16 offset {offset} out of bounds (total {total})")
33 }
34 }
35 }
36}
37
38impl std::error::Error for TextViewError {}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct Position {
43 line: u32,
44 column: u32,
45}
46
47impl Position {
48 pub const fn new(line: u32, column: u32) -> Self {
49 Self { line, column }
50 }
51
52 pub const fn line(&self) -> u32 {
53 self.line
54 }
55
56 pub const fn column(&self) -> u32 {
57 self.column
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub struct TextRange {
64 start: usize,
65 end: usize,
66}
67
68impl TextRange {
69 pub fn new(start: usize, end: usize) -> Result<Self, TextViewError> {
70 if start > end {
71 return Err(TextViewError::InvalidRange { start, end });
72 }
73 Ok(Self { start, end })
74 }
75
76 pub fn empty(offset: usize) -> Self {
77 Self {
78 start: offset,
79 end: offset,
80 }
81 }
82
83 pub fn start(&self) -> usize {
84 self.start
85 }
86
87 pub fn end(&self) -> usize {
88 self.end
89 }
90
91 pub fn len(&self) -> usize {
92 self.end - self.start
93 }
94
95 pub fn is_empty(&self) -> bool {
96 self.start == self.end
97 }
98
99 pub fn contains(&self, offset: usize) -> bool {
100 self.start <= offset && offset < self.end
101 }
102
103 pub fn intersects(&self, other: &TextRange) -> bool {
104 self.start < other.end && other.start < self.end
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct RangeChange {
111 start: usize,
112 old_end: usize,
113 new_end: usize,
114}
115
116impl RangeChange {
117 pub const fn new(start: usize, old_end: usize, new_end: usize) -> Self {
118 Self {
119 start,
120 old_end,
121 new_end,
122 }
123 }
124
125 pub const fn start(&self) -> usize {
126 self.start
127 }
128
129 pub const fn old_end(&self) -> usize {
130 self.old_end
131 }
132
133 pub const fn new_end(&self) -> usize {
134 self.new_end
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct Selection {
141 anchor: usize,
142 head: usize,
143}
144
145impl Selection {
146 pub const fn new(anchor: usize, head: usize) -> Self {
147 Self { anchor, head }
148 }
149
150 pub const fn cursor(offset: usize) -> Self {
151 Self {
152 anchor: offset,
153 head: offset,
154 }
155 }
156
157 pub const fn anchor(&self) -> usize {
158 self.anchor
159 }
160
161 pub const fn head(&self) -> usize {
162 self.head
163 }
164
165 pub fn range(&self) -> TextRange {
167 if self.anchor <= self.head {
168 TextRange {
169 start: self.anchor,
170 end: self.head,
171 }
172 } else {
173 TextRange {
174 start: self.head,
175 end: self.anchor,
176 }
177 }
178 }
179
180 pub fn is_cursor(&self) -> bool {
181 self.anchor == self.head
182 }
183
184 pub fn is_forward(&self) -> bool {
185 self.anchor <= self.head
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct LineIndex {
192 line_starts: Vec<u32>,
193 len: u32,
194}
195
196impl LineIndex {
197 pub fn new(text: &str) -> Self {
198 let len = u32::try_from(text.len()).expect("text length exceeds u32::MAX");
199 let mut line_starts = vec![0u32];
200 for (i, b) in text.bytes().enumerate() {
201 if b == b'\n' {
202 let next = u32::try_from(i + 1).expect("offset exceeds u32::MAX");
203 line_starts.push(next);
204 }
205 }
206 Self { line_starts, len }
207 }
208
209 pub fn line_count(&self) -> u32 {
210 u32::try_from(self.line_starts.len()).expect("line count exceeds u32::MAX")
211 }
212
213 pub fn text_len(&self) -> u32 {
214 self.len
215 }
216
217 pub fn offset_to_position(&self, text: &str, offset: usize) -> Result<Position, TextViewError> {
219 let len = self.len as usize;
220 if offset > len {
221 return Err(TextViewError::OffsetOutOfBounds { offset, len });
222 }
223 if offset < len && !text.is_char_boundary(offset) {
224 return Err(TextViewError::InvalidUtf8Boundary { offset });
225 }
226 let line_idx = self.line_of_offset(offset)?;
227 let line_start = self.line_starts[line_idx as usize] as usize;
228 let column = u32::try_from(offset - line_start).expect("column exceeds u32::MAX");
229 Ok(Position::new(line_idx, column))
230 }
231
232 pub fn position_to_offset(
234 &self,
235 text: &str,
236 position: Position,
237 ) -> Result<usize, TextViewError> {
238 let line = position.line();
239 let lc = self.line_count();
240 if line >= lc {
241 return Err(TextViewError::LineOutOfBounds {
242 line,
243 line_count: lc,
244 });
245 }
246 let line_start = self.line_starts[line as usize] as usize;
247 let line_end = if line + 1 < lc {
248 self.line_starts[(line + 1) as usize] as usize
249 } else {
250 self.len as usize
251 };
252 let col = position.column() as usize;
253 let offset = line_start + col;
254 if offset > line_end {
255 return Err(TextViewError::OffsetOutOfBounds {
256 offset,
257 len: self.len as usize,
258 });
259 }
260 if offset < text.len() && !text.is_char_boundary(offset) {
261 return Err(TextViewError::InvalidUtf8Boundary { offset });
262 }
263 Ok(offset)
264 }
265
266 pub fn line_range(&self, line: u32) -> Result<TextRange, TextViewError> {
268 let lc = self.line_count();
269 if line >= lc {
270 return Err(TextViewError::LineOutOfBounds {
271 line,
272 line_count: lc,
273 });
274 }
275 let start = self.line_starts[line as usize] as usize;
276 let end_with_nl = if line + 1 < lc {
277 self.line_starts[(line + 1) as usize] as usize
278 } else {
279 self.len as usize
280 };
281 let end = if end_with_nl > start && line + 1 < lc {
282 end_with_nl - 1
283 } else {
284 end_with_nl
285 };
286 Ok(TextRange { start, end })
287 }
288
289 pub fn line_range_with_newline(&self, line: u32) -> Result<TextRange, TextViewError> {
291 let lc = self.line_count();
292 if line >= lc {
293 return Err(TextViewError::LineOutOfBounds {
294 line,
295 line_count: lc,
296 });
297 }
298 let start = self.line_starts[line as usize] as usize;
299 let end = if line + 1 < lc {
300 self.line_starts[(line + 1) as usize] as usize
301 } else {
302 self.len as usize
303 };
304 Ok(TextRange { start, end })
305 }
306
307 pub fn line_of_offset(&self, offset: usize) -> Result<u32, TextViewError> {
309 let len = self.len as usize;
310 if offset > len {
311 return Err(TextViewError::OffsetOutOfBounds { offset, len });
312 }
313 let idx = self
314 .line_starts
315 .partition_point(|&s| (s as usize) <= offset);
316 let line = if idx == 0 { 0 } else { idx - 1 };
317 Ok(u32::try_from(line).expect("line index exceeds u32::MAX"))
318 }
319}
320
321#[derive(Debug, Clone)]
322struct Utf16Anchor {
323 byte_offset: u32,
324 utf16_offset: u32,
325 byte_len: u8,
326 utf16_len: u8,
327}
328
329#[derive(Debug, Clone)]
331pub struct Utf16Mapping {
332 anchors: Vec<Utf16Anchor>,
333 total_bytes: u32,
334 total_utf16: u32,
335}
336
337impl Utf16Mapping {
338 pub fn new(text: &str) -> Self {
339 let mut anchors = Vec::new();
340 let mut byte_off: u32 = 0;
341 let mut utf16_off: u32 = 0;
342
343 for ch in text.chars() {
344 let byte_len = ch.len_utf8();
345 let utf16_len = ch.len_utf16();
346
347 if byte_len != 1 {
348 anchors.push(Utf16Anchor {
349 byte_offset: byte_off,
350 utf16_offset: utf16_off,
351 byte_len: u8::try_from(byte_len).expect("char byte len exceeds u8"),
352 utf16_len: u8::try_from(utf16_len).expect("char utf16 len exceeds u8"),
353 });
354 }
355
356 byte_off += u32::try_from(byte_len).expect("byte offset exceeds u32");
357 utf16_off += u32::try_from(utf16_len).expect("utf16 offset exceeds u32");
358 }
359
360 Self {
361 anchors,
362 total_bytes: byte_off,
363 total_utf16: utf16_off,
364 }
365 }
366
367 pub fn byte_to_utf16(&self, byte_offset: usize) -> Result<usize, TextViewError> {
369 let total = self.total_bytes as usize;
370 if byte_offset > total {
371 return Err(TextViewError::OffsetOutOfBounds {
372 offset: byte_offset,
373 len: total,
374 });
375 }
376
377 if self.anchors.is_empty() {
378 return Ok(byte_offset);
379 }
380
381 let idx = self
382 .anchors
383 .partition_point(|a| (a.byte_offset as usize) <= byte_offset);
384
385 if idx == 0 {
386 return Ok(byte_offset);
387 }
388
389 let anchor = &self.anchors[idx - 1];
390 let ab = anchor.byte_offset as usize;
391 let au = anchor.utf16_offset as usize;
392 let blen = anchor.byte_len as usize;
393 let ulen = anchor.utf16_len as usize;
394
395 if byte_offset > ab && byte_offset < ab + blen {
396 return Err(TextViewError::InvalidUtf8Boundary {
397 offset: byte_offset,
398 });
399 }
400
401 if byte_offset == ab {
402 return Ok(au);
403 }
404
405 let ascii_past = byte_offset - (ab + blen);
407 Ok(au + ulen + ascii_past)
408 }
409
410 pub fn utf16_to_byte(&self, utf16_offset: usize) -> Result<usize, TextViewError> {
412 let total = self.total_utf16 as usize;
413 if utf16_offset > total {
414 return Err(TextViewError::Utf16OffsetOutOfBounds {
415 offset: utf16_offset,
416 total,
417 });
418 }
419
420 if self.anchors.is_empty() {
421 return Ok(utf16_offset);
422 }
423
424 let idx = self
425 .anchors
426 .partition_point(|a| (a.utf16_offset as usize) <= utf16_offset);
427
428 if idx == 0 {
429 return Ok(utf16_offset);
430 }
431
432 let anchor = &self.anchors[idx - 1];
433 let ab = anchor.byte_offset as usize;
434 let au = anchor.utf16_offset as usize;
435 let blen = anchor.byte_len as usize;
436 let ulen = anchor.utf16_len as usize;
437
438 if utf16_offset > au && utf16_offset < au + ulen {
439 return Err(TextViewError::Utf16OffsetOutOfBounds {
440 offset: utf16_offset,
441 total,
442 });
443 }
444
445 if utf16_offset == au {
446 return Ok(ab);
447 }
448
449 let ascii_past = utf16_offset - (au + ulen);
450 Ok(ab + blen + ascii_past)
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn position_new() {
460 let p = Position::new(3, 7);
461 assert_eq!(p.line(), 3);
462 assert_eq!(p.column(), 7);
463 }
464
465 #[test]
466 fn text_range_new_ok() {
467 let r = TextRange::new(2, 5).unwrap();
468 assert_eq!(r.start(), 2);
469 assert_eq!(r.end(), 5);
470 assert_eq!(r.len(), 3);
471 assert!(!r.is_empty());
472 }
473
474 #[test]
475 fn text_range_new_reversed() {
476 let err = TextRange::new(5, 2).unwrap_err();
477 assert_eq!(err, TextViewError::InvalidRange { start: 5, end: 2 });
478 }
479
480 #[test]
481 fn text_range_empty() {
482 let r = TextRange::empty(10);
483 assert_eq!(r.start(), 10);
484 assert_eq!(r.end(), 10);
485 assert!(r.is_empty());
486 assert_eq!(r.len(), 0);
487 }
488
489 #[test]
490 fn text_range_contains() {
491 let r = TextRange::new(2, 5).unwrap();
492 assert!(r.contains(2));
493 assert!(r.contains(4));
494 assert!(!r.contains(5));
495 assert!(!r.contains(1));
496 }
497
498 #[test]
499 fn text_range_intersects() {
500 let a = TextRange::new(2, 5).unwrap();
501 let b = TextRange::new(4, 8).unwrap();
502 let c = TextRange::new(5, 8).unwrap();
503 assert!(a.intersects(&b));
504 assert!(!a.intersects(&c));
505 let e1 = TextRange::empty(3);
506 let e2 = TextRange::empty(3);
507 assert!(!e1.intersects(&e2));
508 }
509
510 #[test]
511 fn range_change_new() {
512 let change = RangeChange::new(2, 5, 8);
513 assert_eq!(change.start(), 2);
514 assert_eq!(change.old_end(), 5);
515 assert_eq!(change.new_end(), 8);
516 }
517
518 #[test]
519 fn range_change_start() {
520 let change = RangeChange::new(3, 7, 9);
521 assert_eq!(change.start(), 3);
522 }
523
524 #[test]
525 fn range_change_old_end() {
526 let change = RangeChange::new(3, 7, 9);
527 assert_eq!(change.old_end(), 7);
528 }
529
530 #[test]
531 fn range_change_new_end() {
532 let change = RangeChange::new(3, 7, 9);
533 assert_eq!(change.new_end(), 9);
534 }
535
536 #[test]
537 fn selection_cursor() {
538 let s = Selection::cursor(5);
539 assert!(s.is_cursor());
540 assert_eq!(s.anchor(), 5);
541 assert_eq!(s.head(), 5);
542 assert!(s.is_forward());
543 let r = s.range();
544 assert!(r.is_empty());
545 }
546
547 #[test]
548 fn selection_forward() {
549 let s = Selection::new(2, 8);
550 assert!(!s.is_cursor());
551 assert!(s.is_forward());
552 let r = s.range();
553 assert_eq!(r.start(), 2);
554 assert_eq!(r.end(), 8);
555 }
556
557 #[test]
558 fn selection_backward() {
559 let s = Selection::new(8, 2);
560 assert!(!s.is_cursor());
561 assert!(!s.is_forward());
562 let r = s.range();
563 assert_eq!(r.start(), 2);
564 assert_eq!(r.end(), 8);
565 }
566
567 #[test]
568 fn line_index_empty_text() {
569 let text = "";
570 let idx = LineIndex::new(text);
571 assert_eq!(idx.line_count(), 1);
572 assert_eq!(idx.text_len(), 0);
573
574 let pos = idx.offset_to_position(text, 0).unwrap();
575 assert_eq!(pos, Position::new(0, 0));
576
577 let off = idx.position_to_offset(text, Position::new(0, 0)).unwrap();
578 assert_eq!(off, 0);
579 }
580
581 #[test]
582 fn line_index_single_line() {
583 let text = "hello";
584 let idx = LineIndex::new(text);
585 assert_eq!(idx.line_count(), 1);
586 assert_eq!(idx.text_len(), 5);
587
588 let pos = idx.offset_to_position(text, 3).unwrap();
589 assert_eq!(pos, Position::new(0, 3));
590
591 let off = idx.position_to_offset(text, pos).unwrap();
592 assert_eq!(off, 3);
593
594 let pos_end = idx.offset_to_position(text, 5).unwrap();
595 assert_eq!(pos_end, Position::new(0, 5));
596 }
597
598 #[test]
599 fn line_index_multi_line() {
600 let text = "abc\ndef\nghi";
601 let idx = LineIndex::new(text);
602 assert_eq!(idx.line_count(), 3);
603 assert_eq!(idx.text_len(), 11);
604
605 let pos = idx.offset_to_position(text, 4).unwrap();
606 assert_eq!(pos, Position::new(1, 0));
607 let off = idx.position_to_offset(text, pos).unwrap();
608 assert_eq!(off, 4);
609
610 let pos2 = idx.offset_to_position(text, 9).unwrap();
611 assert_eq!(pos2, Position::new(2, 1));
612 let off2 = idx.position_to_offset(text, pos2).unwrap();
613 assert_eq!(off2, 9);
614 }
615
616 #[test]
617 fn line_index_multibyte() {
618 let text = "あいう\nえお";
619 let idx = LineIndex::new(text);
620 assert_eq!(idx.line_count(), 2);
621
622 let pos = idx.offset_to_position(text, 10).unwrap();
623 assert_eq!(pos, Position::new(1, 0));
624 let off = idx.position_to_offset(text, pos).unwrap();
625 assert_eq!(off, 10);
626
627 let pos2 = idx.offset_to_position(text, 3).unwrap();
628 assert_eq!(pos2, Position::new(0, 3));
629 let off2 = idx.position_to_offset(text, pos2).unwrap();
630 assert_eq!(off2, 3);
631 }
632
633 #[test]
634 fn line_index_roundtrip() {
635 let text = "hello\nworld\n";
636 let idx = LineIndex::new(text);
637
638 for offset in 0..=text.len() {
639 if text.is_char_boundary(offset) {
640 let pos = idx.offset_to_position(text, offset).unwrap();
641 let back = idx.position_to_offset(text, pos).unwrap();
642 assert_eq!(back, offset, "roundtrip failed at offset {offset}");
643 }
644 }
645 }
646
647 #[test]
648 fn line_index_offset_out_of_bounds() {
649 let text = "abc";
650 let idx = LineIndex::new(text);
651 let err = idx.offset_to_position(text, 10).unwrap_err();
652 assert_eq!(err, TextViewError::OffsetOutOfBounds { offset: 10, len: 3 });
653 }
654
655 #[test]
656 fn line_index_line_out_of_bounds() {
657 let text = "abc\ndef";
658 let idx = LineIndex::new(text);
659 let err = idx.line_range(5).unwrap_err();
660 assert_eq!(
661 err,
662 TextViewError::LineOutOfBounds {
663 line: 5,
664 line_count: 2
665 }
666 );
667 }
668
669 #[test]
670 fn line_index_invalid_utf8_boundary() {
671 let text = "あ";
672 let idx = LineIndex::new(text);
673 let err = idx.offset_to_position(text, 1).unwrap_err();
674 assert_eq!(err, TextViewError::InvalidUtf8Boundary { offset: 1 });
675 }
676
677 #[test]
678 fn line_index_line_range() {
679 let text = "abc\ndef\n";
680 let idx = LineIndex::new(text);
681
682 let r0 = idx.line_range(0).unwrap();
683 assert_eq!(r0.start(), 0);
684 assert_eq!(r0.end(), 3);
685
686 let r1 = idx.line_range(1).unwrap();
687 assert_eq!(r1.start(), 4);
688 assert_eq!(r1.end(), 7);
689
690 let r2 = idx.line_range(2).unwrap();
691 assert_eq!(r2.start(), 8);
692 assert_eq!(r2.end(), 8);
693
694 let rn0 = idx.line_range_with_newline(0).unwrap();
695 assert_eq!(rn0.start(), 0);
696 assert_eq!(rn0.end(), 4);
697
698 let rn1 = idx.line_range_with_newline(1).unwrap();
699 assert_eq!(rn1.start(), 4);
700 assert_eq!(rn1.end(), 8);
701 }
702
703 #[test]
704 fn line_index_line_of_offset() {
705 let text = "abc\ndef\nghi";
706 let idx = LineIndex::new(text);
707 assert_eq!(idx.line_of_offset(0).unwrap(), 0);
708 assert_eq!(idx.line_of_offset(3).unwrap(), 0);
709 assert_eq!(idx.line_of_offset(4).unwrap(), 1);
710 assert_eq!(idx.line_of_offset(8).unwrap(), 2);
711 assert_eq!(idx.line_of_offset(11).unwrap(), 2);
712 }
713
714 #[test]
715 fn utf16_mapping_ascii() {
716 let text = "hello world";
717 let m = Utf16Mapping::new(text);
718
719 for i in 0..=text.len() {
720 assert_eq!(m.byte_to_utf16(i).unwrap(), i);
721 assert_eq!(m.utf16_to_byte(i).unwrap(), i);
722 }
723 }
724
725 #[test]
726 fn utf16_mapping_japanese() {
727 let text = "aあb";
728 let m = Utf16Mapping::new(text);
729
730 assert_eq!(m.byte_to_utf16(0).unwrap(), 0);
731 assert_eq!(m.byte_to_utf16(1).unwrap(), 1);
732 assert!(m.byte_to_utf16(2).is_err());
733 assert!(m.byte_to_utf16(3).is_err());
734 assert_eq!(m.byte_to_utf16(4).unwrap(), 2);
735 assert_eq!(m.byte_to_utf16(5).unwrap(), 3);
736
737 assert_eq!(m.utf16_to_byte(0).unwrap(), 0);
738 assert_eq!(m.utf16_to_byte(1).unwrap(), 1);
739 assert_eq!(m.utf16_to_byte(2).unwrap(), 4);
740 assert_eq!(m.utf16_to_byte(3).unwrap(), 5);
741 }
742
743 #[test]
744 fn utf16_mapping_surrogate_pair() {
745 let text = "a😀b";
746 let m = Utf16Mapping::new(text);
747
748 assert_eq!(m.byte_to_utf16(0).unwrap(), 0);
749 assert_eq!(m.byte_to_utf16(1).unwrap(), 1);
750 assert!(m.byte_to_utf16(2).is_err());
751 assert!(m.byte_to_utf16(3).is_err());
752 assert!(m.byte_to_utf16(4).is_err());
753 assert_eq!(m.byte_to_utf16(5).unwrap(), 3);
754 assert_eq!(m.byte_to_utf16(6).unwrap(), 4);
755
756 assert_eq!(m.utf16_to_byte(0).unwrap(), 0);
757 assert_eq!(m.utf16_to_byte(1).unwrap(), 1);
758 assert!(m.utf16_to_byte(2).is_err());
759 assert_eq!(m.utf16_to_byte(3).unwrap(), 5);
760 assert_eq!(m.utf16_to_byte(4).unwrap(), 6);
761 }
762
763 #[test]
764 fn utf16_mapping_roundtrip() {
765 let text = "Hello あいう 😀🎉 world";
766 let m = Utf16Mapping::new(text);
767
768 let mut byte_off = 0;
769 for ch in text.chars() {
770 let u16_off = m.byte_to_utf16(byte_off).unwrap();
771 let back = m.utf16_to_byte(u16_off).unwrap();
772 assert_eq!(back, byte_off, "roundtrip failed at byte {byte_off}");
773 byte_off += ch.len_utf8();
774 }
775 let u16_end = m.byte_to_utf16(byte_off).unwrap();
776 let back_end = m.utf16_to_byte(u16_end).unwrap();
777 assert_eq!(back_end, byte_off);
778 }
779
780 #[test]
781 fn utf16_mapping_out_of_bounds() {
782 let text = "abc";
783 let m = Utf16Mapping::new(text);
784
785 assert!(m.byte_to_utf16(10).is_err());
786 assert!(m.utf16_to_byte(10).is_err());
787 }
788
789 #[test]
790 fn error_display() {
791 let e = TextViewError::InvalidRange { start: 5, end: 2 };
792 let s = e.to_string();
793 assert!(s.contains("5"));
794 assert!(s.contains("2"));
795 }
796}