Skip to main content

kimun_notes/components/text_editor/
rope_buffer.rs

1//! The **rope buffer**: the editor's buffer over the `ropetext` **edit buffer**.
2//!
3//! The engine owns the text, the cursor, the selection and the history, and
4//! deliberately nothing else (ADR-0041). This is the rest of an editor's buffer —
5//! what kimün adds, not what it adapts:
6//!
7//! - **undo groups**: [`RopeBuffer::edit`] scopes and [`RopeBuffer::continue_group`],
8//!   so a compound action or a typing run is one history entry;
9//! - the edit outcome: what a mutation did, measured — changed, bulk, the damage
10//!   hull in one numbering, the line delta — drained by the component through
11//!   [`RopeBuffer::take_outcome`];
12//! - the goal column a vertical motion aims at;
13//! - the **indent step** ([`RopeBuffer::indent_rows`]): the one place Tab, `>>` and
14//!   a list continuation's dedent move a row by;
15//! - the **find pattern** and its row-wise, wrapping search, which outlive the
16//!   find bar so vim's `n`/`N` can repeat them;
17//! - the yank transport the vim engine's **unnamed register** fills from;
18//! - the `(row, col)` vocabulary every caller speaks: the vim engine, the plain
19//!   key table, the find bar and the component all take `&mut RopeBuffer`.
20//!
21//! It knows text, not markdown. List continuation, **auto-surround** and the
22//! emphasis markers are operations *over* it, in `markdown_edits`, and are
23//! tested against a bare one.
24//!
25//! Nothing here mirrors the text into a second representation. Callers that want
26//! rows ask for them ([`RopeBuffer::rows`]) and pay for them there; the buffer
27//! keeps one copy of the note and no derived copy in step with it.
28//!
29use crate::ropetext::motion::{self, Goal, Words};
30use crate::ropetext::{Change, Column, EditBuffer as Rope, Position, Span, Text};
31
32/// How far one **indent step** moves a row, in spaces — what Tab, `>>` and the
33/// visual `>` add, and what their inverses remove.
34///
35/// Not a tab stop, and deliberately not derived from one. A tab stop is elastic
36/// (a `\t` advances to the next multiple of it, so its width depends on where it
37/// starts) and describes how an existing character *draws*; an indent step is a
38/// fixed amount of text an edit *inserts*, and here it is always spaces. Vim
39/// keeps the two apart as `tabstop` and `shiftwidth`, EditorConfig as
40/// `tab_width` and `indent_size`. That both are 4 today
41/// ([`crate::ropetext::Metrics::DEFAULT_TAB_WIDTH`]) is a coincidence of defaults.
42const DEFAULT_INDENT_WIDTH: std::num::NonZeroU8 = std::num::NonZeroU8::new(4).unwrap();
43
44/// What one call to [`RopeBuffer::edit`] did, measured rather than predicted.
45///
46/// `#[must_use]` on purpose: the caller still applies these (the revision clock
47/// serves both backends and so stays on the component), and forgetting to is
48/// exactly the failure this type exists to prevent. A warning is a check; a
49/// convention is not.
50#[must_use = "an edit's outcome drives the revision bump and the parse-damage signal"]
51#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct EditOutcome {
53    /// The buffer's text differs from before the edit. A content comparison —
54    /// never a library return value, which can report `false` after mutating.
55    pub changed: bool,
56    /// The change is not confined to the cursor's row, so the incremental
57    /// parser's cursor damage hint would under-report it.
58    pub bulk: bool,
59    /// Which rows the edits changed, in the new text's numbering — the hull when
60    /// several ran before this was drained.
61    ///
62    /// Told by the engine rather than found by comparing the buffer with a copy
63    /// of its previous self, which is what the revision-tagged rope makes
64    /// possible. The **nvim** backend reports lines and not changes, so it
65    /// leaves this `None` and its consumer falls back to a diff.
66    pub damage: Option<std::ops::Range<usize>>,
67    /// Net rows added (or removed, when negative) by the edits behind `damage`.
68    ///
69    /// Travels with the range because the range is only meaningful in a
70    /// numbering, and a consumer that accumulates reports across several drains
71    /// has to bring the older one forward before it can union them.
72    pub line_delta: isize,
73}
74
75/// `range`, renumbered for a change of `delta` lines starting at `at`.
76///
77/// Rows above the change keep their index; the rest move with it. A `delta` of
78/// zero — every edit that stays within its rows, which is most of them — leaves
79/// the range alone.
80///
81/// Shared because damage is accumulated in two places: here, across the
82/// mutations of one group, and in the view, across the drains between two
83/// frames. Both union ranges recorded against different texts, and both are
84/// wrong in the same way without this.
85pub(super) fn shift_rows(
86    range: std::ops::Range<usize>,
87    at: usize,
88    delta: isize,
89) -> std::ops::Range<usize> {
90    let shift = |row: usize| {
91        if row < at {
92            row
93        } else {
94            row.saturating_add_signed(delta)
95        }
96    };
97    shift(range.start)..shift(range.end)
98}
99
100/// Whether a delete fills the register it removed text from.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102enum Yank {
103    Keep,
104    Discard,
105}
106
107/// One row's share of an indent, addressed against the text before the block
108/// began; the transaction carries it across the rows edited before it.
109#[derive(Debug, Clone, Copy)]
110enum RowEdit {
111    Insert(Position),
112    Delete(Span),
113}
114
115/// A cursor movement, in the vocabulary the editor already speaks.
116///
117/// Deliberately the incumbent's variant set, so the 145 call sites need no
118/// rewriting — but `Jump` takes `usize` rather than `u16`, because clamping a
119/// row to 65535 is a defect the old widget's contract allowed, and this is the
120/// type where it stops being representable.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum CursorMove {
123    Forward,
124    Back,
125    Up,
126    Down,
127    Head,
128    End,
129    Top,
130    Bottom,
131    WordForward,
132    WordBack,
133    WordEnd,
134    /// `W` — a WORD is any run of non-blanks.
135    WordForwardBig,
136    /// `B`.
137    WordBackBig,
138    /// `E`.
139    WordEndBig,
140    /// `ge` / `gE`.
141    WordEndBack {
142        big: bool,
143    },
144    /// `%`. Stays put when there is no bracket ahead on the row, or it is
145    /// unbalanced.
146    MatchingPair,
147    ParagraphForward,
148    ParagraphBack,
149    Jump(usize, usize),
150}
151
152impl CursorMove {
153    /// Whether the movement is vertical, and so keeps the goal column.
154    ///
155    /// `Top` and `Bottom` are row movements but deliberately *not* goal-preserving:
156    /// vim's `gg`/`G` go to a row's first non-blank rather than to a remembered
157    /// column, and making them sticky would be a third behaviour that neither vim
158    /// nor the incumbent has. Change #8 is about `Up`/`Down`.
159    fn is_vertical(self) -> bool {
160        matches!(self, CursorMove::Up | CursorMove::Down)
161    }
162}
163
164/// The open note's text, cursor, selection and history.
165#[derive(Debug)]
166pub struct RopeBuffer {
167    inner: Rope,
168    /// Accumulated since the last drain. The component owns the revision clock
169    /// for as long as the nvim backend has no edit buffer.
170    pending: EditOutcome,
171    /// Nesting depth of [`Self::edit`]. Above zero, a mutation extends the open
172    /// group instead of starting its own.
173    depth: u32,
174    /// Whether the open group has recorded anything yet, so the first mutation
175    /// inside `edit` starts the group and the rest extend it.
176    group_started: bool,
177    /// Set by a backend that has decided the next mutation continues what the
178    /// last one started — a typing run, an insert session. Cleared by using it,
179    /// so continuing is asked for per edit rather than left switched on.
180    continue_group: bool,
181    /// The column a vertical movement is aiming at, which is why walking down
182    /// through a short row and out the other side returns to where it started.
183    goal: Option<Column>,
184    yank: String,
185    search: Option<regex::Regex>,
186    indent_width: std::num::NonZeroU8,
187}
188
189impl Default for RopeBuffer {
190    fn default() -> Self {
191        Self::new(Text::new())
192    }
193}
194
195impl RopeBuffer {
196    pub fn new(text: Text) -> Self {
197        Self {
198            inner: Rope::new(text),
199            pending: EditOutcome::default(),
200            depth: 0,
201            group_started: false,
202            continue_group: false,
203            goal: None,
204            yank: String::new(),
205            search: None,
206            indent_width: DEFAULT_INDENT_WIDTH,
207        }
208    }
209
210    /// Replace the whole buffer, dropping the history with it.
211    pub fn replace(&mut self, text: Text) {
212        self.inner.set_text(text);
213        self.pending = EditOutcome::default();
214        self.goal = None;
215    }
216
217    pub fn text(&self) -> &Text {
218        self.inner.text()
219    }
220
221    /// Pin the **indent step** to a non-default width, so a test can catch a
222    /// reintroduced literal. Nothing configures the step in production yet.
223    #[cfg(test)]
224    pub fn set_indent_width(&mut self, spaces: u8) {
225        self.indent_width =
226            std::num::NonZeroU8::new(spaces).expect("an indent step is at least one space");
227    }
228
229    pub fn snapshot(&self) -> crate::ropetext::Snapshot {
230        self.inner.snapshot()
231    }
232
233    // ── Reads ────────────────────────────────────────────────────────────────
234
235    /// One row's text, or `None` past the end.
236    pub fn row(&self, row: usize) -> Option<std::borrow::Cow<'_, str>> {
237        self.inner.text().line(row)
238    }
239
240    /// How many rows the buffer has. Never zero.
241    pub fn row_count(&self) -> usize {
242        self.inner.text().line_count()
243    }
244
245    /// Every row, materialised.
246    ///
247    /// Not a cache: nothing is maintained between calls, so the cost lands on the
248    /// caller that wants a vector rather than on every edit. That is the whole
249    /// difference from the shim this replaced.
250    pub fn rows(&self) -> Vec<String> {
251        self.inner.text().lines().map(|l| l.to_string()).collect()
252    }
253
254    /// Rows `first..=last`, joined with newlines.
255    pub fn joined_rows(&self, first: usize, last: usize) -> String {
256        (first..=last)
257            .filter_map(|row| self.row(row))
258            .collect::<Vec<_>>()
259            .join("\n")
260    }
261
262    /// Characters in one row.
263    pub fn row_len(&self, row: usize) -> usize {
264        self.inner.text().line_len_chars(row).unwrap_or(0)
265    }
266
267    pub fn cursor(&self) -> (usize, usize) {
268        let cursor = self.inner.cursor();
269        (cursor.row(), cursor.column().get())
270    }
271
272    pub fn is_empty(&self) -> bool {
273        self.inner.text().len_bytes() == 0
274    }
275
276    pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
277        let span = self.inner.selection()?;
278        Some((rc(span.start()), rc(span.end())))
279    }
280
281    pub fn yank_text(&self) -> String {
282        self.yank.clone()
283    }
284
285    pub fn set_yank_text(&mut self, text: impl Into<String>) {
286        self.yank = text.into();
287    }
288
289    pub fn search_pattern(&self) -> Option<&regex::Regex> {
290        self.search.as_ref()
291    }
292
293    pub fn take_outcome(&mut self) -> EditOutcome {
294        std::mem::take(&mut self.pending)
295    }
296
297    // ── Groups ───────────────────────────────────────────────────────────────
298
299    /// Run `f` as one **undo group**.
300    ///
301    /// Every mutation inside lands in a single history entry, however many
302    /// primitives it takes. Nested calls belong to the outermost group, so a
303    /// compound action built from the single-mutation helpers is still one undo.
304    pub fn edit<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
305        if self.depth > 0 {
306            return f(self);
307        }
308        self.depth = 1;
309        self.group_started = false;
310        let out = f(self);
311        self.depth = 0;
312        self.group_started = false;
313        out
314    }
315
316    /// The next mutation joins the previous group instead of starting one.
317    ///
318    /// The policy is the backend's, because only it knows what the user was doing
319    /// — mid-word against after a pause, inside an Insert session against having
320    /// left it. This is the mechanism; `typing_run` and the vim engine are the two
321    /// callers that hold an opinion.
322    pub fn continue_group(&mut self) {
323        self.continue_group = true;
324    }
325
326    /// Apply one primitive as its own group, or as part of an open one.
327    fn mutate(&mut self, f: impl FnOnce(&mut crate::ropetext::Txn<'_>)) -> bool {
328        let extending =
329            (self.depth > 0 && self.group_started) || std::mem::take(&mut self.continue_group);
330        let mut txn = if extending {
331            self.inner.begin_extending()
332        } else {
333            self.inner.begin()
334        };
335        f(&mut txn);
336        let change = txn.commit();
337        if self.depth > 0 {
338            self.group_started = true;
339        }
340        self.record(change)
341    }
342
343    fn record(&mut self, change: Option<Change>) -> bool {
344        let Some(change) = change else {
345            return false;
346        };
347        self.pending.changed = true;
348        self.pending.bulk |= change.is_bulk();
349        self.pending.line_delta += change.line_delta();
350        self.pending.damage = Some(match self.pending.damage.take() {
351            Some(seen) => {
352                // `seen` was recorded against the text as it stood before *this*
353                // change, which may have moved those rows. Hulling the two
354                // directly unions ranges from two different numberings, and the
355                // result is not a superset of either: an edit high in the buffer
356                // followed by one above it that adds a line leaves the first
357                // edit's row below the hull's end, so it is never re-parsed and
358                // renders stale. Bring it into the current numbering first.
359                let seen = shift_rows(seen, change.rows().start, change.line_delta());
360                seen.start.min(change.rows().start)..seen.end.max(change.rows().end)
361            }
362            None => change.rows(),
363        });
364        true
365    }
366
367    // ── Mutations ────────────────────────────────────────────────────────────
368
369    pub fn insert_str(&mut self, s: impl AsRef<str>) -> bool {
370        let text = s.as_ref().to_string();
371        // The anchor goes whether or not it spanned anything, as it does for a
372        // delete: typing is not a selection gesture either.
373        let span = self.inner.selection().filter(|span| !span.is_empty());
374        self.inner.clear_selection();
375        let cursor = self.inner.cursor();
376        self.goal = None;
377        self.mutate(|txn| match span {
378            Some(span) => {
379                txn.replace(span, &text);
380            }
381            None => {
382                txn.insert(cursor, &text);
383            }
384        })
385    }
386
387    pub fn insert_char(&mut self, c: char) {
388        self.insert_str(c.to_string());
389    }
390
391    pub fn insert_newline(&mut self) {
392        self.insert_str("\n");
393    }
394
395    /// Delete `clusters` grapheme clusters forward, a line break counting as one.
396    ///
397    /// Clusters and not scalars, because a delete may not leave half a character
398    /// behind: `forward_by` steps whole clusters, so a caller counting scalars
399    /// over a flag or a ZWJ emoji spends the difference on the text after it.
400    pub fn delete_str(&mut self, clusters: usize) -> bool {
401        if self.take_selection() {
402            return true;
403        }
404        if clusters == 0 {
405            return false;
406        }
407        let from = self.inner.cursor();
408        let to = self.forward_by(from, clusters);
409        self.delete_between(from, to, Yank::Keep)
410    }
411
412    /// Backspace.
413    pub fn delete_char(&mut self) -> bool {
414        if self.take_selection() {
415            return true;
416        }
417        let to = self.inner.cursor();
418        let from = motion::prev_cluster(self.inner.text(), to);
419        // A backspace does not fill the register; only `delete_str`, the word
420        // deletes and `cut` do. Matching the incumbent, which is also vim: `x`
421        // yanks, but a plain backspace in Insert does not.
422        self.delete_between(from, to, Yank::Discard)
423    }
424
425    /// Forward delete.
426    pub fn delete_next_char(&mut self) -> bool {
427        if self.take_selection() {
428            return true;
429        }
430        let from = self.inner.cursor();
431        let to = motion::next_cluster(self.inner.text(), from);
432        self.delete_between(from, to, Yank::Discard)
433    }
434
435    pub fn delete_word(&mut self) -> bool {
436        if self.take_selection() {
437            return true;
438        }
439        let to = self.inner.cursor();
440        let text = self.inner.text();
441        // The incumbent's cascade, which *is* the contract: a word start on this
442        // row, else the row's start, else the line break before it. The last case
443        // is why this cannot simply be a row-local motion.
444        let candidate = motion::word_start_back(text, to, Words::Small);
445        let (from, yank) = if candidate.row() == to.row() && candidate.byte() < to.byte() {
446            (candidate, Yank::Keep)
447        } else if to.column().get() > 0 {
448            (motion::row_start(text, to), Yank::Keep)
449        } else {
450            // Joining rows goes through the incumbent's `delete_newline`, which
451            // does not fill the register. A pasted newline in place of the last
452            // yanked word is a surprising thing to hand back.
453            (motion::prev_cluster(text, to), Yank::Discard)
454        };
455        self.delete_between(from, to, yank)
456    }
457
458    pub fn delete_next_word(&mut self) -> bool {
459        if self.take_selection() {
460            return true;
461        }
462        let from = self.inner.cursor();
463        let text = self.inner.text();
464        // Mirror of `delete_word`: the end of the word at or after the cursor on
465        // this row, else the row's end, else the line break after it. `word_end_
466        // at_or_after` rather than `word_end_forward`, because deleting to the end
467        // of a word must name the word the cursor is *in* — vim's `e` deliberately
468        // looks past it.
469        let candidate = motion::word_end_at_or_after(text, from, Words::Small);
470        let row_end = motion::row_end(text, from);
471        let (to, yank) = match candidate {
472            Some(end) if end.row() == from.row() && end.byte() > from.byte() => (end, Yank::Keep),
473            _ if from.byte() < row_end.byte() => (row_end, Yank::Keep),
474            _ => (motion::next_cluster(text, from), Yank::Discard),
475        };
476        self.delete_between(from, to, yank)
477    }
478
479    pub fn cut(&mut self) -> bool {
480        // Takes the anchor whether or not it spanned anything, like every other
481        // operation that consumes a selection.
482        let span = self.inner.selection().filter(|span| !span.is_empty());
483        self.inner.clear_selection();
484        let Some(span) = span else {
485            return false;
486        };
487        self.yank = self
488            .inner
489            .text()
490            .slice(span)
491            .map(|text| text.to_string())
492            .unwrap_or_default();
493        self.goal = None;
494        self.mutate(|txn| {
495            txn.delete(span);
496        })
497    }
498
499    /// Copying reads: it leaves the selection where it is, and an empty one leaves
500    /// the register alone rather than emptying it.
501    pub fn copy(&mut self) {
502        if let Some(span) = self.inner.selection().filter(|span| !span.is_empty())
503            && let Some(text) = self.inner.text().slice(span)
504        {
505            self.yank = text.to_string();
506        }
507    }
508
509    pub fn paste(&mut self) -> bool {
510        if self.yank.is_empty() {
511            return false;
512        }
513        let text = std::mem::take(&mut self.yank);
514        let changed = self.insert_str(&text);
515        self.yank = text;
516        changed
517    }
518
519    /// Deleting a selection as a *side effect* of typing or of a forward delete
520    /// does not fill the register; an explicit delete does. That asymmetry is the
521    /// incumbent's and vim's both: `d` fills the unnamed register, typing over a
522    /// selection does not.
523    /// Take the selection and delete it, reporting whether anything went.
524    ///
525    /// Taking it is unconditional: an empty selection is not a range, so the
526    /// caller proceeds as though there were none — but the anchor is gone either
527    /// way. Leaving it alive is how an invisible selection outlives the gesture
528    /// that made it — a defect that cost two notes before it was found.
529    fn take_selection(&mut self) -> bool {
530        let span = self.inner.selection().filter(|span| !span.is_empty());
531        self.inner.clear_selection();
532        let Some(span) = span else {
533            return false;
534        };
535        self.delete_between(span.start(), span.end(), Yank::Discard)
536    }
537
538    fn delete_between(&mut self, from: Position, to: Position, yank: Yank) -> bool {
539        let Some(span) = self.inner.text().span(from, to) else {
540            return false;
541        };
542        if span.is_empty() {
543            return false;
544        }
545        if yank == Yank::Keep
546            && let Some(text) = self.inner.text().slice(span)
547        {
548            self.yank = text.to_string();
549        }
550        self.goal = None;
551        self.mutate(|txn| {
552            txn.delete(span);
553        })
554    }
555
556    // ── History ──────────────────────────────────────────────────────────────
557
558    pub fn undo(&mut self) -> bool {
559        let change = self.inner.undo();
560        self.after_history(change)
561    }
562
563    pub fn redo(&mut self) -> bool {
564        let change = self.inner.redo();
565        self.after_history(change)
566    }
567
568    /// The engine restores the selection an entry began with, which the incumbent
569    /// does not. Dropping it keeps undo behaving as it does today: a selection is
570    /// painted, so putting one back is a visible change, and an engine swap is the
571    /// wrong place to make one. The capability stays in the engine for when it is
572    /// asked for on purpose.
573    fn after_history(&mut self, change: Option<Change>) -> bool {
574        if change.is_none() {
575            // Nothing to undo: an operation that did not happen changes nothing,
576            // the selection included.
577            return false;
578        }
579        self.goal = None;
580        self.inner.clear_selection();
581        self.record(change)
582    }
583
584    /// The character at column `col` of `row`, as its start and end columns.
585    ///
586    /// One character on screen can be several chars in the buffer — `❤️` is
587    /// two, a decomposed `é` is `e` plus U+0301 — so "the char under the
588    /// cursor" is a grapheme cluster, and only its edges are positions the
589    /// buffer accepts. A column inside a cluster belongs to that cluster; one at
590    /// or past the row's end gives the row's end twice.
591    ///
592    /// The edges are the text's own ([`motion::next_cluster`]), so this can
593    /// never call a column an edge that the buffer would then refuse.
594    pub fn cluster_at(&self, row: usize, col: usize) -> (usize, usize) {
595        let text = self.inner.text();
596        let Some(len) = text.line_len_chars(row) else {
597            return (col, col);
598        };
599        if col >= len {
600            return (len, len);
601        }
602        // The nearest edge at or before `col` — a cluster is a few chars, so
603        // this walks back a few at most.
604        let Some(start) = (0..=col)
605            .rev()
606            .find_map(|c| text.position(row, Column::new(c)))
607        else {
608            return (col, col);
609        };
610        (start.column().get(), Self::next_edge(text, start, len))
611    }
612
613    /// How many characters (grapheme clusters) columns `from..=to` of `row`
614    /// span. At least one.
615    ///
616    /// One segmentation pass over the row — the same Unicode rule the text
617    /// applies, over the same row, so the two agree on every edge inside it.
618    /// Stepping [`motion::next_cluster`] per character instead is correct but
619    /// re-checks context at every step, which on a paragraph-long row is a
620    /// visible stall.
621    pub fn clusters_through(&self, row: usize, from: usize, to: usize) -> usize {
622        use unicode_segmentation::UnicodeSegmentation;
623        let Some(line) = self.row(row) else {
624            return 1;
625        };
626        let mut start = 0;
627        let mut count = 0;
628        for cluster in line.graphemes(true) {
629            let end = start + cluster.chars().count();
630            if start > to {
631                break;
632            }
633            if end > from {
634                count += 1;
635            }
636            start = end;
637        }
638        count.max(1)
639    }
640
641    /// The column `n` characters right of the character at `col` on `row`,
642    /// stopping at the row's last character. One pass, as
643    /// [`Self::clusters_through`].
644    pub fn col_after_clusters(&self, row: usize, col: usize, n: usize) -> usize {
645        use unicode_segmentation::UnicodeSegmentation;
646        let Some(line) = self.row(row) else {
647            return col;
648        };
649        // Starts of the character holding `col` and of every one after it.
650        let mut starts = line
651            .graphemes(true)
652            .scan(0, |at, cluster| {
653                let start = *at;
654                *at += cluster.chars().count();
655                Some((start, *at))
656            })
657            .skip_while(|&(_, end)| end <= col)
658            .map(|(start, _)| start);
659        let Some(mut at) = starts.next() else {
660            // `col` is at or past the row's end.
661            return line.chars().count();
662        };
663        for next in starts.take(n) {
664            at = next;
665        }
666        at
667    }
668
669    /// The column where the character starting at `at` ends, on its own row
670    /// (a row of `len` chars).
671    fn next_edge(text: &Text, at: Position, len: usize) -> usize {
672        let next = motion::next_cluster(text, at);
673        if next.row() == at.row() && next.byte() > at.byte() {
674            next.column().get()
675        } else {
676            len
677        }
678    }
679
680    // ── Marks ────────────────────────────────────────────────────────────────
681
682    /// Put mark `name` at `(row, col)`. `false`, setting nothing, when the
683    /// buffer has no such position.
684    ///
685    /// From here on the mark follows the text it points at: rows added or
686    /// removed above it carry it along, text typed before it on its row pushes
687    /// it right, and deleting the text under it collapses it to where the
688    /// deletion began. Undo and redo put it back where it was before and after
689    /// each change. The edit buffer owns the marks, beside the cursor and the
690    /// history that saves them.
691    pub fn set_mark(&mut self, name: char, (row, col): (usize, usize)) -> bool {
692        let Some(at) = self.inner.text().position(row, Column::new(col)) else {
693            return false;
694        };
695        self.inner.set_mark(name, at)
696    }
697
698    /// Where mark `name` points now, if it is set.
699    pub fn mark(&self, name: char) -> Option<(usize, usize)> {
700        self.inner.mark(name).map(|m| (m.row(), m.column().get()))
701    }
702
703    // ── Cursor and selection ─────────────────────────────────────────────────
704
705    /// Move the cursor, extending a live selection.
706    ///
707    /// Directional movement keeps the anchor deliberately: that is how vim's
708    /// Visual mode extends.
709    pub fn move_cursor(&mut self, movement: CursorMove) {
710        let text = self.inner.text();
711        let from = self.inner.cursor();
712        let goal = self
713            .goal
714            .filter(|_| movement.is_vertical())
715            .unwrap_or_else(|| from.column());
716
717        let to = match movement {
718            CursorMove::Forward => motion::next_cluster(text, from),
719            CursorMove::Back => motion::prev_cluster(text, from),
720            CursorMove::Up => motion::vertical(text, from, -1, Goal::Column(goal)),
721            CursorMove::Down => motion::vertical(text, from, 1, Goal::Column(goal)),
722            CursorMove::Head => motion::row_start(text, from),
723            CursorMove::End => motion::row_end(text, from),
724            // The first and last *row*, keeping the column — not the start and
725            // end of the text, which is a different place on a non-empty row.
726            CursorMove::Top => {
727                let up = -(from.row() as isize);
728                motion::vertical(text, from, up, Goal::Column(goal))
729            }
730            CursorMove::Bottom => {
731                let down = (text.line_count().saturating_sub(1) as isize) - from.row() as isize;
732                motion::vertical(text, from, down, Goal::Column(goal))
733            }
734            CursorMove::WordForward => motion::word_start_forward(text, from, Words::Small),
735            CursorMove::WordBack => motion::word_start_back(text, from, Words::Small),
736            CursorMove::WordForwardBig => motion::word_start_forward(text, from, Words::Big),
737            CursorMove::WordBackBig => motion::word_start_back(text, from, Words::Big),
738            // Inclusive, like `WordEnd`: a cursor landing on a word's end wants the
739            // last cluster, not the place after it.
740            CursorMove::WordEndBig => match motion::word_end_forward(text, from, Words::Big) {
741                Some(end) => motion::prev_cluster(text, end),
742                None => from,
743            },
744            CursorMove::WordEndBack { big } => {
745                let words = if big { Words::Big } else { Words::Small };
746                match motion::word_end_back(text, from, words) {
747                    Some(end) => motion::prev_cluster(text, end),
748                    None => from,
749                }
750            }
751            CursorMove::MatchingPair => motion::matching_bracket(text, from).unwrap_or(from),
752            // The crate's word end is exclusive — just past the last cluster —
753            // because that is what an operator range wants. A *cursor* landing on
754            // a word end wants the last cluster itself, as vim's `e` does. This is
755            // the inclusive-to-half-open conversion CONTEXT names under **span
756            // kind**, and the adapter is where it belongs: the engine holds no view
757            // on which convention a caller uses.
758            CursorMove::WordEnd => match motion::word_end_forward(text, from, Words::Small) {
759                Some(end) => motion::prev_cluster(text, end),
760                // Nothing ahead: the incumbent walks to the end of the text rather
761                // than staying put, and vim's `e` on a trailing blank line does the
762                // same.
763                None => motion::text_end(text),
764            },
765            CursorMove::ParagraphForward => motion::paragraph_forward(text, from),
766            CursorMove::ParagraphBack => motion::paragraph_back(text, from),
767            CursorMove::Jump(row, column) => {
768                match text.position(row, Column::new(column)) {
769                    Some(position) => position,
770                    // Refused, not clamped: a keypress that did nothing is
771                    // recoverable in a way one that edited elsewhere is not.
772                    None => return,
773                }
774            }
775        };
776
777        self.goal = if movement.is_vertical() {
778            Some(goal)
779        } else {
780            None
781        };
782        self.place(to);
783    }
784
785    /// Move the cursor to `(row, col)`, refusing a position the buffer cannot
786    /// address rather than landing somewhere else.
787    pub fn jump_to(&mut self, row: usize, col: usize) -> bool {
788        let Some(to) = self.inner.text().position(row, Column::new(col)) else {
789            return false;
790        };
791        self.goal = None;
792        self.place(to);
793        true
794    }
795
796    /// Move to a position the caller worked out itself — a visual-line motion,
797    /// which needs a layout the buffer does not have.
798    pub fn move_to(&mut self, to: Position) {
799        if self.inner.text().is_stale(to) {
800            return;
801        }
802        self.goal = None;
803        self.place(to);
804    }
805
806    fn place(&mut self, to: Position) {
807        if self.inner.selection().is_some() {
808            self.inner.extend_to(to);
809        } else {
810            self.inner.set_cursor(to);
811        }
812    }
813
814    pub fn start_selection(&mut self) {
815        // Anchors *here*, even when a selection is already live: starting one is
816        // a fresh gesture, not an extension of the last.
817        let cursor = self.inner.cursor();
818        self.inner.clear_selection();
819        self.inner.extend_to(cursor);
820    }
821
822    pub fn cancel_selection(&mut self) {
823        self.inner.clear_selection();
824    }
825
826    pub fn select_all(&mut self) {
827        let span = self.inner.text().full_span();
828        self.inner.select(span);
829    }
830
831    pub fn set_selection(&mut self, start: (usize, usize), end: (usize, usize)) -> bool {
832        let text = self.inner.text();
833        let Some(from) = text.position(start.0, Column::new(start.1)) else {
834            return false;
835        };
836        let Some(to) = text.position(end.0, Column::new(end.1)) else {
837            return false;
838        };
839        let Some(span) = text.span(from, to) else {
840            return false;
841        };
842        self.inner.select(span)
843    }
844
845    /// The text between two `(row, col)` pairs, in either order; `None` when the
846    /// range is empty or names a position the buffer cannot address.
847    pub fn text_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<String> {
848        let span = self
849            .span_between(start, end)
850            .filter(|span| !span.is_empty())?;
851        self.inner.text().slice(span).map(|text| text.into_owned())
852    }
853
854    /// The selected text, or `None` when nothing — or nothing of width — is
855    /// selected.
856    pub fn selection_text(&self) -> Option<String> {
857        let (start, end) = self.selection_range()?;
858        self.text_between(start, end)
859    }
860
861    // ── Indent ───────────────────────────────────────────────────────────────
862
863    /// Indent or dedent `rows` by one **indent step**, as one **undo group** —
864    /// and one transaction.
865    ///
866    /// Indenting puts `indent_width` spaces at the start of every row — always
867    /// spaces, never a tab. Dedenting takes up to one step of leading spaces,
868    /// or the run up to and including a first tab, which counts as a whole
869    /// step: `"  \tfoo"` loses both spaces and the tab and stops there.
870    ///
871    /// The cursor keeps the character it sat on — its column moves by its own
872    /// row's change, so `>>` on the `n` of `one` leaves it on `n`, neovim's
873    /// rule — and a selection is put back the same way, endpoint by endpoint.
874    /// Both fall out of the engine's own mapping across the transaction, so the
875    /// history entry holds the cursor's real before and after, and an undo or
876    /// redo lands where the user was. Rows past the end are ignored. Reports
877    /// whether any text changed; a dedent that finds nothing to remove changes
878    /// nothing and says so.
879    pub fn indent_rows(&mut self, rows: std::ops::RangeInclusive<usize>, dedent: bool) -> bool {
880        let step = self.indent_width.get() as usize;
881        let first = *rows.start();
882        let last = (*rows.end()).min(self.row_count() - 1);
883        if first > last {
884            return false;
885        }
886        // Address every row's edit against the text as it stands; the
887        // transaction carries each later one across the earlier ones.
888        let text = self.inner.text();
889        let mut edits: Vec<RowEdit> = Vec::with_capacity(last - first + 1);
890        let mut deltas: Vec<isize> = Vec::with_capacity(last - first + 1);
891        for row in first..=last {
892            let at = text
893                .position(row, Column::new(0))
894                .expect("row is within the buffer");
895            if dedent {
896                let count = self.leading_step(row);
897                if count > 0
898                    && let Some(span) = text.span(at, self.forward_by(at, count))
899                {
900                    edits.push(RowEdit::Delete(span));
901                }
902                deltas.push(-(count as isize));
903            } else {
904                edits.push(RowEdit::Insert(at));
905                deltas.push(step as isize);
906            }
907        }
908        if edits.is_empty() {
909            return false;
910        }
911        let selection = self.selection_range();
912        let shifted = |(row, col): (usize, usize)| -> (usize, usize) {
913            if row < first || row > last {
914                return (row, col);
915            }
916            (row, col.saturating_add_signed(deltas[row - first]))
917        };
918        let spaces = " ".repeat(step);
919        self.goal = None;
920        self.mutate(|txn| {
921            for edit in &edits {
922                match *edit {
923                    RowEdit::Insert(at) => {
924                        txn.insert(at, &spaces);
925                    }
926                    RowEdit::Delete(span) => {
927                        txn.delete(span);
928                    }
929                }
930            }
931            // An edit drops the anchor; a selection that was live goes back,
932            // each endpoint shifted by its own row's change.
933            if let Some((start, end)) = selection {
934                let (start, end) = (shifted(start), shifted(end));
935                let text = txn.text();
936                if let Some(from) = text.position(start.0, Column::new(start.1))
937                    && let Some(to) = text.position(end.0, Column::new(end.1))
938                    && let Some(span) = text.span(from, to)
939                {
940                    txn.select(span);
941                }
942            }
943        })
944    }
945
946    /// What one dedent removes from the front of `row`: up to a step of spaces,
947    /// or the run up to and including a first tab. Counted in grapheme
948    /// clusters — the unit the delete steps — so a space that carries a
949    /// combining mark ends the run rather than being half-removed.
950    fn leading_step(&self, row: usize) -> usize {
951        use unicode_segmentation::UnicodeSegmentation;
952        let step = self.indent_width.get() as usize;
953        let Some(line) = self.row(row) else {
954            return 0;
955        };
956        let mut count = 0;
957        for cluster in line.graphemes(true).take(step) {
958            match cluster {
959                " " => count += 1,
960                "\t" => return count + 1,
961                _ => break,
962            }
963        }
964        count
965    }
966
967    // ── Search ───────────────────────────────────────────────────────────────
968    //
969    // Not the engine's business: it holds the pattern because vim's
970    // `n`/`N` outlive the find bar, and matches a row at a time because a **find
971    // pattern** can never span a newline.
972
973    pub fn set_search_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
974        if pattern.is_empty() {
975            self.search = None;
976            return Ok(());
977        }
978        self.search = Some(regex::Regex::new(pattern)?);
979        Ok(())
980    }
981
982    /// Move the cursor to the next match. Never extends a selection.
983    pub fn search_forward(&mut self, match_cursor: bool) -> bool {
984        self.step_search(false, match_cursor)
985    }
986
987    /// Move the cursor to the previous match. Never extends a selection.
988    pub fn search_back(&mut self, match_cursor: bool) -> bool {
989        self.step_search(true, match_cursor)
990    }
991
992    /// Repeat the persisted pattern (vim `n` / `N`).
993    pub fn search_repeat(&mut self, backward: bool) -> bool {
994        self.step_search(backward, false)
995    }
996
997    fn step_search(&mut self, backward: bool, match_cursor: bool) -> bool {
998        // A search is not a selection gesture, so the anchor goes first — and
999        // here that is one call rather than an invariant to remember, because
1000        // `set_cursor` drops it and `extend_to` keeps it.
1001        self.cancel_selection();
1002        let Some(found) = self.find_match(backward, match_cursor) else {
1003            return false;
1004        };
1005        self.goal = None;
1006        self.inner.set_cursor(found);
1007        true
1008    }
1009
1010    fn find_match(&self, backward: bool, match_cursor: bool) -> Option<Position> {
1011        let pattern = self.search.as_ref()?;
1012        let text = self.inner.text();
1013        let cursor = self.inner.cursor();
1014        let rows = text.line_count();
1015
1016        // `0..=rows` visits the cursor's row twice: once at the start, and once
1017        // more at the end. That last visit IS the wrap, so the hits the first
1018        // visit stepped over — the ones behind the cursor — are exactly what it
1019        // is for. Filtering them again there is what made search unable to come
1020        // back around to them.
1021        for step in 0..=rows {
1022            let wrapped = step == rows;
1023            let row = if backward {
1024                (cursor.row() + rows - (step % rows.max(1))) % rows
1025            } else {
1026                (cursor.row() + step) % rows
1027            };
1028            let line = text.line(row)?;
1029            let mut hits: Vec<usize> = pattern
1030                .find_iter(&line)
1031                .map(|found| line[..found.start()].chars().count())
1032                .collect();
1033            if backward {
1034                hits.reverse();
1035            }
1036            for column in hits {
1037                let same_row = row == cursor.row();
1038                let beyond = if backward {
1039                    column < cursor.column().get()
1040                } else if match_cursor {
1041                    column >= cursor.column().get()
1042                } else {
1043                    column > cursor.column().get()
1044                };
1045                if wrapped || !same_row || beyond {
1046                    // A match can start inside a grapheme cluster — a regex like
1047                    // `.` or a search for a scalar that also appears inside a ZWJ
1048                    // sequence. That start is not addressable, so skip the
1049                    // candidate; abandoning the whole scan there would report "no
1050                    // match" while the bar's own count says otherwise.
1051                    if let Some(at) = text.position(row, Column::new(column)) {
1052                        return Some(at);
1053                    }
1054                }
1055            }
1056        }
1057        None
1058    }
1059
1060    /// The span of the match starting exactly at the cursor, if any.
1061    pub fn match_at_cursor(&self) -> Option<((usize, usize), (usize, usize))> {
1062        let pattern = self.search.as_ref()?;
1063        let text = self.inner.text();
1064        let cursor = self.inner.cursor();
1065        let line = text.line(cursor.row())?;
1066        let byte = line
1067            .char_indices()
1068            .nth(cursor.column().get())
1069            .map(|(at, _)| at)
1070            .unwrap_or(line.len());
1071        let found = pattern.find_at(&line, byte)?;
1072        if found.start() != byte {
1073            return None;
1074        }
1075        let chars = line[found.range()].chars().count();
1076        Some((rc(cursor), (cursor.row(), cursor.column().get() + chars)))
1077    }
1078
1079    // ── Helpers ──────────────────────────────────────────────────────────────
1080
1081    /// `chars` scalars forward of `from`, clamped to the end of the text.
1082    fn forward_by(&self, from: Position, chars: usize) -> Position {
1083        let text = self.inner.text();
1084        let mut at = from;
1085        for _ in 0..chars {
1086            let next = motion::next_cluster(text, at);
1087            if next.byte() == at.byte() {
1088                break;
1089            }
1090            at = next;
1091        }
1092        at
1093    }
1094
1095    /// The span between two `(row, col)` pairs, for callers that still speak in
1096    /// them.
1097    fn span_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<Span> {
1098        let text = self.inner.text();
1099        let from = text.position(start.0, Column::new(start.1))?;
1100        let to = text.position(end.0, Column::new(end.1))?;
1101        text.span(from, to)
1102    }
1103}
1104
1105fn rc(position: Position) -> (usize, usize) {
1106    (position.row(), position.column().get())
1107}
1108
1109#[cfg(test)]
1110mod search_tests {
1111    use super::*;
1112    use crate::ropetext::Text;
1113
1114    fn buffer(text: &str, pattern: &str, cursor: (usize, usize)) -> RopeBuffer {
1115        let mut buf = RopeBuffer::new(Text::from(text));
1116        buf.set_search_pattern(pattern).expect("valid pattern");
1117        buf.move_cursor(CursorMove::Jump(cursor.0, cursor.1));
1118        buf
1119    }
1120
1121    #[test]
1122    fn a_forward_search_wraps_to_a_match_behind_the_cursor() {
1123        // One row, one match, cursor past it. Before the wrap visit stopped
1124        // re-filtering, this reported no match while the find bar counted one.
1125        let mut buf = buffer("xx foo", "foo", (0, 5));
1126        assert!(buf.search_forward(false), "the match is behind the cursor");
1127        assert_eq!(buf.cursor(), (0, 3));
1128    }
1129
1130    #[test]
1131    fn a_backward_search_wraps_to_a_match_ahead_of_the_cursor() {
1132        let mut buf = buffer("xx foo", "foo", (0, 1));
1133        assert!(buf.search_back(false));
1134        assert_eq!(buf.cursor(), (0, 3));
1135    }
1136
1137    #[test]
1138    fn wrapping_crosses_rows_back_to_the_cursors_own_row() {
1139        let mut buf = buffer("aaa\nxx foo", "foo", (1, 5));
1140        assert!(buf.search_forward(false));
1141        assert_eq!(buf.cursor(), (1, 3));
1142    }
1143
1144    #[test]
1145    fn the_only_match_is_re_offered_rather_than_reported_missing() {
1146        // vim's answer: "search hit BOTTOM, continuing at TOP" lands back on the
1147        // same match. Reporting false would paint "no match" over a match that is
1148        // highlighted on screen.
1149        let mut buf = buffer("xx foo", "foo", (0, 3));
1150        assert!(buf.search_forward(false), "the one match is still a match");
1151        assert_eq!(
1152            buf.cursor(),
1153            (0, 3),
1154            "and the cursor has nowhere else to go"
1155        );
1156    }
1157
1158    #[test]
1159    fn a_match_starting_inside_a_cluster_is_skipped_not_fatal() {
1160        // "\u{1F469}\u{200D}\u{1F4BB}" is one cluster; the laptop scalar sits at
1161        // char column 2, inside it. That start is unaddressable — but the real
1162        // match on row 1 is, and abandoning the scan at the first unaddressable
1163        // candidate is what made the bar say "no match" beside a count of two.
1164        let mut buf = buffer(
1165            "\u{1F469}\u{200D}\u{1F4BB}\nx\u{1F4BB}",
1166            "\u{1F4BB}",
1167            (0, 0),
1168        );
1169        assert!(buf.search_forward(false), "the row 1 match is reachable");
1170        assert_eq!(buf.cursor(), (1, 1));
1171    }
1172}
1173
1174#[cfg(test)]
1175mod cluster_tests {
1176    use super::*;
1177    use crate::ropetext::Text;
1178
1179    #[test]
1180    fn delete_str_spends_its_count_on_clusters() {
1181        // "[[" plus a regional-indicator flag: 4 scalars, 3 clusters. Three is
1182        // what removes exactly `[[` and the flag — a caller counting the four
1183        // scalars would take the space after them too.
1184        let mut buf = RopeBuffer::new(Text::from("[[\u{1F1EA}\u{1F1F8} rest"));
1185        buf.move_cursor(CursorMove::Jump(0, 0));
1186        buf.delete_str(3);
1187        assert_eq!(buf.rows(), &[" rest"]);
1188    }
1189
1190    #[test]
1191    fn inserting_before_a_combining_mark_keeps_the_cursor_addressable() {
1192        // A row starting with a lone combining acute — NFD text pasted from
1193        // macOS. Typing 'a' in front of it makes "a\u{301}", one cluster, and
1194        // the post-edit cursor byte lands inside it.
1195        let mut buf = RopeBuffer::new(Text::from("\u{301}f"));
1196        buf.move_cursor(CursorMove::Jump(0, 0));
1197        buf.insert_char('a');
1198        assert_eq!(buf.rows(), &["a\u{301}f"]);
1199    }
1200}
1201
1202#[cfg(test)]
1203mod damage_tests {
1204    use super::*;
1205    use crate::ropetext::Text;
1206
1207    #[test]
1208    fn damage_from_several_edits_is_in_one_numbering() {
1209        // Two mutations in one group, the second ABOVE the first and changing
1210        // the line count — so the first edit's row moves before the group ends.
1211        let mut buf = RopeBuffer::new(Text::from("r0\nr1\nr2\nr3\nr4"));
1212        buf.edit(|b| {
1213            b.move_cursor(CursorMove::Jump(4, 0));
1214            b.insert_str("X");
1215            b.move_cursor(CursorMove::Jump(0, 0));
1216            b.insert_newline();
1217        });
1218        assert_eq!(buf.rows(), ["", "r0", "r1", "r2", "r3", "Xr4"]);
1219
1220        let damage = buf.take_outcome().damage.expect("the edits were reported");
1221        assert!(
1222            damage.contains(&5),
1223            "the row edited first is row 5 once the group ends, but the damage \
1224             reported was {damage:?} — a range in the older numbering"
1225        );
1226    }
1227}
1228
1229#[cfg(test)]
1230mod read_tests {
1231    use super::*;
1232    use crate::ropetext::Text;
1233
1234    #[test]
1235    fn text_between_slices_by_char_columns_across_rows_in_either_order() {
1236        let buf = RopeBuffer::new(Text::from("héllo🦀\nworld"));
1237        assert_eq!(
1238            buf.text_between((0, 1), (1, 2)).as_deref(),
1239            Some("éllo🦀\nwo")
1240        );
1241        assert_eq!(
1242            buf.text_between((1, 2), (0, 1)).as_deref(),
1243            Some("éllo🦀\nwo")
1244        );
1245    }
1246
1247    #[test]
1248    fn an_empty_or_unaddressable_range_is_none() {
1249        let buf = RopeBuffer::new(Text::from("abc"));
1250        assert_eq!(buf.text_between((0, 1), (0, 1)), None);
1251        assert_eq!(buf.text_between((0, 0), (7, 0)), None);
1252    }
1253
1254    #[test]
1255    fn selection_text_is_the_live_selection_or_none() {
1256        let mut buf = RopeBuffer::new(Text::from("hello world"));
1257        assert_eq!(buf.selection_text(), None);
1258        assert!(buf.set_selection((0, 0), (0, 5)));
1259        assert_eq!(buf.selection_text().as_deref(), Some("hello"));
1260        buf.start_selection();
1261        assert_eq!(
1262            buf.selection_text(),
1263            None,
1264            "a zero-width selection is not text"
1265        );
1266    }
1267}
1268
1269#[cfg(test)]
1270mod indent_tests {
1271    use super::*;
1272    use crate::ropetext::Text;
1273
1274    fn buffer(text: &str) -> RopeBuffer {
1275        RopeBuffer::new(Text::from(text))
1276    }
1277
1278    #[test]
1279    fn indent_inserts_one_step_of_spaces_never_a_tab() {
1280        let mut buf = buffer("foo\nbar");
1281        assert!(buf.indent_rows(0..=1, false));
1282        assert_eq!(buf.rows(), &["    foo", "    bar"]);
1283    }
1284
1285    #[test]
1286    fn indent_follows_indent_width() {
1287        let mut buf = buffer("x");
1288        buf.set_indent_width(2);
1289        assert!(buf.indent_rows(0..=0, false));
1290        assert_eq!(buf.rows(), &["  x"]);
1291    }
1292
1293    #[test]
1294    fn dedent_removes_up_to_one_step_of_spaces() {
1295        let mut buf = buffer("        x\n  y\nz");
1296        assert!(buf.indent_rows(0..=2, true));
1297        assert_eq!(buf.rows(), &["    x", "y", "z"]);
1298    }
1299
1300    #[test]
1301    fn dedent_counts_a_leading_tab_as_a_whole_step() {
1302        let mut buf = buffer("\t\tx\n  \ty");
1303        assert!(buf.indent_rows(0..=1, true));
1304        // One tab is one step; the spaces before a tab go with it, and the
1305        // step ends there.
1306        assert_eq!(buf.rows(), &["\tx", "y"]);
1307    }
1308
1309    #[test]
1310    fn dedent_stops_at_a_space_that_carries_a_combining_mark() {
1311        // "  \u{301}foo": the second space and the acute accent are one cluster.
1312        let mut buf = buffer("  \u{301}foo");
1313        assert!(buf.set_selection((0, 3), (0, 6)));
1314        assert!(buf.indent_rows(0..=0, true));
1315        assert_eq!(buf.rows(), &[" \u{301}foo"]);
1316        assert_eq!(buf.selection_range(), Some(((0, 2), (0, 5))));
1317    }
1318
1319    #[test]
1320    fn dedent_with_nothing_to_remove_reports_no_change() {
1321        let mut buf = buffer("foo");
1322        assert!(!buf.indent_rows(0..=0, true));
1323        assert_eq!(buf.rows(), &["foo"]);
1324        assert!(!buf.take_outcome().changed);
1325    }
1326
1327    #[test]
1328    fn rows_past_the_end_are_ignored() {
1329        let mut buf = buffer("a\nb");
1330        assert!(buf.indent_rows(1..=9, false));
1331        assert_eq!(buf.rows(), &["a", "    b"]);
1332        assert!(!buf.indent_rows(5..=9, false));
1333        assert_eq!(buf.rows(), &["a", "    b"]);
1334    }
1335
1336    #[test]
1337    fn the_cursor_keeps_its_character() {
1338        let mut buf = buffer("one\ntwo");
1339        assert!(buf.jump_to(0, 1)); // on 'n'
1340        buf.indent_rows(0..=1, false);
1341        assert_eq!(buf.cursor(), (0, 1 + 4));
1342        buf.indent_rows(0..=1, true);
1343        assert_eq!(buf.cursor(), (0, 1));
1344    }
1345
1346    #[test]
1347    fn a_cursor_outside_the_rows_does_not_move() {
1348        let mut buf = buffer("a\nb\nc");
1349        assert!(buf.jump_to(2, 1));
1350        buf.indent_rows(0..=1, false);
1351        assert_eq!(buf.cursor(), (2, 1));
1352    }
1353
1354    #[test]
1355    fn a_dedent_never_pushes_the_cursor_below_column_zero() {
1356        let mut buf = buffer("    x");
1357        assert!(buf.jump_to(0, 2));
1358        buf.indent_rows(0..=0, true);
1359        assert_eq!(buf.cursor(), (0, 0));
1360    }
1361
1362    #[test]
1363    fn the_selection_is_put_back_shifted_with_its_rows() {
1364        let mut buf = buffer("hello world\nnext");
1365        assert!(buf.set_selection((0, 6), (1, 2)));
1366        buf.indent_rows(0..=1, false);
1367        assert_eq!(buf.selection_range(), Some(((0, 10), (1, 6))));
1368        assert_eq!(buf.rows()[0].trim_start(), "hello world");
1369    }
1370
1371    #[test]
1372    fn a_dedent_under_a_selection_shifts_each_endpoint_by_its_own_row() {
1373        let mut buf = buffer("    foo\n  bar\nbaz");
1374        assert!(buf.set_selection((0, 4), (2, 3)));
1375        assert!(buf.indent_rows(0..=2, true));
1376        assert_eq!(buf.rows(), &["foo", "bar", "baz"]);
1377        assert_eq!(buf.selection_range(), Some(((0, 0), (2, 3))));
1378    }
1379
1380    #[test]
1381    fn a_block_indent_is_one_undo_group() {
1382        let mut buf = buffer("a\nb\nc");
1383        buf.indent_rows(0..=2, false);
1384        assert!(buf.undo(), "the block is one entry");
1385        assert_eq!(buf.rows(), &["a", "b", "c"]);
1386        assert!(!buf.undo(), "and has nothing left to take back");
1387    }
1388
1389    #[test]
1390    fn undo_and_redo_of_an_indent_land_the_cursor_on_its_own_character() {
1391        let mut buf = buffer("one\ntwo");
1392        assert!(buf.jump_to(0, 1)); // on 'n'
1393        buf.indent_rows(0..=1, false);
1394        assert_eq!(buf.cursor(), (0, 5));
1395        assert!(buf.undo());
1396        assert_eq!(buf.cursor(), (0, 1), "undo returns to where the user was");
1397        assert!(buf.redo());
1398        assert_eq!(
1399            buf.cursor(),
1400            (0, 5),
1401            "redo lands where the indent left them"
1402        );
1403    }
1404
1405    #[test]
1406    fn a_block_indent_is_one_transaction() {
1407        let mut buf = buffer("a\nb\nc");
1408        buf.indent_rows(0..=2, false);
1409        let outcome = buf.take_outcome();
1410        assert!(outcome.changed);
1411        assert!(outcome.bulk, "three rows in one change");
1412        assert!(buf.undo());
1413        assert_eq!(buf.rows(), &["a", "b", "c"]);
1414        assert!(!buf.undo());
1415    }
1416
1417    #[test]
1418    fn a_dedent_does_not_fill_the_yank_transport() {
1419        let mut buf = buffer("    x");
1420        buf.set_yank_text("kept");
1421        buf.indent_rows(0..=0, true);
1422        assert_eq!(buf.yank_text(), "kept");
1423    }
1424
1425    #[test]
1426    fn damage_covers_every_touched_row() {
1427        let mut buf = buffer("a\nb\nc\nd");
1428        buf.indent_rows(1..=2, false);
1429        let outcome = buf.take_outcome();
1430        assert!(outcome.changed);
1431        let damage = outcome.damage.expect("the edits were reported");
1432        assert!(damage.contains(&1) && damage.contains(&2), "{damage:?}");
1433        assert_eq!(outcome.line_delta, 0);
1434    }
1435}
1436
1437#[cfg(test)]
1438mod mark_tests {
1439    use super::*;
1440    use crate::ropetext::Text;
1441
1442    /// A mark keeps pointing at its text: rows added above carry it down,
1443    /// text typed before it on its row carries it right, and deleting the
1444    /// rows above brings it back up.
1445    #[test]
1446    fn marks_follow_the_text() {
1447        let mut t = RopeBuffer::new(Text::from("one\ntwo"));
1448        assert!(t.set_mark('<', (1, 1)));
1449        t.jump_to(0, 0);
1450        t.insert_str("zero\n");
1451        assert_eq!(t.mark('<'), Some((2, 1)));
1452        t.jump_to(2, 0);
1453        t.insert_str("XX");
1454        assert_eq!(t.mark('<'), Some((2, 3)));
1455        t.jump_to(0, 0);
1456        t.start_selection();
1457        t.jump_to(1, 0);
1458        t.cut();
1459        assert_eq!(t.mark('<'), Some((1, 3)));
1460    }
1461
1462    /// Undo swaps the text in whole; the mark still lands on its rows.
1463    #[test]
1464    fn marks_follow_undo_and_a_new_text_drops_them() {
1465        let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
1466        t.set_mark('>', (2, 0));
1467        t.jump_to(0, 0);
1468        t.insert_str("x\n");
1469        assert_eq!(t.mark('>'), Some((3, 0)));
1470        t.undo();
1471        assert_eq!(t.mark('>'), Some((2, 0)));
1472        t.replace(Text::from("other"));
1473        assert_eq!(t.mark('>'), None);
1474    }
1475
1476    /// An undo can restore a row whose characters sit differently: a mark
1477    /// whose column now falls inside an emoji moves on to the next whole
1478    /// character, as a mapped one does — not back to column 0.
1479    #[test]
1480    fn a_mark_undo_leaves_inside_a_character_moves_to_the_next() {
1481        let mut t = RopeBuffer::new(Text::from("ab❤️x"));
1482        t.jump_to(0, 2);
1483        t.start_selection();
1484        t.jump_to(0, 4);
1485        t.cut();
1486        assert_eq!(t.rows(), &["abx"]);
1487        assert!(t.set_mark('<', (0, 3)));
1488        t.undo();
1489        assert_eq!(t.rows(), &["ab❤️x"]);
1490        assert_eq!(t.mark('<'), Some((0, 4)));
1491    }
1492}