Skip to main content

kimun_notes/ropetext/
buffer.rs

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