1use std::borrow::Cow;
4use std::fmt;
5
6use ropey::{Rope, RopeSlice};
7use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};
8
9use crate::ropetext::position::{Column, Position, Revision, Span};
10
11#[derive(Debug, Clone)]
24pub struct Text {
25 rope: Rope,
26 revision: Revision,
27}
28
29impl Default for Text {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl fmt::Display for Text {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 for chunk in self.rope.chunks() {
38 f.write_str(chunk)?;
39 }
40 Ok(())
41 }
42}
43
44impl From<&str> for Text {
45 fn from(s: &str) -> Self {
47 Self {
48 rope: Rope::from_str(&normalise_breaks(s)),
49 revision: Revision::fresh(),
50 }
51 }
52}
53
54impl Text {
55 pub fn new() -> Self {
57 Self {
58 rope: Rope::new(),
59 revision: Revision::fresh(),
60 }
61 }
62
63 pub fn revision(&self) -> Revision {
67 self.revision
68 }
69
70 pub fn len_bytes(&self) -> usize {
71 self.rope.len_bytes()
72 }
73
74 pub fn len_chars(&self) -> usize {
75 self.rope.len_chars()
76 }
77
78 pub fn line_count(&self) -> usize {
81 self.rope.len_lines()
82 }
83
84 pub fn line(&self, row: usize) -> Option<Cow<'_, str>> {
90 self.line_slice(row).map(cow_of)
91 }
92
93 pub fn line_len_chars(&self, row: usize) -> Option<usize> {
95 self.line_slice(row).map(|l| l.len_chars())
96 }
97
98 pub fn lines(&self) -> impl Iterator<Item = Cow<'_, str>> {
100 (0..self.line_count()).filter_map(|row| self.line(row))
101 }
102
103 pub fn slice(&self, span: Span) -> Option<Cow<'_, str>> {
107 if span.revision() != self.revision {
108 return None;
109 }
110 Some(cow_of(self.rope.byte_slice(span.byte_range())))
111 }
112
113 pub fn is_stale(&self, position: Position) -> bool {
115 position.revision() != self.revision
116 }
117
118 pub fn position(&self, row: usize, column: Column) -> Option<Position> {
126 let line = self.line_slice(row)?;
127 if column.get() > line.len_chars() {
128 return None;
129 }
130 let byte = self.rope.line_to_byte(row) + line.char_to_byte(column.get());
131 if !self.is_cluster_boundary(byte) {
132 return None;
133 }
134 Some(Position::new(byte, row, column, self.revision))
135 }
136
137 pub fn position_at_byte(&self, byte: usize) -> Option<Position> {
140 if byte > self.rope.len_bytes()
141 || !self.is_char_boundary(byte)
142 || !self.is_cluster_boundary(byte)
143 {
144 return None;
145 }
146 Some(self.position_at_addressable_byte(byte))
147 }
148
149 pub fn position_at_byte_snapped(&self, byte: usize) -> Option<Position> {
160 if byte > self.rope.len_bytes() {
161 return None;
162 }
163 let mut at = byte;
164 while !self.is_char_boundary(at) {
165 at -= 1;
166 }
167 if !self.is_cluster_boundary(at) {
168 at = self.cluster_start_at_or_before(at);
169 }
170 Some(self.position_at_addressable_byte(at))
171 }
172
173 pub fn start(&self) -> Position {
175 Position::new(0, 0, Column::ZERO, self.revision)
176 }
177
178 pub fn end(&self) -> Position {
180 self.position_at_addressable_byte(self.rope.len_bytes())
181 }
182
183 pub fn full_span(&self) -> Span {
185 Span::new(self.start(), self.end())
186 }
187
188 pub fn span(&self, a: Position, b: Position) -> Option<Span> {
195 if self.is_stale(a) || self.is_stale(b) {
196 return None;
197 }
198 Some(if a.byte() <= b.byte() {
199 Span::new(a, b)
200 } else {
201 Span::new(b, a)
202 })
203 }
204
205 pub(crate) fn splice(&mut self, bytes: std::ops::Range<usize>, text: &str) -> usize {
224 assert!(
225 bytes.start <= bytes.end,
226 "splice range {bytes:?} is inverted"
227 );
228 assert!(
229 bytes.end <= self.rope.len_bytes(),
230 "splice range {bytes:?} runs past the text's {} bytes",
231 self.rope.len_bytes()
232 );
233 let start = self.char_boundary(bytes.start);
234 let end = self.char_boundary(bytes.end);
235 if start != end {
236 self.rope.remove(start..end);
237 }
238 let normalised = normalise_breaks(text);
239 if !normalised.is_empty() {
240 self.rope.insert(start, &normalised);
241 }
242 self.revision = Revision::fresh();
243 normalised.len()
244 }
245
246 fn char_boundary(&self, byte: usize) -> usize {
252 let chars = self.rope.byte_to_char(byte);
253 assert_eq!(
254 self.rope.char_to_byte(chars),
255 byte,
256 "splice byte {byte} is inside a character"
257 );
258 chars
259 }
260
261 pub(crate) fn reidentified(&self) -> Self {
268 Self {
269 rope: self.rope.clone(),
270 revision: Revision::fresh(),
271 }
272 }
273
274 pub(crate) fn row_of_byte(&self, byte: usize) -> usize {
276 self.rope.byte_to_line(byte)
277 }
278
279 pub(crate) fn row_start_byte(&self, row: usize) -> usize {
281 self.rope
282 .line_to_byte(row.min(self.line_count().saturating_sub(1)))
283 }
284
285 pub(crate) fn position_at_cursor_byte(&self, byte: usize) -> Position {
298 match self.position_at_byte(byte) {
299 Some(position) => position,
300 None => {
301 let forward = self.next_cluster_byte(byte.min(self.len_bytes()));
302 self.position_at_byte(forward).unwrap_or_else(|| self.end())
303 }
304 }
305 }
306
307 pub fn position_near(&self, row: usize, column: usize) -> Position {
314 let row = row.min(self.line_count().saturating_sub(1));
315 let len = self.line_len_chars(row).unwrap_or(0);
316 let column = column.min(len);
317 (column..=len)
318 .find_map(|c| self.position(row, Column::new(c)))
319 .or_else(|| self.position(row, Column::ZERO))
320 .unwrap_or_else(|| self.start())
321 }
322
323 pub(crate) fn position_at_derived_byte(&self, byte: usize) -> Position {
331 match self.position_at_byte(byte) {
332 Some(position) => position,
333 None => {
334 debug_assert!(false, "derived byte {byte} is not addressable");
335 self.position_at_byte_snapped(byte.min(self.len_bytes()))
336 .unwrap_or_else(|| self.start())
337 }
338 }
339 }
340
341 fn line_slice(&self, row: usize) -> Option<RopeSlice<'_>> {
343 let line = self.rope.get_line(row)?;
344 let chars = line.len_chars();
345 if chars > 0 && line.char(chars - 1) == '\n' {
346 Some(line.slice(..chars - 1))
347 } else {
348 Some(line)
349 }
350 }
351
352 fn position_at_addressable_byte(&self, byte: usize) -> Position {
354 let row = self.rope.byte_to_line(byte);
355 let line_start = self.rope.line_to_byte(row);
356 let column = Column::new(self.rope.byte_slice(line_start..byte).len_chars());
357 Position::new(byte, row, column, self.revision)
358 }
359
360 fn is_char_boundary(&self, byte: usize) -> bool {
361 let len = self.rope.len_bytes();
362 if byte == 0 || byte == len {
363 return true;
364 }
365 if byte > len {
366 return false;
367 }
368 let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(byte);
369 chunk.is_char_boundary(byte - chunk_start)
370 }
371
372 fn is_cluster_boundary(&self, byte: usize) -> bool {
379 let len = self.rope.len_bytes();
380 if byte == 0 || byte == len {
381 return true;
382 }
383 if byte > len || !self.is_char_boundary(byte) {
384 return false;
385 }
386 let mut cursor = GraphemeCursor::new(byte, len, true);
387 let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(byte);
388 for _ in 0..MAX_CONTEXT_REQUESTS {
391 match cursor.is_boundary(chunk, chunk_start) {
392 Ok(is) => return is,
393 Err(GraphemeIncomplete::PreContext(upto)) => {
394 if upto == 0 {
395 return true;
396 }
397 let (pre, pre_start, _, _) = self.rope.chunk_at_byte(upto - 1);
398 cursor.provide_context(pre, pre_start);
399 }
400 Err(_) => return false,
401 }
402 }
403 debug_assert!(false, "grapheme cursor kept asking for context at {byte}");
404 false
405 }
406
407 pub(crate) fn next_cluster_byte(&self, byte: usize) -> usize {
410 self.step_cluster(byte, true)
411 }
412
413 pub(crate) fn prev_cluster_byte(&self, byte: usize) -> usize {
415 self.step_cluster(byte, false)
416 }
417
418 pub(crate) fn scalar_at(&self, byte: usize) -> Option<char> {
424 if byte >= self.rope.len_bytes() {
425 return None;
426 }
427 Some(self.rope.char(self.rope.byte_to_char(byte)))
428 }
429
430 fn step_cluster(&self, byte: usize, forward: bool) -> usize {
438 let len = self.rope.len_bytes();
439 let limit = if forward { len } else { 0 };
440 if byte == limit {
441 return limit;
442 }
443 let mut back = WINDOW_BYTES;
444 let mut ahead = WINDOW_BYTES;
445 loop {
446 let low = self.char_boundary_at_or_before(byte.saturating_sub(back));
447 let high = self.char_boundary_at_or_after((byte + ahead).min(len));
448 let window = cow_of(self.rope.byte_slice(low..high));
449 let mut cursor = GraphemeCursor::new(byte, len, true);
450 let step = if forward {
451 cursor.next_boundary(&window, low)
452 } else {
453 cursor.prev_boundary(&window, low)
454 };
455 match step {
456 Ok(Some(at)) => return at,
457 Ok(None) => return limit,
458 Err(GraphemeIncomplete::NextChunk) => ahead *= 4,
459 Err(GraphemeIncomplete::PreContext(_) | GraphemeIncomplete::PrevChunk) => back *= 4,
460 Err(_) => return limit,
461 }
462 if low == 0 && high == len {
463 debug_assert!(false, "grapheme cursor wants context beyond the text");
466 return byte;
467 }
468 }
469 }
470
471 fn char_boundary_at_or_before(&self, byte: usize) -> usize {
472 let mut at = byte.min(self.rope.len_bytes());
473 while !self.is_char_boundary(at) {
474 at -= 1;
475 }
476 at
477 }
478
479 fn char_boundary_at_or_after(&self, byte: usize) -> usize {
480 let mut at = byte.min(self.rope.len_bytes());
481 while !self.is_char_boundary(at) {
482 at += 1;
483 }
484 at
485 }
486
487 fn cluster_start_at_or_before(&self, byte: usize) -> usize {
494 let row = self.rope.byte_to_line(byte);
495 let row_start = self.rope.line_to_byte(row);
496 let line = cow_of(self.rope.line(row));
497 let offset = byte - row_start;
498 let mut start = 0;
499 for (at, _) in line.grapheme_indices(true) {
500 if at > offset {
501 break;
502 }
503 start = at;
504 }
505 row_start + start
506 }
507}
508
509const MAX_CONTEXT_REQUESTS: usize = 64;
511
512const WINDOW_BYTES: usize = 64;
516
517fn cow_of(slice: RopeSlice<'_>) -> Cow<'_, str> {
518 match slice.as_str() {
519 Some(s) => Cow::Borrowed(s),
520 None => Cow::Owned(slice.to_string()),
521 }
522}
523
524fn normalise_breaks(s: &str) -> Cow<'_, str> {
527 if !s.contains('\r') {
528 return Cow::Borrowed(s);
529 }
530 let mut out = String::with_capacity(s.len());
531 let mut chars = s.chars().peekable();
532 while let Some(c) = chars.next() {
533 if c == '\r' {
534 if chars.peek() == Some(&'\n') {
535 chars.next();
536 }
537 out.push('\n');
538 } else {
539 out.push(c);
540 }
541 }
542 Cow::Owned(out)
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 const COMBINING: &str = "e\u{301}f";
551 const FAMILY: &str = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
553
554 fn col(n: usize) -> Column {
555 Column::new(n)
556 }
557
558 #[test]
561 #[should_panic(expected = "is inside a character")]
562 fn splicing_inside_a_character_is_refused() {
563 let mut t = Text::from("héllo");
566 t.splice(2..3, "");
567 }
568
569 #[test]
570 #[should_panic(expected = "is inside a character")]
571 fn splicing_that_ends_inside_a_character_is_refused() {
572 let mut t = Text::from("héllo");
573 t.splice(1..2, "");
574 }
575
576 #[test]
577 #[should_panic(expected = "is inverted")]
578 fn splicing_an_inverted_range_is_refused() {
579 let mut t = Text::from("hello");
580 let (start, end) = (3usize, 1usize);
583 t.splice(start..end, "");
584 }
585
586 #[test]
587 #[should_panic(expected = "runs past the text's")]
588 fn splicing_past_the_end_is_refused() {
589 let mut t = Text::from("hello");
590 t.splice(4..9, "");
591 }
592
593 #[test]
594 fn splicing_at_the_very_end_is_allowed() {
595 let mut t = Text::from("hello");
597 t.splice(5..5, "!");
598 assert_eq!(t.line(0).expect("one row"), "hello!");
599 }
600
601 #[test]
604 fn empty_text_has_one_empty_row() {
605 let t = Text::new();
606 assert_eq!(t.line_count(), 1);
607 assert_eq!(t.line(0).as_deref(), Some(""));
608 assert_eq!(t.len_bytes(), 0);
609 }
610
611 #[test]
612 fn trailing_newline_opens_a_final_empty_row() {
613 let t = Text::from("a\n");
614 assert_eq!(t.line_count(), 2);
615 assert_eq!(t.line(1).as_deref(), Some(""));
616 }
617
618 #[test]
619 fn no_trailing_newline_is_distinguishable_from_one() {
620 assert_eq!(Text::from("a").line_count(), 1);
621 assert_eq!(Text::from("a\n").line_count(), 2);
622 assert_eq!(Text::from("a").to_string(), "a");
623 assert_eq!(Text::from("a\n").to_string(), "a\n");
624 }
625
626 #[test]
627 fn lines_come_back_without_their_break() {
628 let t = Text::from("one\ntwo\nthree");
629 assert_eq!(
630 t.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
631 ["one", "two", "three"]
632 );
633 }
634
635 #[test]
636 fn line_past_the_end_is_none() {
637 let t = Text::from("one\ntwo");
638 assert!(t.line(2).is_none());
639 assert!(t.position(2, col(0)).is_none());
640 }
641
642 #[test]
645 fn crlf_normalises_and_leaves_no_carriage_return() {
646 let t = Text::from("a\r\nb\r\n");
647 assert_eq!(t.to_string(), "a\nb\n");
648 assert_eq!(t.line(0).as_deref(), Some("a"));
649 assert!(!t.to_string().contains('\r'));
650 }
651
652 #[test]
653 fn lone_carriage_return_is_a_line_break() {
654 let t = Text::from("a\rb");
655 assert_eq!(t.line_count(), 2);
656 assert_eq!(t.line(1).as_deref(), Some("b"));
657 }
658
659 #[test]
660 fn only_a_newline_breaks_a_row() {
661 for exotic in ["\u{b}", "\u{c}", "\u{85}", "\u{2028}", "\u{2029}"] {
665 let t = Text::from(format!("a{exotic}b").as_str());
666 assert_eq!(
667 t.line_count(),
668 1,
669 "{exotic:?} must be an ordinary character, not a break"
670 );
671 }
672 }
673
674 #[test]
677 fn end_of_row_is_addressable_but_past_it_is_not() {
678 let t = Text::from("hello\nworld");
679 assert!(t.position(0, col(5)).is_some());
680 assert!(t.position(0, col(6)).is_none());
681 }
682
683 #[test]
684 fn column_is_chars_and_byte_is_bytes() {
685 let t = Text::from("w\u{f8}rld"); let p = t.position(0, col(2)).expect("char 2 is addressable");
687 assert_eq!(p.column().get(), 2);
688 assert_eq!(p.byte(), 3);
689 }
690
691 #[test]
692 fn row_and_column_survive_the_round_trip_through_byte() {
693 let t = Text::from("one\ntw\u{f8}\nthree");
694 let p = t.position(1, col(3)).expect("end of row 1");
695 let q = t.position_at_byte(p.byte()).expect("same place by byte");
696 assert_eq!((q.row(), q.column().get()), (1, 3));
697 }
698
699 #[test]
700 fn a_position_inside_a_character_is_refused() {
701 let t = Text::from("w\u{f8}rld");
702 assert!(t.position_at_byte(2).is_none(), "byte 2 splits ø");
703 }
704
705 #[test]
706 fn a_position_inside_a_cluster_is_refused() {
707 let t = Text::from(COMBINING);
708 assert!(t.position(0, col(1)).is_none());
711 assert!(t.position(0, col(0)).is_some());
712 assert!(t.position(0, col(2)).is_some());
713 }
714
715 #[test]
716 fn a_position_inside_a_zwj_sequence_is_refused() {
717 let t = Text::from(FAMILY);
718 assert!(t.position(0, col(0)).is_some());
719 for interior in 1..5 {
720 assert!(
721 t.position(0, col(interior)).is_none(),
722 "char {interior} is inside the family cluster"
723 );
724 }
725 assert!(t.position(0, col(5)).is_some(), "past the whole cluster");
726 }
727
728 #[test]
729 fn start_and_end_address_the_whole_text() {
730 let t = Text::from("one\ntwo");
731 assert_eq!(t.start().byte(), 0);
732 assert_eq!(t.end().byte(), 7);
733 assert_eq!((t.end().row(), t.end().column().get()), (1, 3));
734 }
735
736 #[test]
737 fn end_of_a_text_ending_in_a_newline_is_the_empty_row() {
738 let t = Text::from("a\n");
739 assert_eq!((t.end().row(), t.end().column().get()), (1, 0));
740 }
741
742 #[test]
745 fn snapping_lands_on_the_start_of_the_cluster() {
746 let t = Text::from(COMBINING);
747 let acute_start = 1; let p = t
749 .position_at_byte_snapped(acute_start)
750 .expect("inside the text");
751 assert_eq!(p.byte(), 0, "snapped back to the start of the cluster");
752 }
753
754 #[test]
755 fn snapping_a_valid_position_changes_nothing() {
756 let t = Text::from("hello");
757 let p = t.position_at_byte_snapped(3).expect("inside the text");
758 assert_eq!(p.byte(), 3);
759 }
760
761 #[test]
762 fn snapping_inside_a_character_lands_on_the_character() {
763 let t = Text::from("w\u{f8}rld");
764 let p = t.position_at_byte_snapped(2).expect("inside the text");
765 assert_eq!(p.byte(), 1);
766 }
767
768 #[test]
769 fn snapping_past_the_end_is_still_refused() {
770 let t = Text::from("hello");
771 assert!(t.position_at_byte_snapped(6).is_none());
772 }
773
774 #[test]
777 fn two_texts_never_share_a_revision() {
778 let a = Text::from("same");
779 let b = Text::from("same");
780 assert_ne!(a.revision(), b.revision());
781 }
782
783 #[test]
784 fn a_clone_is_the_same_text_and_keeps_its_revision() {
785 let a = Text::from("shared");
786 let b = a.clone();
787 assert_eq!(a.revision(), b.revision());
788 assert!(!b.is_stale(a.start()));
789 }
790
791 #[test]
792 fn a_position_from_another_text_is_stale() {
793 let a = Text::from("hello");
794 let b = Text::from("hello");
795 let p = a.position(0, col(2)).expect("addressable in a");
796 assert!(b.is_stale(p));
797 assert!(b.span(p, b.start()).is_none());
798 assert!(b.slice(a.full_span()).is_none());
799 }
800
801 #[test]
804 fn a_span_comes_back_ordered() {
805 let t = Text::from("hello");
806 let a = t.position(0, col(1)).unwrap();
807 let b = t.position(0, col(4)).unwrap();
808 let forward = t.span(a, b).unwrap();
809 let backward = t.span(b, a).unwrap();
810 assert_eq!(forward, backward);
811 assert_eq!(forward.byte_range(), 1..4);
812 }
813
814 #[test]
815 fn slicing_a_span_reads_the_text_between_its_ends() {
816 let t = Text::from("one\ntwo\nthree");
817 let a = t.position(0, col(1)).unwrap();
818 let b = t.position(2, col(2)).unwrap();
819 let span = t.span(a, b).unwrap();
820 assert_eq!(t.slice(span).as_deref(), Some("ne\ntwo\nth"));
821 }
822
823 #[test]
824 fn an_empty_span_says_so() {
825 let t = Text::from("hello");
826 let p = t.position(0, col(2)).unwrap();
827 assert!(t.span(p, p).unwrap().is_empty());
828 }
829
830 #[test]
831 fn the_full_span_is_the_whole_text() {
832 let t = Text::from("one\ntwo");
833 assert_eq!(t.slice(t.full_span()).as_deref(), Some("one\ntwo"));
834 }
835
836 mod properties {
839 use super::*;
840 use proptest::prelude::*;
841
842 fn cluster_boundaries(s: &str) -> Vec<usize> {
844 let mut out: Vec<usize> = s.grapheme_indices(true).map(|(i, _)| i).collect();
845 out.push(s.len());
846 out
847 }
848
849 proptest! {
850 #[test]
853 fn addressable_bytes_are_exactly_the_cluster_boundaries(s in ".{0,200}") {
854 let normalised = normalise_breaks(&s).into_owned();
855 let t = Text::from(normalised.as_str());
856 let expected = cluster_boundaries(&normalised);
857 for byte in 0..=normalised.len() {
858 let got = t.position_at_byte(byte).is_some();
859 prop_assert_eq!(
860 got,
861 expected.contains(&byte),
862 "byte {} of {:?}", byte, normalised
863 );
864 }
865 }
866
867 #[test]
870 fn snapping_lands_on_a_boundary_at_or_before(s in ".{0,200}") {
871 let normalised = normalise_breaks(&s).into_owned();
872 let t = Text::from(normalised.as_str());
873 let boundaries = cluster_boundaries(&normalised);
874 for byte in 0..=normalised.len() {
875 let p = t.position_at_byte_snapped(byte).expect("inside the text");
876 prop_assert!(p.byte() <= byte);
877 prop_assert!(boundaries.contains(&p.byte()));
878 if boundaries.contains(&byte) {
879 prop_assert_eq!(p.byte(), byte);
880 }
881 }
882 }
883
884 #[test]
886 fn row_column_and_byte_agree(s in ".{0,200}") {
887 let normalised = normalise_breaks(&s).into_owned();
888 let t = Text::from(normalised.as_str());
889 for byte in cluster_boundaries(&normalised) {
890 let by_byte = t.position_at_byte(byte).expect("a boundary is addressable");
891 let by_col = t
892 .position(by_byte.row(), by_byte.column())
893 .expect("its own row and column are addressable");
894 prop_assert_eq!(by_col, by_byte);
895 }
896 }
897
898 #[test]
901 fn rows_rejoin_into_the_text(s in ".{0,200}") {
902 let normalised = normalise_breaks(&s).into_owned();
903 let t = Text::from(normalised.as_str());
904 let rejoined = t.lines().collect::<Vec<_>>().join("\n");
905 prop_assert_eq!(rejoined, normalised);
906 }
907 }
908 }
909}