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(crate) fn position_at_derived_byte(&self, byte: usize) -> Position {
315 match self.position_at_byte(byte) {
316 Some(position) => position,
317 None => {
318 debug_assert!(false, "derived byte {byte} is not addressable");
319 self.position_at_byte_snapped(byte.min(self.len_bytes()))
320 .unwrap_or_else(|| self.start())
321 }
322 }
323 }
324
325 fn line_slice(&self, row: usize) -> Option<RopeSlice<'_>> {
327 let line = self.rope.get_line(row)?;
328 let chars = line.len_chars();
329 if chars > 0 && line.char(chars - 1) == '\n' {
330 Some(line.slice(..chars - 1))
331 } else {
332 Some(line)
333 }
334 }
335
336 fn position_at_addressable_byte(&self, byte: usize) -> Position {
338 let row = self.rope.byte_to_line(byte);
339 let line_start = self.rope.line_to_byte(row);
340 let column = Column::new(self.rope.byte_slice(line_start..byte).len_chars());
341 Position::new(byte, row, column, self.revision)
342 }
343
344 fn is_char_boundary(&self, byte: usize) -> bool {
345 let len = self.rope.len_bytes();
346 if byte == 0 || byte == len {
347 return true;
348 }
349 if byte > len {
350 return false;
351 }
352 let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(byte);
353 chunk.is_char_boundary(byte - chunk_start)
354 }
355
356 fn is_cluster_boundary(&self, byte: usize) -> bool {
363 let len = self.rope.len_bytes();
364 if byte == 0 || byte == len {
365 return true;
366 }
367 if byte > len || !self.is_char_boundary(byte) {
368 return false;
369 }
370 let mut cursor = GraphemeCursor::new(byte, len, true);
371 let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(byte);
372 for _ in 0..MAX_CONTEXT_REQUESTS {
375 match cursor.is_boundary(chunk, chunk_start) {
376 Ok(is) => return is,
377 Err(GraphemeIncomplete::PreContext(upto)) => {
378 if upto == 0 {
379 return true;
380 }
381 let (pre, pre_start, _, _) = self.rope.chunk_at_byte(upto - 1);
382 cursor.provide_context(pre, pre_start);
383 }
384 Err(_) => return false,
385 }
386 }
387 debug_assert!(false, "grapheme cursor kept asking for context at {byte}");
388 false
389 }
390
391 pub(crate) fn next_cluster_byte(&self, byte: usize) -> usize {
394 self.step_cluster(byte, true)
395 }
396
397 pub(crate) fn prev_cluster_byte(&self, byte: usize) -> usize {
399 self.step_cluster(byte, false)
400 }
401
402 pub(crate) fn scalar_at(&self, byte: usize) -> Option<char> {
408 if byte >= self.rope.len_bytes() {
409 return None;
410 }
411 Some(self.rope.char(self.rope.byte_to_char(byte)))
412 }
413
414 fn step_cluster(&self, byte: usize, forward: bool) -> usize {
422 let len = self.rope.len_bytes();
423 let limit = if forward { len } else { 0 };
424 if byte == limit {
425 return limit;
426 }
427 let mut back = WINDOW_BYTES;
428 let mut ahead = WINDOW_BYTES;
429 loop {
430 let low = self.char_boundary_at_or_before(byte.saturating_sub(back));
431 let high = self.char_boundary_at_or_after((byte + ahead).min(len));
432 let window = cow_of(self.rope.byte_slice(low..high));
433 let mut cursor = GraphemeCursor::new(byte, len, true);
434 let step = if forward {
435 cursor.next_boundary(&window, low)
436 } else {
437 cursor.prev_boundary(&window, low)
438 };
439 match step {
440 Ok(Some(at)) => return at,
441 Ok(None) => return limit,
442 Err(GraphemeIncomplete::NextChunk) => ahead *= 4,
443 Err(GraphemeIncomplete::PreContext(_) | GraphemeIncomplete::PrevChunk) => back *= 4,
444 Err(_) => return limit,
445 }
446 if low == 0 && high == len {
447 debug_assert!(false, "grapheme cursor wants context beyond the text");
450 return byte;
451 }
452 }
453 }
454
455 fn char_boundary_at_or_before(&self, byte: usize) -> usize {
456 let mut at = byte.min(self.rope.len_bytes());
457 while !self.is_char_boundary(at) {
458 at -= 1;
459 }
460 at
461 }
462
463 fn char_boundary_at_or_after(&self, byte: usize) -> usize {
464 let mut at = byte.min(self.rope.len_bytes());
465 while !self.is_char_boundary(at) {
466 at += 1;
467 }
468 at
469 }
470
471 fn cluster_start_at_or_before(&self, byte: usize) -> usize {
478 let row = self.rope.byte_to_line(byte);
479 let row_start = self.rope.line_to_byte(row);
480 let line = cow_of(self.rope.line(row));
481 let offset = byte - row_start;
482 let mut start = 0;
483 for (at, _) in line.grapheme_indices(true) {
484 if at > offset {
485 break;
486 }
487 start = at;
488 }
489 row_start + start
490 }
491}
492
493const MAX_CONTEXT_REQUESTS: usize = 64;
495
496const WINDOW_BYTES: usize = 64;
500
501fn cow_of(slice: RopeSlice<'_>) -> Cow<'_, str> {
502 match slice.as_str() {
503 Some(s) => Cow::Borrowed(s),
504 None => Cow::Owned(slice.to_string()),
505 }
506}
507
508fn normalise_breaks(s: &str) -> Cow<'_, str> {
511 if !s.contains('\r') {
512 return Cow::Borrowed(s);
513 }
514 let mut out = String::with_capacity(s.len());
515 let mut chars = s.chars().peekable();
516 while let Some(c) = chars.next() {
517 if c == '\r' {
518 if chars.peek() == Some(&'\n') {
519 chars.next();
520 }
521 out.push('\n');
522 } else {
523 out.push(c);
524 }
525 }
526 Cow::Owned(out)
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 const COMBINING: &str = "e\u{301}f";
535 const FAMILY: &str = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
537
538 fn col(n: usize) -> Column {
539 Column::new(n)
540 }
541
542 #[test]
545 #[should_panic(expected = "is inside a character")]
546 fn splicing_inside_a_character_is_refused() {
547 let mut t = Text::from("héllo");
550 t.splice(2..3, "");
551 }
552
553 #[test]
554 #[should_panic(expected = "is inside a character")]
555 fn splicing_that_ends_inside_a_character_is_refused() {
556 let mut t = Text::from("héllo");
557 t.splice(1..2, "");
558 }
559
560 #[test]
561 #[should_panic(expected = "is inverted")]
562 fn splicing_an_inverted_range_is_refused() {
563 let mut t = Text::from("hello");
564 let (start, end) = (3usize, 1usize);
567 t.splice(start..end, "");
568 }
569
570 #[test]
571 #[should_panic(expected = "runs past the text's")]
572 fn splicing_past_the_end_is_refused() {
573 let mut t = Text::from("hello");
574 t.splice(4..9, "");
575 }
576
577 #[test]
578 fn splicing_at_the_very_end_is_allowed() {
579 let mut t = Text::from("hello");
581 t.splice(5..5, "!");
582 assert_eq!(t.line(0).expect("one row"), "hello!");
583 }
584
585 #[test]
588 fn empty_text_has_one_empty_row() {
589 let t = Text::new();
590 assert_eq!(t.line_count(), 1);
591 assert_eq!(t.line(0).as_deref(), Some(""));
592 assert_eq!(t.len_bytes(), 0);
593 }
594
595 #[test]
596 fn trailing_newline_opens_a_final_empty_row() {
597 let t = Text::from("a\n");
598 assert_eq!(t.line_count(), 2);
599 assert_eq!(t.line(1).as_deref(), Some(""));
600 }
601
602 #[test]
603 fn no_trailing_newline_is_distinguishable_from_one() {
604 assert_eq!(Text::from("a").line_count(), 1);
605 assert_eq!(Text::from("a\n").line_count(), 2);
606 assert_eq!(Text::from("a").to_string(), "a");
607 assert_eq!(Text::from("a\n").to_string(), "a\n");
608 }
609
610 #[test]
611 fn lines_come_back_without_their_break() {
612 let t = Text::from("one\ntwo\nthree");
613 assert_eq!(
614 t.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
615 ["one", "two", "three"]
616 );
617 }
618
619 #[test]
620 fn line_past_the_end_is_none() {
621 let t = Text::from("one\ntwo");
622 assert!(t.line(2).is_none());
623 assert!(t.position(2, col(0)).is_none());
624 }
625
626 #[test]
629 fn crlf_normalises_and_leaves_no_carriage_return() {
630 let t = Text::from("a\r\nb\r\n");
631 assert_eq!(t.to_string(), "a\nb\n");
632 assert_eq!(t.line(0).as_deref(), Some("a"));
633 assert!(!t.to_string().contains('\r'));
634 }
635
636 #[test]
637 fn lone_carriage_return_is_a_line_break() {
638 let t = Text::from("a\rb");
639 assert_eq!(t.line_count(), 2);
640 assert_eq!(t.line(1).as_deref(), Some("b"));
641 }
642
643 #[test]
644 fn only_a_newline_breaks_a_row() {
645 for exotic in ["\u{b}", "\u{c}", "\u{85}", "\u{2028}", "\u{2029}"] {
649 let t = Text::from(format!("a{exotic}b").as_str());
650 assert_eq!(
651 t.line_count(),
652 1,
653 "{exotic:?} must be an ordinary character, not a break"
654 );
655 }
656 }
657
658 #[test]
661 fn end_of_row_is_addressable_but_past_it_is_not() {
662 let t = Text::from("hello\nworld");
663 assert!(t.position(0, col(5)).is_some());
664 assert!(t.position(0, col(6)).is_none());
665 }
666
667 #[test]
668 fn column_is_chars_and_byte_is_bytes() {
669 let t = Text::from("w\u{f8}rld"); let p = t.position(0, col(2)).expect("char 2 is addressable");
671 assert_eq!(p.column().get(), 2);
672 assert_eq!(p.byte(), 3);
673 }
674
675 #[test]
676 fn row_and_column_survive_the_round_trip_through_byte() {
677 let t = Text::from("one\ntw\u{f8}\nthree");
678 let p = t.position(1, col(3)).expect("end of row 1");
679 let q = t.position_at_byte(p.byte()).expect("same place by byte");
680 assert_eq!((q.row(), q.column().get()), (1, 3));
681 }
682
683 #[test]
684 fn a_position_inside_a_character_is_refused() {
685 let t = Text::from("w\u{f8}rld");
686 assert!(t.position_at_byte(2).is_none(), "byte 2 splits ø");
687 }
688
689 #[test]
690 fn a_position_inside_a_cluster_is_refused() {
691 let t = Text::from(COMBINING);
692 assert!(t.position(0, col(1)).is_none());
695 assert!(t.position(0, col(0)).is_some());
696 assert!(t.position(0, col(2)).is_some());
697 }
698
699 #[test]
700 fn a_position_inside_a_zwj_sequence_is_refused() {
701 let t = Text::from(FAMILY);
702 assert!(t.position(0, col(0)).is_some());
703 for interior in 1..5 {
704 assert!(
705 t.position(0, col(interior)).is_none(),
706 "char {interior} is inside the family cluster"
707 );
708 }
709 assert!(t.position(0, col(5)).is_some(), "past the whole cluster");
710 }
711
712 #[test]
713 fn start_and_end_address_the_whole_text() {
714 let t = Text::from("one\ntwo");
715 assert_eq!(t.start().byte(), 0);
716 assert_eq!(t.end().byte(), 7);
717 assert_eq!((t.end().row(), t.end().column().get()), (1, 3));
718 }
719
720 #[test]
721 fn end_of_a_text_ending_in_a_newline_is_the_empty_row() {
722 let t = Text::from("a\n");
723 assert_eq!((t.end().row(), t.end().column().get()), (1, 0));
724 }
725
726 #[test]
729 fn snapping_lands_on_the_start_of_the_cluster() {
730 let t = Text::from(COMBINING);
731 let acute_start = 1; let p = t
733 .position_at_byte_snapped(acute_start)
734 .expect("inside the text");
735 assert_eq!(p.byte(), 0, "snapped back to the start of the cluster");
736 }
737
738 #[test]
739 fn snapping_a_valid_position_changes_nothing() {
740 let t = Text::from("hello");
741 let p = t.position_at_byte_snapped(3).expect("inside the text");
742 assert_eq!(p.byte(), 3);
743 }
744
745 #[test]
746 fn snapping_inside_a_character_lands_on_the_character() {
747 let t = Text::from("w\u{f8}rld");
748 let p = t.position_at_byte_snapped(2).expect("inside the text");
749 assert_eq!(p.byte(), 1);
750 }
751
752 #[test]
753 fn snapping_past_the_end_is_still_refused() {
754 let t = Text::from("hello");
755 assert!(t.position_at_byte_snapped(6).is_none());
756 }
757
758 #[test]
761 fn two_texts_never_share_a_revision() {
762 let a = Text::from("same");
763 let b = Text::from("same");
764 assert_ne!(a.revision(), b.revision());
765 }
766
767 #[test]
768 fn a_clone_is_the_same_text_and_keeps_its_revision() {
769 let a = Text::from("shared");
770 let b = a.clone();
771 assert_eq!(a.revision(), b.revision());
772 assert!(!b.is_stale(a.start()));
773 }
774
775 #[test]
776 fn a_position_from_another_text_is_stale() {
777 let a = Text::from("hello");
778 let b = Text::from("hello");
779 let p = a.position(0, col(2)).expect("addressable in a");
780 assert!(b.is_stale(p));
781 assert!(b.span(p, b.start()).is_none());
782 assert!(b.slice(a.full_span()).is_none());
783 }
784
785 #[test]
788 fn a_span_comes_back_ordered() {
789 let t = Text::from("hello");
790 let a = t.position(0, col(1)).unwrap();
791 let b = t.position(0, col(4)).unwrap();
792 let forward = t.span(a, b).unwrap();
793 let backward = t.span(b, a).unwrap();
794 assert_eq!(forward, backward);
795 assert_eq!(forward.byte_range(), 1..4);
796 }
797
798 #[test]
799 fn slicing_a_span_reads_the_text_between_its_ends() {
800 let t = Text::from("one\ntwo\nthree");
801 let a = t.position(0, col(1)).unwrap();
802 let b = t.position(2, col(2)).unwrap();
803 let span = t.span(a, b).unwrap();
804 assert_eq!(t.slice(span).as_deref(), Some("ne\ntwo\nth"));
805 }
806
807 #[test]
808 fn an_empty_span_says_so() {
809 let t = Text::from("hello");
810 let p = t.position(0, col(2)).unwrap();
811 assert!(t.span(p, p).unwrap().is_empty());
812 }
813
814 #[test]
815 fn the_full_span_is_the_whole_text() {
816 let t = Text::from("one\ntwo");
817 assert_eq!(t.slice(t.full_span()).as_deref(), Some("one\ntwo"));
818 }
819
820 mod properties {
823 use super::*;
824 use proptest::prelude::*;
825
826 fn cluster_boundaries(s: &str) -> Vec<usize> {
828 let mut out: Vec<usize> = s.grapheme_indices(true).map(|(i, _)| i).collect();
829 out.push(s.len());
830 out
831 }
832
833 proptest! {
834 #[test]
837 fn addressable_bytes_are_exactly_the_cluster_boundaries(s in ".{0,200}") {
838 let normalised = normalise_breaks(&s).into_owned();
839 let t = Text::from(normalised.as_str());
840 let expected = cluster_boundaries(&normalised);
841 for byte in 0..=normalised.len() {
842 let got = t.position_at_byte(byte).is_some();
843 prop_assert_eq!(
844 got,
845 expected.contains(&byte),
846 "byte {} of {:?}", byte, normalised
847 );
848 }
849 }
850
851 #[test]
854 fn snapping_lands_on_a_boundary_at_or_before(s in ".{0,200}") {
855 let normalised = normalise_breaks(&s).into_owned();
856 let t = Text::from(normalised.as_str());
857 let boundaries = cluster_boundaries(&normalised);
858 for byte in 0..=normalised.len() {
859 let p = t.position_at_byte_snapped(byte).expect("inside the text");
860 prop_assert!(p.byte() <= byte);
861 prop_assert!(boundaries.contains(&p.byte()));
862 if boundaries.contains(&byte) {
863 prop_assert_eq!(p.byte(), byte);
864 }
865 }
866 }
867
868 #[test]
870 fn row_column_and_byte_agree(s in ".{0,200}") {
871 let normalised = normalise_breaks(&s).into_owned();
872 let t = Text::from(normalised.as_str());
873 for byte in cluster_boundaries(&normalised) {
874 let by_byte = t.position_at_byte(byte).expect("a boundary is addressable");
875 let by_col = t
876 .position(by_byte.row(), by_byte.column())
877 .expect("its own row and column are addressable");
878 prop_assert_eq!(by_col, by_byte);
879 }
880 }
881
882 #[test]
885 fn rows_rejoin_into_the_text(s in ".{0,200}") {
886 let normalised = normalise_breaks(&s).into_owned();
887 let t = Text::from(normalised.as_str());
888 let rejoined = t.lines().collect::<Vec<_>>().join("\n");
889 prop_assert_eq!(rejoined, normalised);
890 }
891 }
892 }
893}