Skip to main content

kimun_notes/ropetext/
buffer.rs

1//! The edit buffer: text, cursor, selection and history as one thing.
2
3use std::ops::Range;
4
5use crate::ropetext::change::{Change, Edit};
6use crate::ropetext::history::{DEFAULT_BUDGET_BYTES, Entry, History, Shape};
7use crate::ropetext::position::{Position, Revision, Span};
8use crate::ropetext::text::Text;
9
10/// Which side of an insertion a remapped offset ends up on.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum Gravity {
13    /// Text inserted exactly here pushes the offset along. What typing does to a
14    /// cursor.
15    Forward,
16    /// Text inserted exactly here is left in front of the offset. What a caller
17    /// holding a marker wants: the marker stays where it was pointing.
18    Backward,
19}
20
21/// One primitive applied inside a transaction, in the coordinates it was applied
22/// in.
23#[derive(Debug, Clone, Copy)]
24struct Applied {
25    at: usize,
26    removed: usize,
27    inserted: usize,
28    /// The text's revision immediately before this was applied, so a position
29    /// made against that state can be recognised and carried forward.
30    revision_before: Revision,
31}
32
33/// A text with its cursor and selection, taken at one moment.
34///
35/// Cheap: the text shares its structure with the buffer's, so this is not a copy
36/// of the note. Hand one to a background task or keep one for a preview and the
37/// cursor cannot drift away from the text it was read with, because they arrived
38/// together and neither can change.
39#[derive(Debug, Clone)]
40pub struct Snapshot {
41    pub text: Text,
42    pub cursor: Position,
43    pub selection: Option<Span>,
44}
45
46/// The open note's text, cursor, selection and edit history.
47///
48/// Every mutation goes through a transaction ([`Self::begin`]), and a
49/// transaction is exactly one entry in the history. Nothing has to count history
50/// entries, predict how many an operation will push, or reconstruct where a
51/// group started: the entry holds the states it ran between.
52#[derive(Debug)]
53pub struct EditBuffer {
54    text: Text,
55    cursor: Position,
56    anchor: Option<Position>,
57    history: History,
58}
59
60impl Default for EditBuffer {
61    fn default() -> Self {
62        Self::new(Text::new())
63    }
64}
65
66impl EditBuffer {
67    pub fn new(text: Text) -> Self {
68        let cursor = text.start();
69        Self {
70            text,
71            cursor,
72            anchor: None,
73            history: History::new(DEFAULT_BUDGET_BYTES),
74        }
75    }
76
77    pub fn text(&self) -> &Text {
78        &self.text
79    }
80
81    pub fn cursor(&self) -> Position {
82        self.cursor
83    }
84
85    /// The selected range, or `None` when nothing is selected.
86    ///
87    /// The selection lives here rather than beside the buffer so that "moving the
88    /// cursor without extending drops the selection" is a property of the thing
89    /// that owns both, and not a rule every caller has to remember. Reaching for
90    /// a search result and forgetting to drop the anchor is how an invisible
91    /// selection gets deleted by the next keystroke.
92    pub fn selection(&self) -> Option<Span> {
93        let anchor = self.anchor?;
94        self.text.span(anchor, self.cursor)
95    }
96
97    /// Everything a reader needs, consistent by construction.
98    pub fn snapshot(&self) -> Snapshot {
99        Snapshot {
100            text: self.text.clone(),
101            cursor: self.cursor,
102            selection: self.selection(),
103        }
104    }
105
106    /// Move the cursor, dropping any selection.
107    ///
108    /// Returns `false` — and moves nothing — when `to` addresses a state this
109    /// buffer has left. Not an assertion: a position outliving the text it was
110    /// made against is a real thing that happens to real callers, an event
111    /// arriving after a background edit being the obvious way. Refusing it is a
112    /// keystroke that did nothing, which is recoverable in a way editing the
113    /// wrong place is not.
114    pub fn set_cursor(&mut self, to: Position) -> bool {
115        if self.text.is_stale(to) {
116            return false;
117        }
118        self.cursor = to;
119        self.anchor = None;
120        true
121    }
122
123    /// Move the cursor, keeping or starting a selection.
124    ///
125    /// The counterpart of [`Self::set_cursor`], and the reason that one can drop
126    /// the anchor unconditionally: every caller says which it means.
127    pub fn extend_to(&mut self, to: Position) -> bool {
128        if self.text.is_stale(to) {
129            return false;
130        }
131        if self.anchor.is_none() {
132            self.anchor = Some(self.cursor);
133        }
134        self.cursor = to;
135        true
136    }
137
138    /// Select `span`, leaving the cursor at its end.
139    pub fn select(&mut self, span: Span) -> bool {
140        if span.revision() != self.text.revision() {
141            return false;
142        }
143        self.anchor = Some(span.start());
144        self.cursor = span.end();
145        true
146    }
147
148    pub fn clear_selection(&mut self) {
149        self.anchor = None;
150    }
151
152    /// Replace the whole buffer and forget its history.
153    ///
154    /// For opening a different note, not for editing. The history describes
155    /// states the new text cannot reach, so keeping it would let an undo jump
156    /// from one note into another.
157    pub fn set_text(&mut self, text: Text) {
158        self.text = text;
159        self.cursor = self.text.start();
160        self.anchor = None;
161        self.history.clear();
162    }
163
164    /// How many bytes of edit history to retain.
165    pub fn set_history_budget(&mut self, bytes: usize) {
166        self.history.set_budget(bytes);
167    }
168
169    /// Open a transaction that will record its own [undo group].
170    ///
171    /// [undo group]: crate::ropetext::EditBuffer
172    pub fn begin(&mut self) -> Txn<'_> {
173        Txn::new(self, false)
174    }
175
176    /// Open a transaction that folds into the previous group.
177    ///
178    /// For a backend that has decided this edit continues what the last one
179    /// started — the next character of a typing run. Where a group ends is the
180    /// backend's judgement, because only the backend knows what the user was
181    /// doing; this crate keeps no clock and no policy.
182    ///
183    /// Falls back to [`Self::begin`] when there is nothing to fold into.
184    pub fn begin_extending(&mut self) -> Txn<'_> {
185        Txn::new(self, true)
186    }
187
188    /// Undo the newest group, restoring the text, cursor and selection it began
189    /// from.
190    ///
191    /// All of it or none of it. The entry holds the state the group started from,
192    /// so there is no replay to run out of, no count to get wrong, and no way to
193    /// land part-way through an action the user asked to undo whole.
194    pub fn undo(&mut self) -> Option<Change> {
195        let entry = self.history.undo_candidate()?;
196        let text = entry.before.reidentified();
197        let shape = entry.inverse.clone();
198        let cursor = entry.cursor_before;
199        let anchor = entry.anchor_before;
200        self.history.step_back();
201        Some(self.restore(text, shape, cursor, anchor))
202    }
203
204    /// Redo the group a previous [`Self::undo`] took back.
205    pub fn redo(&mut self) -> Option<Change> {
206        let entry = self.history.redo_candidate()?;
207        let text = entry.after.reidentified();
208        let shape = entry.forward.clone();
209        let cursor = entry.cursor_after;
210        let anchor = entry.anchor_after;
211        self.history.step_forward();
212        Some(self.restore(text, shape, cursor, anchor))
213    }
214
215    fn restore(
216        &mut self,
217        text: Text,
218        shape: Shape,
219        cursor: usize,
220        anchor: Option<usize>,
221    ) -> Change {
222        self.text = text;
223        self.cursor = self.text.position_at_derived_byte(cursor);
224        self.anchor = anchor.map(|a| self.text.position_at_derived_byte(a));
225        Change::new(
226            self.text.revision(),
227            shape.edits,
228            shape.rows,
229            shape.line_delta,
230        )
231    }
232}
233
234/// A set of edits that lands as one [undo group](EditBuffer).
235///
236/// Dropping a transaction without committing rolls it back: the buffer returns to
237/// the text, cursor and selection it had when the transaction opened. That is
238/// cheap for the same reason the history is — restoring a text is restoring a
239/// handle — and it means a panic part-way through a compound edit cannot leave
240/// half of one behind.
241#[must_use = "a transaction that is dropped without commit() is rolled back"]
242pub struct Txn<'a> {
243    buffer: &'a mut EditBuffer,
244    before: Text,
245    cursor_before: usize,
246    anchor_before: Option<usize>,
247    applied: Vec<Applied>,
248    /// Retained bytes, accumulated as edits land.
249    retained: usize,
250    extending: bool,
251    committed: bool,
252}
253
254impl<'a> Txn<'a> {
255    fn new(buffer: &'a mut EditBuffer, extending: bool) -> Self {
256        let before = buffer.text.clone();
257        let cursor_before = buffer.cursor.byte();
258        let anchor_before = buffer.anchor.map(Position::byte);
259        Self {
260            buffer,
261            before,
262            cursor_before,
263            anchor_before,
264            applied: Vec::new(),
265            retained: 0,
266            extending,
267            committed: false,
268        }
269    }
270
271    /// The text as it stands part-way through the transaction.
272    pub fn text(&self) -> &Text {
273        &self.buffer.text
274    }
275
276    pub fn cursor(&self) -> Position {
277        self.buffer.cursor
278    }
279
280    pub fn selection(&self) -> Option<Span> {
281        self.buffer.selection()
282    }
283
284    /// Carry a position made earlier in — or before — this transaction forward to
285    /// where it now points.
286    ///
287    /// `None` when the position belongs to neither this transaction nor the state
288    /// it opened from. Text inserted exactly at the position is left in front of
289    /// it, so a marker keeps pointing at what it was pointing at; text deleted
290    /// across it collapses it to the start of what was removed.
291    pub fn map(&self, position: Position) -> Option<Position> {
292        let byte = self.remap(position, Gravity::Backward)?;
293        Some(self.buffer.text.position_at_derived_byte(byte))
294    }
295
296    /// Insert `text` at `at`.
297    ///
298    /// Returns `false`, changing nothing, when `at` belongs to neither this
299    /// transaction nor the state it opened from.
300    pub fn insert(&mut self, at: Position, text: &str) -> bool {
301        let Some(byte) = self.remap(at, Gravity::Backward) else {
302            return false;
303        };
304        self.splice(byte..byte, text)
305    }
306
307    /// Remove the text in `span`.
308    pub fn delete(&mut self, span: Span) -> bool {
309        self.replace(span, "")
310    }
311
312    /// Replace the text in `span` with `text`.
313    ///
314    /// Deleting and inserting as one primitive, so the pair cannot be recorded as
315    /// two undo steps and cannot be interrupted between them.
316    pub fn replace(&mut self, span: Span, text: &str) -> bool {
317        let Some(start) = self.remap(span.start(), Gravity::Backward) else {
318            return false;
319        };
320        let Some(end) = self.remap(span.end(), Gravity::Forward) else {
321            return false;
322        };
323        if end < start {
324            debug_assert!(false, "span ends collapsed past its start");
325            return false;
326        }
327        self.splice(start..end, text)
328    }
329
330    /// Move the cursor, dropping any selection. Not itself an edit.
331    pub fn set_cursor(&mut self, to: Position) -> bool {
332        self.buffer.set_cursor(to)
333    }
334
335    /// Select `span`, leaving the cursor at its end.
336    ///
337    /// For a compound edit that must leave a selection behind — wrapping a
338    /// selection in brackets leaves the inner text selected, so the next wrap
339    /// nests.
340    pub fn select(&mut self, span: Span) -> bool {
341        self.buffer.select(span)
342    }
343
344    pub fn clear_selection(&mut self) {
345        self.buffer.clear_selection();
346    }
347
348    /// Record the transaction and report what it did.
349    ///
350    /// `None` when nothing changed: no history entry, no revision, nothing for a
351    /// consumer to re-derive. A transaction that only moved the cursor keeps the
352    /// move — cursor position is not history.
353    pub fn commit(mut self) -> Option<Change> {
354        self.committed = true;
355        if self.applied.is_empty() {
356            return None;
357        }
358
359        let forward = self.forward_shape();
360        let inverse = self.inverse_shape();
361        let entry = Entry {
362            before: self.before.clone(),
363            after: self.buffer.text.clone(),
364            forward: forward.clone(),
365            inverse,
366            cursor_before: self.cursor_before,
367            cursor_after: self.buffer.cursor.byte(),
368            anchor_before: self.anchor_before,
369            anchor_after: self.buffer.anchor.map(Position::byte),
370            retained: self.retained,
371        };
372
373        if self.extending {
374            if let Some(entry) = self.buffer.history.extend_newest(entry) {
375                self.buffer.history.push(entry);
376            }
377        } else {
378            self.buffer.history.push(entry);
379        }
380
381        Some(Change::new(
382            self.buffer.text.revision(),
383            forward.edits,
384            forward.rows,
385            forward.line_delta,
386        ))
387    }
388
389    // -- internals ----------------------------------------------------------
390
391    fn splice(&mut self, bytes: Range<usize>, text: &str) -> bool {
392        if bytes.is_empty() && text.is_empty() {
393            return true;
394        }
395        let revision_before = self.buffer.text.revision();
396        let removed = bytes.len();
397        let at = bytes.start;
398        let inserted = self.buffer.text.splice(bytes, text);
399
400        self.applied.push(Applied {
401            at,
402            removed,
403            inserted,
404            revision_before,
405        });
406        self.retained += removed + inserted;
407
408        let edit = Applied {
409            at,
410            removed,
411            inserted,
412            revision_before,
413        };
414        let cursor = shift(self.buffer.cursor.byte(), &edit, Gravity::Forward);
415        // Not `position_at_derived_byte`: this offset is legitimately allowed to
416        // land inside a cluster — inserting before a combining mark joins it —
417        // and a cursor snaps forward out of one rather than back into the text
418        // it just typed.
419        self.buffer.cursor = self.buffer.text.position_at_cursor_byte(cursor);
420        // An edit is not a selection gesture. Typing over a selection must not
421        // leave the typed text selected, and a caller that does want a selection
422        // afterwards — wrapping a selection in brackets, so the next wrap nests —
423        // says so with `select`.
424        self.buffer.anchor = None;
425        true
426    }
427
428    /// Where `position` points now, or `None` if it never pointed into this
429    /// transaction's lineage.
430    fn remap(&self, position: Position, gravity: Gravity) -> Option<usize> {
431        if position.revision() == self.buffer.text.revision() {
432            return Some(position.byte());
433        }
434        let from = self
435            .applied
436            .iter()
437            .position(|a| a.revision_before == position.revision())?;
438        let mut byte = position.byte();
439        for edit in &self.applied[from..] {
440            byte = shift(byte, edit, gravity);
441        }
442        Some(byte)
443    }
444
445    /// What the transaction did, in the final text's coordinates.
446    fn forward_shape(&self) -> Shape {
447        let mut edits = Vec::with_capacity(self.applied.len());
448        let mut rows: Option<Range<usize>> = None;
449        for (i, edit) in self.applied.iter().enumerate() {
450            let later = &self.applied[i + 1..];
451            let start = later
452                .iter()
453                .fold(edit.at, |b, e| shift(b, e, Gravity::Backward));
454            let end = later.iter().fold(edit.at + edit.inserted, |b, e| {
455                shift(b, e, Gravity::Forward)
456            });
457            let end = end.max(start).min(self.buffer.text.len_bytes());
458            let start = start.min(end);
459            edits.push(Edit::new(start..end, edit.removed));
460            rows = Some(union(rows, self.rows_of(&self.buffer.text, start..end)));
461        }
462        Shape {
463            edits: Some(edits),
464            rows: rows.unwrap_or(0..0),
465            line_delta: line_delta(&self.before, &self.buffer.text),
466        }
467    }
468
469    /// What undoing it would do, in the original text's coordinates.
470    ///
471    /// Computed here rather than derived at undo time, because here is where both
472    /// coordinate spaces are still known.
473    fn inverse_shape(&self) -> Shape {
474        let mut edits = Vec::with_capacity(self.applied.len());
475        let mut rows: Option<Range<usize>> = None;
476        for (i, edit) in self.applied.iter().enumerate() {
477            let earlier = &self.applied[..i];
478            // Walk the edit's own position back through the ones before it, so
479            // it lands in the coordinates the transaction opened in.
480            let start = earlier.iter().rev().fold(edit.at, unshift);
481            let end = (start + edit.removed).min(self.before.len_bytes());
482            let start = start.min(end);
483            edits.push(Edit::new(start..end, edit.inserted));
484            rows = Some(union(rows, self.rows_of(&self.before, start..end)));
485        }
486        Shape {
487            edits: Some(edits),
488            rows: rows.unwrap_or(0..0),
489            line_delta: line_delta(&self.buffer.text, &self.before),
490        }
491    }
492
493    fn rows_of(&self, text: &Text, bytes: Range<usize>) -> Range<usize> {
494        let first = text.row_of_byte(bytes.start.min(text.len_bytes()));
495        let last = text.row_of_byte(bytes.end.min(text.len_bytes()));
496        first..last + 1
497    }
498}
499
500impl Drop for Txn<'_> {
501    fn drop(&mut self) {
502        if self.committed {
503            return;
504        }
505        self.buffer.text = self.before.clone();
506        self.buffer.cursor = self
507            .buffer
508            .text
509            .position_at_derived_byte(self.cursor_before);
510        self.buffer.anchor = self
511            .anchor_before
512            .map(|byte| self.buffer.text.position_at_derived_byte(byte));
513    }
514}
515
516/// Carry `byte` across one applied edit.
517fn shift(byte: usize, edit: &Applied, gravity: Gravity) -> usize {
518    let end = edit.at + edit.removed;
519    if byte < edit.at {
520        byte
521    } else if byte > edit.at && byte >= end {
522        byte - edit.removed + edit.inserted
523    } else {
524        // At the edit, or inside what it removed. Gravity decides which side of
525        // the new text the offset ends up on: a cursor follows what was typed,
526        // a marker stays pointing where it pointed.
527        match gravity {
528            Gravity::Forward => edit.at + edit.inserted,
529            Gravity::Backward => edit.at,
530        }
531    }
532}
533
534/// Carry `byte` back across one applied edit, into the coordinates before it.
535fn unshift(byte: usize, edit: &Applied) -> usize {
536    let end = edit.at + edit.inserted;
537    if byte <= edit.at {
538        byte
539    } else if byte >= end {
540        byte - edit.inserted + edit.removed
541    } else {
542        edit.at
543    }
544}
545
546fn union(rows: Option<Range<usize>>, next: Range<usize>) -> Range<usize> {
547    match rows {
548        Some(rows) => rows.start.min(next.start)..rows.end.max(next.end),
549        None => next,
550    }
551}
552
553fn line_delta(from: &Text, to: &Text) -> isize {
554    to.line_count() as isize - from.line_count() as isize
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use crate::ropetext::position::Column;
561
562    fn buffer(text: &str) -> EditBuffer {
563        EditBuffer::new(Text::from(text))
564    }
565
566    fn at(buf: &EditBuffer, row: usize, col: usize) -> Position {
567        buf.text()
568            .position(row, Column::new(col))
569            .expect("addressable in the test fixture")
570    }
571
572    fn span(buf: &EditBuffer, from: (usize, usize), to: (usize, usize)) -> Span {
573        let a = at(buf, from.0, from.1);
574        let b = at(buf, to.0, to.1);
575        buf.text().span(a, b).expect("same text")
576    }
577
578    // -- primitives ---------------------------------------------------------
579
580    #[test]
581    fn inserting_at_the_cursor_carries_the_cursor_along() {
582        let mut buf = buffer("hello");
583        let p = at(&buf, 0, 5);
584        buf.set_cursor(p);
585        let mut txn = buf.begin();
586        assert!(txn.insert(p, ", world"));
587        txn.commit();
588        assert_eq!(buf.text().to_string(), "hello, world");
589        assert_eq!(buf.cursor().column().get(), 12);
590    }
591
592    #[test]
593    fn inserting_after_the_cursor_leaves_the_cursor_where_it_was() {
594        let mut buf = buffer("hello");
595        buf.set_cursor(at(&buf, 0, 1));
596        let target = at(&buf, 0, 4);
597        let mut txn = buf.begin();
598        txn.insert(target, "XXX");
599        txn.commit();
600        assert_eq!(buf.cursor().byte(), 1, "an edit elsewhere is not a move");
601    }
602
603    #[test]
604    fn inserting_before_the_cursor_pushes_it_along() {
605        let mut buf = buffer("hello");
606        buf.set_cursor(at(&buf, 0, 4));
607        let target = at(&buf, 0, 0);
608        let mut txn = buf.begin();
609        txn.insert(target, "XX");
610        txn.commit();
611        assert_eq!(buf.cursor().column().get(), 6);
612    }
613
614    #[test]
615    fn deleting_across_the_cursor_collapses_it_to_the_start() {
616        let mut buf = buffer("hello world");
617        buf.set_cursor(at(&buf, 0, 8));
618        let s = span(&buf, (0, 5), (0, 11));
619        let mut txn = buf.begin();
620        txn.delete(s);
621        txn.commit();
622        assert_eq!(buf.text().to_string(), "hello");
623        assert_eq!(buf.cursor().byte(), 5);
624    }
625
626    #[test]
627    fn replacing_leaves_the_cursor_after_the_new_text() {
628        let mut buf = buffer("hello world");
629        let s = span(&buf, (0, 6), (0, 11));
630        buf.set_cursor(s.end());
631        let mut txn = buf.begin();
632        txn.replace(s, "there");
633        txn.commit();
634        assert_eq!(buf.text().to_string(), "hello there");
635        assert_eq!(buf.cursor().column().get(), 11);
636    }
637
638    #[test]
639    fn an_edit_clears_the_selection() {
640        let mut buf = buffer("hello world");
641        let s = span(&buf, (0, 0), (0, 5));
642        buf.select(s);
643        assert!(buf.selection().is_some());
644        let mut txn = buf.begin();
645        txn.replace(s, "bye");
646        txn.commit();
647        assert_eq!(buf.text().to_string(), "bye world");
648        assert!(
649            buf.selection().is_none(),
650            "typing over a selection must not leave the typed text selected"
651        );
652    }
653
654    #[test]
655    fn a_compound_edit_can_leave_a_selection_behind() {
656        // Wrapping a selection in brackets keeps the inner text selected, so a
657        // second wrap nests.
658        let mut buf = buffer("word");
659        let s = span(&buf, (0, 0), (0, 4));
660        let mut txn = buf.begin();
661        txn.insert(s.end(), "]");
662        txn.insert(s.start(), "[");
663        let inner = txn
664            .text()
665            .span(
666                txn.text().position(0, Column::new(1)).unwrap(),
667                txn.text().position(0, Column::new(5)).unwrap(),
668            )
669            .unwrap();
670        txn.select(inner);
671        txn.commit();
672        assert_eq!(buf.text().to_string(), "[word]");
673        assert_eq!(
674            buf.text()
675                .slice(buf.selection().expect("still selected"))
676                .as_deref(),
677            Some("word")
678        );
679    }
680
681    #[test]
682    fn several_primitives_are_one_undo() {
683        let mut buf = buffer("word");
684        let s = span(&buf, (0, 0), (0, 4));
685        let mut txn = buf.begin();
686        txn.insert(s.end(), "]");
687        txn.insert(s.start(), "[");
688        txn.commit();
689        assert_eq!(buf.text().to_string(), "[word]");
690        buf.undo();
691        assert_eq!(buf.text().to_string(), "word", "one action, one undo");
692    }
693
694    #[test]
695    fn a_position_from_before_the_transaction_still_points_at_its_text() {
696        let mut buf = buffer("one two");
697        let two = at(&buf, 0, 4);
698        let head = at(&buf, 0, 0);
699        let mut txn = buf.begin();
700        txn.insert(head, "zero ");
701        // `two` was made before the insert; it now sits five bytes further on.
702        let moved = txn.map(two).expect("carried forward");
703        assert_eq!(moved.byte(), 9);
704        txn.commit();
705    }
706
707    #[test]
708    fn a_position_from_another_text_is_refused() {
709        let mut buf = buffer("hello");
710        let other = Text::from("hello");
711        let foreign = other.position(0, Column::new(2)).unwrap();
712        let mut txn = buf.begin();
713        assert!(!txn.insert(foreign, "x"));
714        assert!(txn.map(foreign).is_none());
715        txn.commit();
716        assert_eq!(buf.text().to_string(), "hello", "nothing happened");
717    }
718
719    #[test]
720    fn a_stale_cursor_is_refused_rather_than_approximated() {
721        let mut buf = buffer("hello");
722        let stale = at(&buf, 0, 4);
723        let head = at(&buf, 0, 0);
724        let mut txn = buf.begin();
725        txn.insert(head, "xx");
726        txn.commit();
727        assert!(!buf.set_cursor(stale));
728    }
729
730    // -- transactions -------------------------------------------------------
731
732    #[test]
733    fn a_transaction_that_changes_nothing_reports_nothing() {
734        let mut buf = buffer("hello");
735        let p = at(&buf, 0, 2);
736        let mut txn = buf.begin();
737        txn.insert(p, "");
738        assert!(txn.commit().is_none());
739        assert!(buf.undo().is_none(), "and records no history");
740    }
741
742    #[test]
743    fn a_transaction_that_only_moves_the_cursor_keeps_the_move() {
744        let mut buf = buffer("hello");
745        let p = at(&buf, 0, 3);
746        let mut txn = buf.begin();
747        txn.set_cursor(p);
748        assert!(txn.commit().is_none(), "a cursor move is not history");
749        assert_eq!(buf.cursor().byte(), 3);
750    }
751
752    #[test]
753    fn dropping_a_transaction_rolls_it_back() {
754        let mut buf = buffer("hello");
755        let end = at(&buf, 0, 5);
756        buf.set_cursor(end);
757        {
758            let mut txn = buf.begin();
759            txn.insert(end, " world");
760            assert_eq!(txn.text().to_string(), "hello world");
761            // dropped without commit
762        }
763        assert_eq!(buf.text().to_string(), "hello");
764        assert_eq!(buf.cursor().byte(), 5);
765        assert!(buf.undo().is_none(), "a rollback is not an undo step");
766    }
767
768    // -- history ------------------------------------------------------------
769
770    #[test]
771    fn a_delete_that_removes_nothing_records_no_history() {
772        // Pressing Delete at the end of a buffer must not consume an undo step.
773        // The implementation this crate replaces does: its `delete_str` walks to a
774        // row past the end, returns `true`, and pushes an entry that removed
775        // nothing — so the *next* undo takes back an unrelated edit.
776        let mut buf = buffer("hello");
777        let start = at(&buf, 0, 0);
778        let mut txn = buf.begin();
779        txn.insert(start, "x");
780        txn.commit();
781
782        // Taken from the current text: a span made before that insert would be
783        // stale, and refused for that reason rather than this one.
784        let end = buf.text().end();
785        let empty = buf.text().span(end, end).expect("same text");
786        let mut txn = buf.begin();
787        assert!(txn.delete(empty), "an empty delete is not a failure");
788        assert!(txn.commit().is_none(), "but it is not a change either");
789
790        buf.undo();
791        assert_eq!(
792            buf.text().to_string(),
793            "hello",
794            "the undo took back the insert, not a phantom delete"
795        );
796        assert!(buf.undo().is_none(), "and there was only ever one entry");
797    }
798
799    #[test]
800    fn undo_of_a_forward_delete_returns_the_cursor_to_where_the_user_was() {
801        // The near end of the deleted range, which is where the cursor was when
802        // the delete happened — not the far end, which it never visited.
803        let mut buf = buffer("hello world");
804        let s = span(&buf, (0, 5), (0, 11));
805        buf.set_cursor(s.start());
806        let mut txn = buf.begin();
807        txn.delete(s);
808        txn.commit();
809        assert_eq!(buf.text().to_string(), "hello");
810        buf.undo();
811        assert_eq!(buf.text().to_string(), "hello world");
812        assert_eq!(buf.cursor().byte(), 5, "not 11");
813    }
814
815    #[test]
816    fn undo_and_redo_walk_the_text_back_and_forward() {
817        let mut buf = buffer("");
818        for word in ["a", "b", "c"] {
819            let end = buf.text().end();
820            let mut txn = buf.begin();
821            txn.insert(end, word);
822            txn.commit();
823        }
824        assert_eq!(buf.text().to_string(), "abc");
825        buf.undo();
826        assert_eq!(buf.text().to_string(), "ab");
827        buf.undo();
828        assert_eq!(buf.text().to_string(), "a");
829        buf.redo();
830        assert_eq!(buf.text().to_string(), "ab");
831        buf.redo();
832        assert_eq!(buf.text().to_string(), "abc");
833        assert!(buf.redo().is_none());
834    }
835
836    #[test]
837    fn undo_restores_the_cursor_the_group_started_from() {
838        let mut buf = buffer("hello");
839        let start = at(&buf, 0, 2);
840        buf.set_cursor(start);
841        let mut txn = buf.begin();
842        txn.insert(start, "XYZ");
843        txn.commit();
844        assert_eq!(buf.cursor().byte(), 5);
845        buf.undo();
846        assert_eq!(buf.cursor().byte(), 2);
847    }
848
849    #[test]
850    fn undo_mints_a_new_revision_rather_than_reusing_the_old_one() {
851        let mut buf = buffer("hello");
852        let before = buf.text().revision();
853        let p = at(&buf, 0, 5);
854        let mut txn = buf.begin();
855        txn.insert(p, "!");
856        txn.commit();
857        buf.undo();
858        assert_eq!(buf.text().to_string(), "hello");
859        assert_ne!(
860            buf.text().revision(),
861            before,
862            "the same content at a later point in the timeline is a later revision"
863        );
864    }
865
866    #[test]
867    fn extending_folds_a_typing_run_into_one_undo() {
868        let mut buf = buffer("");
869        let first = buf.text().end();
870        let mut txn = buf.begin();
871        txn.insert(first, "h");
872        txn.commit();
873        for c in ["e", "l", "l", "o"] {
874            let end = buf.text().end();
875            let mut txn = buf.begin_extending();
876            txn.insert(end, c);
877            txn.commit();
878        }
879        assert_eq!(buf.text().to_string(), "hello");
880        buf.undo();
881        assert_eq!(
882            buf.text().to_string(),
883            "",
884            "the whole run went back at once"
885        );
886        assert!(buf.undo().is_none());
887    }
888
889    #[test]
890    fn extending_with_no_previous_group_records_one() {
891        let mut buf = buffer("");
892        let end = buf.text().end();
893        let mut txn = buf.begin_extending();
894        txn.insert(end, "a");
895        txn.commit();
896        buf.undo();
897        assert_eq!(buf.text().to_string(), "");
898    }
899
900    #[test]
901    fn a_new_group_after_an_undo_drops_the_redo_side() {
902        let mut buf = buffer("");
903        let end = buf.text().end();
904        let mut txn = buf.begin();
905        txn.insert(end, "a");
906        txn.commit();
907        buf.undo();
908        let end = buf.text().end();
909        let mut txn = buf.begin();
910        txn.insert(end, "b");
911        txn.commit();
912        assert!(buf.redo().is_none());
913        assert_eq!(buf.text().to_string(), "b");
914    }
915
916    #[test]
917    fn opening_a_different_note_forgets_the_history() {
918        let mut buf = buffer("first");
919        let end = buf.text().end();
920        let mut txn = buf.begin();
921        txn.insert(end, "!");
922        txn.commit();
923        buf.set_text(Text::from("second"));
924        assert!(
925            buf.undo().is_none(),
926            "an undo must not walk from one note into another"
927        );
928        assert_eq!(buf.text().to_string(), "second");
929    }
930
931    // -- what a change reports ---------------------------------------------
932
933    #[test]
934    fn a_change_reports_the_row_it_touched() {
935        let mut buf = buffer("one\ntwo\nthree");
936        let p = at(&buf, 1, 1);
937        let mut txn = buf.begin();
938        txn.insert(p, "X");
939        let change = txn.commit().expect("something changed");
940        assert_eq!(change.rows(), 1..2);
941        assert_eq!(change.line_delta(), 0);
942        assert!(!change.is_bulk());
943    }
944
945    #[test]
946    fn a_change_reports_added_rows() {
947        let mut buf = buffer("one\ntwo");
948        let p = at(&buf, 0, 3);
949        let mut txn = buf.begin();
950        txn.insert(p, "\nmiddle");
951        let change = txn.commit().expect("something changed");
952        assert_eq!(change.line_delta(), 1);
953        assert_eq!(change.rows(), 0..2);
954        assert!(change.is_bulk());
955    }
956
957    #[test]
958    fn a_change_reports_removed_rows() {
959        let mut buf = buffer("one\ntwo\nthree");
960        let s = span(&buf, (0, 3), (2, 0));
961        let mut txn = buf.begin();
962        txn.delete(s);
963        let change = txn.commit().expect("something changed");
964        assert_eq!(change.line_delta(), -2);
965        assert_eq!(buf.text().to_string(), "onethree");
966    }
967
968    #[test]
969    fn a_change_names_the_bytes_it_wrote() {
970        let mut buf = buffer("hello");
971        let p = at(&buf, 0, 5);
972        let mut txn = buf.begin();
973        txn.insert(p, "!!");
974        let change = txn.commit().expect("something changed");
975        let edits = change.edits().expect("a single transaction is precise");
976        assert_eq!(edits.len(), 1);
977        assert_eq!(edits[0].inserted(), 5..7);
978        assert_eq!(edits[0].removed_bytes(), 0);
979    }
980
981    #[test]
982    fn undoing_a_coalesced_group_declines_to_name_bytes() {
983        let mut buf = buffer("");
984        let end = buf.text().end();
985        let mut txn = buf.begin();
986        txn.insert(end, "a");
987        txn.commit();
988        let end = buf.text().end();
989        let mut txn = buf.begin_extending();
990        txn.insert(end, "b");
991        txn.commit();
992        let change = buf.undo().expect("there was a group");
993        assert!(
994            change.edits().is_none(),
995            "two coordinate spaces cannot be reported as one list"
996        );
997        assert!(change.rows().start <= change.rows().end);
998    }
999
1000    #[test]
1001    fn an_undo_reports_the_rows_of_the_text_it_restored() {
1002        let mut buf = buffer("one\ntwo\nthree");
1003        let p = at(&buf, 2, 0);
1004        let mut txn = buf.begin();
1005        txn.insert(p, "new\n");
1006        txn.commit();
1007        assert_eq!(buf.text().line_count(), 4);
1008        let change = buf.undo().expect("there was a group");
1009        assert_eq!(change.line_delta(), -1);
1010        assert!(
1011            change.rows().end <= buf.text().line_count(),
1012            "rows {:?} must address the restored text of {} rows",
1013            change.rows(),
1014            buf.text().line_count()
1015        );
1016    }
1017
1018    // -- snapshots ----------------------------------------------------------
1019
1020    #[test]
1021    fn a_snapshot_holds_a_text_its_cursor_cannot_drift_from() {
1022        let mut buf = buffer("hello");
1023        buf.set_cursor(at(&buf, 0, 5));
1024        let snap = buf.snapshot();
1025        let end = buf.text().end();
1026        let mut txn = buf.begin();
1027        txn.insert(end, " world");
1028        txn.commit();
1029        assert_eq!(snap.text.to_string(), "hello");
1030        assert_eq!(snap.cursor.byte(), 5);
1031        assert!(!snap.text.is_stale(snap.cursor));
1032        assert_eq!(
1033            snap.text.slice(snap.text.full_span()).as_deref(),
1034            Some("hello")
1035        );
1036    }
1037}