Skip to main content

kimun_notes/ropetext/
text.rs

1//! The text value: a rope, a revision, and the only way to make a [`Position`].
2
3use 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/// A text, as a value.
12///
13/// `Clone` is cheap: the rope shares its structure, so a clone is not a copy of
14/// the text but *the same text*, held elsewhere. That is what lets a background
15/// task, a history entry or a preview hold one without anybody duplicating a
16/// buffer, and it is why a clone keeps the same [`Revision`] — only a change
17/// mints a new one.
18///
19/// The text never contains a carriage return. A `\r\n` or a lone `\r` in the
20/// input becomes `\n` on construction, so every layer above measures, wraps and
21/// addresses exactly one kind of line break. Restoring a file's original line
22/// endings on save is the caller's business; it has the file, this does not.
23#[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    /// Takes `s` as the text's content, normalising `\r\n` and lone `\r` to `\n`.
46    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    /// An empty text: one row, zero characters.
56    pub fn new() -> Self {
57        Self {
58            rope: Rope::new(),
59            revision: Revision::fresh(),
60        }
61    }
62
63    /// Which state this text is. Every [`Position`] carries the revision it was
64    /// built against, so one made against an earlier state is refused rather
65    /// than read as some other place.
66    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    /// Number of logical rows. A trailing newline opens a final empty row, so
79    /// `"a\n"` is two rows and the cursor can sit on the second.
80    pub fn line_count(&self) -> usize {
81        self.rope.len_lines()
82    }
83
84    /// Row `row` without its line break, or `None` if there is no such row.
85    ///
86    /// Borrowed when the row sits inside one of the rope's chunks, owned when it
87    /// straddles two. Nothing above holds a duplicate of the document, so a full
88    /// pass over the text costs what it costs and a partial pass costs less.
89    pub fn line(&self, row: usize) -> Option<Cow<'_, str>> {
90        self.line_slice(row).map(cow_of)
91    }
92
93    /// Length of row `row` in Unicode scalars, excluding its line break.
94    pub fn line_len_chars(&self, row: usize) -> Option<usize> {
95        self.line_slice(row).map(|l| l.len_chars())
96    }
97
98    /// Every row, without line breaks.
99    pub fn lines(&self) -> impl Iterator<Item = Cow<'_, str>> {
100        (0..self.line_count()).filter_map(|row| self.line(row))
101    }
102
103    /// The text within `span`.
104    ///
105    /// `None` when the span addresses another revision of the text.
106    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    /// Whether `position` addresses a state this text has moved on from.
114    pub fn is_stale(&self, position: Position) -> bool {
115        position.revision() != self.revision
116    }
117
118    /// The position at `(row, column)`, or `None` if this text cannot address it.
119    ///
120    /// Refused rather than approximated: past the end of the row, past the last
121    /// row, or partway through a grapheme cluster all yield `None`. A caller
122    /// that gets `None` has asked for somewhere that does not exist, and a
123    /// keystroke that does nothing is recoverable in a way one that edits the
124    /// wrong place is not.
125    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    /// The position at byte offset `byte`, or `None` if that offset is past the
138    /// end, inside a character, or inside a grapheme cluster.
139    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    /// The position at the start of the grapheme cluster containing `byte`.
150    ///
151    /// This one **approximates**, which is why it says so in its name. It exists
152    /// for offsets that arrive from somewhere with a different idea of where a
153    /// character ends — an external editor reporting a cursor in bytes, say —
154    /// where refusing would drop the update entirely. Anything originating
155    /// inside this crate should use [`Self::position_at_byte`] and be told it was
156    /// wrong.
157    ///
158    /// `None` only when `byte` is past the end of the text.
159    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    /// The first position in the text.
174    pub fn start(&self) -> Position {
175        Position::new(0, 0, Column::ZERO, self.revision)
176    }
177
178    /// The position just past the last character.
179    pub fn end(&self) -> Position {
180        self.position_at_addressable_byte(self.rope.len_bytes())
181    }
182
183    /// The whole text as one span.
184    pub fn full_span(&self) -> Span {
185        Span::new(self.start(), self.end())
186    }
187
188    /// An ordered span between two positions of *this* text.
189    ///
190    /// Order does not matter: the span comes back with its ends the right way
191    /// round. `None` when either position addresses another revision — which is
192    /// also the only place two positions are checked against each other, and the
193    /// reason [`Position`] is not `Ord`.
194    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    // -- internals ----------------------------------------------------------
206
207    /// Replace `bytes` with `text`, becoming a new revision.
208    ///
209    /// `bytes` must be a byte range this text can address; the buffer only ever
210    /// derives it from [`Span`]s, which are checked. Inserted text is normalised
211    /// like any other, so a paste carrying `\r\n` cannot smuggle a carriage
212    /// return past the invariant.
213    /// Returns how many bytes were inserted, which is not `text.len()` when the
214    /// normalisation collapsed a `\r\n`.
215    ///
216    /// The precondition is asserted rather than trusted, because this is the one
217    /// place the text is mutated and the range reaches it as bare arithmetic —
218    /// `Txn` remaps its spans across earlier edits in the same transaction. An
219    /// inverted or out-of-range range would panic inside the rope anyway; a byte
220    /// *inside a character* would not. `byte_to_char` rounds it down, and the
221    /// edit would silently land somewhere the caller never asked for, in a note
222    /// that autosaves. Corrupting the text is worse than refusing to.
223    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    /// Char index at `byte`, which must start a character.
247    ///
248    /// `Rope::byte_to_char` answers for a byte inside one by naming the
249    /// character that contains it, so the round trip back is what distinguishes
250    /// a boundary from an interior byte.
251    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    /// The same content as a new revision.
262    ///
263    /// Undo restores content, not identity: a revision names a point in the edit
264    /// timeline, and going back to earlier content is a *later* point. Keeping
265    /// revisions monotonic is what lets a cache and a background task compare
266    /// two of them without also asking which way history was walking.
267    pub(crate) fn reidentified(&self) -> Self {
268        Self {
269            rope: self.rope.clone(),
270            revision: Revision::fresh(),
271        }
272    }
273
274    /// Row containing `byte`, which must be addressable.
275    pub(crate) fn row_of_byte(&self, byte: usize) -> usize {
276        self.rope.byte_to_line(byte)
277    }
278
279    /// Byte offset where `row` starts.
280    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    /// The position a cursor takes at `byte`, snapping **forward** if the byte
286    /// falls inside a cluster.
287    ///
288    /// Distinct from [`Self::position_at_derived_byte`] because the direction
289    /// matters and the two want opposite ones. An edit's own end offset is a
290    /// char boundary, but char boundaries are not cluster boundaries: typing `a`
291    /// in front of a lone combining acute produces one cluster, and the offset
292    /// between them addresses nothing. Snapping back — which is what
293    /// `position_at_byte_snapped` does, since it names the *enclosing* cluster —
294    /// would leave the cursor in front of the character just typed, so the next
295    /// keystroke would land in reverse order. Forward is the only direction that
296    /// keeps typing moving the way the typist is.
297    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    /// The position at `byte`, snapping if the byte is somehow not addressable.
308    ///
309    /// For offsets this crate derived itself — a remapped cursor, a restored
310    /// history entry. They should always land on a cluster boundary; the assert
311    /// is what surfaces the case that does not, in development rather than in a
312    /// user's note, and the snap is what keeps it from being a panic if it ever
313    /// does.
314    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    /// Row `row` with any trailing `\n` trimmed off.
326    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    /// `byte` is known to be addressable; derive the rest of the position.
337    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    /// Whether `byte` sits between two grapheme clusters.
357    ///
358    /// Answered from the chunk containing `byte`, with preceding context fetched
359    /// only when the segmenter asks for it — so this is a chunk-local operation,
360    /// not a walk from the start of the row. That is what makes it affordable on
361    /// the paths that build a position per overlay per visible row.
362    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        // Each PreContext asks for strictly earlier text, so this terminates;
373        // the bound is a backstop against a segmenter that disagrees.
374        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    /// Byte offset of the next cluster boundary after `byte`, or the end of the
392    /// text.
393    pub(crate) fn next_cluster_byte(&self, byte: usize) -> usize {
394        self.step_cluster(byte, true)
395    }
396
397    /// Byte offset of the previous cluster boundary before `byte`, or zero.
398    pub(crate) fn prev_cluster_byte(&self, byte: usize) -> usize {
399        self.step_cluster(byte, false)
400    }
401
402    /// The first scalar of the cluster starting at `byte`, or `None` at the end
403    /// of the text.
404    ///
405    /// A cluster's class — blank, word, punctuation — is its first scalar's, which
406    /// is what every editor's word motions use.
407    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    /// One cluster boundary in either direction.
415    ///
416    /// Runs the segmenter over a window around `byte` rather than the whole row.
417    /// A cluster is a handful of bytes in practice, so the window almost always
418    /// suffices; when the segmenter says it needs more, it says which side, and
419    /// the window grows. Stepping a row's worth of text to move the cursor one
420    /// place would make an arrow key cost the length of its line.
421    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                // The window is the whole text and the segmenter still wants
448                // more, which it cannot get. Refuse to move rather than guess.
449                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    /// Start of the grapheme cluster containing `byte`, which must be a char
472    /// boundary.
473    ///
474    /// Walks the row rather than the chunk. Unlike [`Self::is_cluster_boundary`]
475    /// this runs at one call site — an offset arriving from outside — so O(row)
476    /// is the right trade for an implementation that is obviously correct.
477    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
493/// Backstop on [`Text::is_cluster_boundary`]'s context loop.
494const MAX_CONTEXT_REQUESTS: usize = 64;
495
496/// Bytes either side of an offset handed to the segmenter to start with. Wide
497/// enough for any cluster that occurs in prose; grown on demand for ones that do
498/// not.
499const 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
508/// `\r\n` and lone `\r` become `\n`. Borrows when there is nothing to do, which
509/// is every note not written on Windows.
510fn 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    /// "e" plus a combining acute: two chars, one cluster.
534    const COMBINING: &str = "e\u{301}f";
535    /// Man-woman-girl family: three scalars joined by two ZWJs, one cluster.
536    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    // -- splice preconditions -----------------------------------------------
543
544    #[test]
545    #[should_panic(expected = "is inside a character")]
546    fn splicing_inside_a_character_is_refused() {
547        // Without the check the rope rounds this down to the start of the `é`
548        // and deletes a character the caller never named.
549        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        // Built from values: a literal `3..1` is a lint, and the point is that
565        // arithmetic can produce one where a literal never would.
566        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        // The boundary the check must not exclude: an append is `len..len`.
580        let mut t = Text::from("hello");
581        t.splice(5..5, "!");
582        assert_eq!(t.line(0).expect("one row"), "hello!");
583    }
584
585    // -- shape --------------------------------------------------------------
586
587    #[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    // -- line endings -------------------------------------------------------
627
628    #[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        // Ropey's default line-break set includes these; ours does not, because
646        // CommonMark's does not and neither does splitting on '\n'. A row here
647        // must be a row to the markdown parser as well.
648        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    // -- positions ----------------------------------------------------------
659
660    #[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"); // "wørld": ø is two bytes
670        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        // char 1 is the combining acute — a place the renderer cannot show a
693        // cursor, so the text refuses to name it.
694        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    // -- snapping -----------------------------------------------------------
727
728    #[test]
729    fn snapping_lands_on_the_start_of_the_cluster() {
730        let t = Text::from(COMBINING);
731        let acute_start = 1; // byte offset of the combining mark
732        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    // -- revisions ----------------------------------------------------------
759
760    #[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    // -- spans --------------------------------------------------------------
786
787    #[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    // -- properties ---------------------------------------------------------
821
822    mod properties {
823        use super::*;
824        use proptest::prelude::*;
825
826        /// Naive reference: every cluster boundary in the text, by byte offset.
827        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            /// A byte offset is addressable exactly when it is a cluster
835            /// boundary. No clamping, no rounding, in either direction.
836            #[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            /// Snapping always lands on a cluster boundary at or before where it
852            /// was asked, and never moves a boundary that was already fine.
853            #[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            /// (row, column) and byte offset name the same places.
869            #[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            /// Rows rejoin into the text, so nothing is lost or invented by the
883            /// break-trimming.
884            #[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}