Skip to main content

scrive_core/
document.rs

1//! The document aggregate: the buffer plus its edit history and every derived
2//! view (selections, decorations, highlight cache, brackets, folds, find).
3//!
4//! [`Document`] is the public choke point for changing text. Every edit goes
5//! through [`Document::edit`]/[`Document::edit_grouped`], which run the
6//! transaction engine and record the inverse for undo — so a caller can never
7//! mutate the buffer behind history's back. The one invariant this module rests
8//! on: every position-tracked derived fact rides a committed edit's patch
9//! through the single mover `rebase_views`, on both the forward path and
10//! undo/redo, so no two copies of one position can drift apart.
11
12use core::ops::Range;
13
14use std::borrow::Cow;
15
16use crate::bracket::Brackets;
17use crate::buffer::{Buffer, EolFlavor, LoadError, Revision, Snapshot};
18use crate::coords::{Bias, Point};
19use crate::decorations::{
20    DecorationKind, DecorationStore, Diagnostic, DiagnosticsOutcome, Severity, Stickiness,
21    TrackedRange,
22};
23use crate::display_map::{BufferRow, DisplayRow};
24use crate::find::{FindQuery, FindState};
25use std::cell::{Ref, RefCell};
26
27use crate::fold_map::{FoldMap, FoldSet};
28use crate::highlight::{HighlightCache, HighlightEngine, HighlightSpan, SyntaxDef, TokenTheme};
29use crate::history::{GroupingHint, History};
30use crate::movement::{self, ColumnDir, Granularity, Motion};
31use crate::selection::{Selection, SelectionId, SelectionSet};
32use crate::transaction::{apply, Committed, EditOp, TransactionError};
33
34/// A text document: buffer + undo history + selections + every derived view.
35#[derive(Debug)]
36pub struct Document {
37    pub(crate) buffer: Buffer,
38    history: History,
39    pub(crate) selections: SelectionSet,
40    /// The anchored rectangle while in column (box) selection mode. `Some`
41    /// only between consecutive Ctrl+Shift+Alt+Arrow presses; any other action
42    /// clears it, leaving the box as an ordinary multi-cursor set.
43    column: Option<ColumnSelection>,
44    /// The incremental syntax-highlight cache, present once the app injects
45    /// a grammar+theme via [`Document::set_syntax`]. Spliced on every commit.
46    highlight: Option<HighlightCache>,
47    /// Matched brackets — spliced incrementally on every edit; drives bracket
48    /// colorization and the matching-bracket highlight.
49    brackets: Brackets,
50    /// Tracked-range decoration store: diagnostic squiggles, find matches, and
51    /// snippet stops. Rides every forward edit's patch in the commit path so
52    /// positions stay reunited with content (one store, one mover).
53    decorations: DecorationStore,
54    /// Auto-close provenance is store-as-truth: the `AutoClosePair` decorations
55    /// in THIS store ARE the live pairs (one per caret that auto-closed, so
56    /// multi-caret), each spanning `[open char start, close char start)`. It is
57    /// its OWN [`DecorationStore`], separate from `decorations`, because a pair
58    /// is a small (≤ caret-count), self-invalidating, per-gesture set: giving it
59    /// a dedicated store keeps every arm/clear/validate O(pairs) instead of
60    /// scanning the bulk (10k-find-match / diagnostic) store. There is no
61    /// separate handle to keep in sync — the store itself is the truth: the
62    /// pairs ride every edit through the one mover (`rebase_views` moves this
63    /// store beside `decorations`), `validate_autoclose` drops the ones no caret
64    /// still occupies, and emptiness is read straight off the store
65    /// (`!self.autoclose.is_empty()`).
66    autoclose: DecorationStore,
67    /// Active code folds — **view state**, never on the buffer or the undo
68    /// stack. Rides the commit-path patch mover (see `rebase_views`) like a
69    /// decoration, so folds survive edits and undo/redo with no fold-specific
70    /// resync. Queried per frame through a `FoldMap` (never stored).
71    folds: FoldSet,
72    /// Find view-state: the active query, the active match's decoration id, and
73    /// the coverage/cap bookkeeping. The match set itself lives ONLY in
74    /// `decorations` (the sorted `FindMatch` set IS the set) and is repaired
75    /// eagerly per commit via `rebase_views`. Idle until `set_find_query`.
76    find: FindState,
77    /// The language's line-comment prefix (e.g. `//`), injected by the app
78    /// like the grammar — the core ships no language knowledge. `None`
79    /// makes [`Document::toggle_line_comment`] a no-op.
80    line_comment: Option<String>,
81    /// The expand-selection ladder: each [`Document::expand_selection`]
82    /// pushes the pre-expansion set so `shrink_selection` can walk back down.
83    /// Transient like `column` — any other gesture or edit clears it.
84    expand_stack: Vec<SelectionSet>,
85    /// Monotonic reveal-request counter: bumped by actions that move the caret
86    /// programmatically — outside the widget's own input path — so the widget
87    /// can autoscroll to the new caret. The view compares it to its last-seen
88    /// value and reveals once on a change (a generation counter rather than an
89    /// `Option<request>`, since a `&Document` renderer can't take or clear a
90    /// request at layout). Verbs bump it via [`Document::request_reveal`] ONLY
91    /// when they actually changed something — a no-op F8 or bracket jump never
92    /// scrolls.
93    reveal_seq: u64,
94    /// The strategy of the last reveal request.
95    reveal_mode: RevealMode,
96    /// Memoized [`FoldMap`]. The render path queries it ~20× per frame, so it is
97    /// cached rather than rebuilt (a fresh build is O(folds), which at document
98    /// scale with everything collapsed would stall scrolling). A fold toggle
99    /// ([`FoldSet::generation`]) or a line-count change rebuilds from scratch; a
100    /// no-line-change edit shifts the cached map in place via
101    /// [`FoldMap::apply_patch`] (driven by [`Document::edit_grouped`]), so plain
102    /// typing over a document-scale fold set costs O(edit), not an O(folds)
103    /// rebuild per keystroke. Because it is shifted incrementally it could in
104    /// principle drift, so the drift oracle
105    /// (`fold_map_cache_matches_a_fresh_build_across_changes`) deep-equals it
106    /// against a fresh build to keep it honest.
107    fold_cache: RefCell<FoldMapCache>,
108}
109
110/// The document's memoized [`FoldMap`] plus the `(buffer revision, fold
111/// generation)` key it was built at.
112#[derive(Debug)]
113struct FoldMapCache {
114    key: (u64, u64),
115    map: FoldMap,
116}
117
118/// How a reveal request should autoscroll the view to the newest caret.
119#[derive(Copy, Clone, Debug, PartialEq, Eq)]
120pub enum RevealMode {
121    /// Fit the newest caret with the margin band, but HOLD the viewport if any
122    /// cursor is already on screen — a multi-cursor op (select-all-occurrences,
123    /// multi-cursor typing) must not scroll away from cursors the user can
124    /// already see. Identical to a plain fit for a single caret.
125    Fit,
126    /// Always fit the newest caret, even when other cursors are on screen — a
127    /// deliberate jump to a just-added cursor (Ctrl+D add-next-occurrence).
128    FitForce,
129    /// Center the target row (find / diagnostic navigation).
130    Center,
131}
132
133/// The two corners of a column (box) selection, in DISPLAY space:
134/// display *cells*, not byte columns — tabs and collapsed inline folds make
135/// the two disagree, and the box is a visual rectangle. Every spanned row
136/// resolves the same two cells through the one click inverse
137/// ([`FoldMap::hit_row`]), clamping to its own content, so the box stays
138/// rectangular over ragged lines and shrinks exactly when the active corner
139/// moves back toward the anchor.
140#[derive(Copy, Clone, Debug)]
141struct ColumnSelection {
142    anchor: CellCorner,
143    active: CellCorner,
144}
145
146/// One box corner: a buffer row (visible when the corner was placed) and a
147/// *virtual* display cell — the cell may sit past the row's content.
148#[derive(Copy, Clone, Debug)]
149struct CellCorner {
150    row: u32,
151    cell: u32,
152}
153
154impl Document {
155    /// Load `text` as a new document (see [`Buffer::new`] for the load
156    /// contract: size limit, CRLF normalization, EOL-flavor detection). Starts
157    /// with a single caret at the top.
158    ///
159    /// [`Buffer::new`]: crate::Buffer::new
160    pub fn new(text: &str) -> Result<Self, LoadError> {
161        let buffer = Buffer::new(text)?;
162        let brackets = Brackets::match_text(&buffer.text());
163        Ok(Self {
164            buffer,
165            history: History::new(),
166            selections: SelectionSet::new(0),
167            column: None,
168            highlight: None,
169            brackets,
170            decorations: DecorationStore::new(),
171            autoclose: DecorationStore::new(),
172            folds: FoldSet::new(),
173            find: FindState::new(),
174            line_comment: None,
175            expand_stack: Vec::new(),
176            reveal_seq: 0,
177            reveal_mode: RevealMode::Fit,
178            // Seed with a never-matching key so the first `fold_map()` builds it.
179            fold_cache: RefCell::new(FoldMapCache { key: (u64::MAX, u64::MAX), map: FoldMap::empty() }),
180        })
181    }
182
183    /// The memoized fold projection — buffer rows ↔ display rows, hidden
184    /// interiors, hit-testing. Rebuilt from scratch only when the buffer revision
185    /// or the [`FoldSet`] generation changed since the cached build; otherwise the
186    /// cached map is returned untouched, so the render path's ~20 per-frame calls
187    /// cost O(1) instead of O(folds) each. The one owner every fold query flows
188    /// through.
189    #[must_use]
190    pub fn fold_map(&self) -> Ref<'_, FoldMap> {
191        self.ensure_fold_map();
192        Ref::map(self.fold_cache.borrow(), |c| &c.map)
193    }
194
195    /// Rebuild the fold cache iff its inputs changed, leaving it current. Split
196    /// from [`fold_map`](Self::fold_map) so a caller that also needs `&mut` on a
197    /// sibling field (e.g. `move_carets` mutating `selections`) can freshen the
198    /// cache and then borrow `fold_cache` and that field disjointly — instead of
199    /// building a throwaway O(folds) `FoldMap::new` per keystroke.
200    fn ensure_fold_map(&self) {
201        let key = (self.buffer.revision().0, self.folds.generation());
202        if self.fold_cache.borrow().key != key {
203            let map = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
204            *self.fold_cache.borrow_mut() = FoldMapCache { key, map };
205        }
206    }
207
208    /// Ask the view to reveal the newest selection: bump the generation
209    /// and record the strategy ([`RevealMode`]). The ONE way a core verb
210    /// requests autoscroll; call it only after actually changing state.
211    fn request_reveal(&mut self, mode: RevealMode) {
212        self.reveal_seq += 1;
213        self.reveal_mode = mode;
214    }
215
216    /// The [`RevealMode`] of the pending reveal request.
217    #[must_use]
218    pub fn reveal_mode(&self) -> RevealMode {
219        self.reveal_mode
220    }
221
222    /// Inject the language's line-comment prefix (e.g. `"//"`) — app-supplied
223    /// configuration, like [`Document::set_syntax`]; `None` disables
224    /// toggle-comment.
225    pub fn set_line_comment(&mut self, prefix: Option<&str>) {
226        self.line_comment = prefix.map(str::to_owned);
227    }
228
229    /// The injected line-comment prefix, if any.
230    #[must_use]
231    pub fn line_comment(&self) -> Option<&str> {
232        self.line_comment.as_deref()
233    }
234
235    /// Read-only access to the underlying buffer (text, lines, coordinates,
236    /// snapshots).
237    #[must_use]
238    pub fn buffer(&self) -> &Buffer {
239        &self.buffer
240    }
241
242    /// The current selection set (carets and ranges).
243    #[must_use]
244    pub fn selections(&self) -> &SelectionSet {
245        &self.selections
246    }
247
248    /// Replace the selection set (e.g. a mouse click placing a caret). Offsets
249    /// are clamped to the buffer.
250    pub fn set_selections(&mut self, selections: SelectionSet) {
251        self.reset_transient();
252        self.selections = selections;
253    }
254
255    /// Move (or, with `extend`, drag) every caret by `motion`. Vertical
256    /// motion is fold-aware: the caret steps display rows, skipping folds.
257    pub fn move_carets(&mut self, motion: Motion, extend: bool) {
258        self.reset_transient();
259        // Read the MEMOIZED fold map, not a throwaway `FoldMap::new` per arrow
260        // press: a fresh build is O(folds), which janks at document scale with
261        // everything collapsed. Freshen the cache, then borrow `fold_cache` and
262        // `selections` disjointly.
263        self.ensure_fold_map();
264        let tab = self.tab_size();
265        let cache = self.fold_cache.borrow();
266        movement::move_selections(&mut self.selections, &self.buffer, &cache.map, tab, motion, extend);
267    }
268
269    /// Add a bare caret at `offset` — the Alt+Click add-caret gesture.
270    /// Merges into any selection it touches via the set's merge rule.
271    pub fn add_caret(&mut self, offset: u32) {
272        self.reset_transient();
273        self.selections.add_caret(offset);
274    }
275
276    /// Ctrl+D. If the newest selection is an empty caret, select the word
277    /// surrounding it; otherwise add the next literal occurrence of the newest
278    /// selection's text as a new (and now newest) selection — scanning forward
279    /// from its end, wrapping to the document start, skipping already-selected
280    /// ranges. No word / no further match ⇒ no change. Selection-only, no edit.
281    pub fn add_next_occurrence(&mut self) {
282        self.reset_transient();
283        let newest = *self.selections.newest();
284        if newest.is_empty() {
285            // First press: expand the caret to its word. `add_selection` mints a
286            // newer id, so the range absorbs the caret and becomes the newest.
287            if let Some((ws, we)) = movement::surrounding_word(&self.buffer, newest.head()) {
288                if ws != we {
289                    self.selections.add_selection(ws, we);
290                    // Ctrl+D jumps to the just-added cursor even if others are on
291                    // screen — a deliberate FitForce, not the hold-if-visible Fit
292                    // that select-all-occurrences uses.
293                    self.request_reveal(RevealMode::FitForce);
294                }
295            }
296            return;
297        }
298        // Read just the needle, not the whole rope: reading the selection alone
299        // keeps each press O(needle), never O(document).
300        let needle = self.buffer.slice(newest.start()..newest.end()).into_owned();
301        let len = needle.len() as u32;
302        // Taken-check by binary search over the selection starts (already sorted
303        // by start), not an O(carets) linear `any` per candidate.
304        let taken: Vec<(u32, u32)> =
305            self.selections.all().iter().map(|s| (s.start(), s.end())).collect();
306        let is_taken = |s: u32| {
307            taken.binary_search_by_key(&s, |&(st, _)| st).is_ok_and(|i| taken[i].1 == s + len)
308        };
309        // Next literal occurrence at/after the caret, wrapping to the start —
310        // windowed over the buffer, never materializing it.
311        let from = newest.end();
312        let found = scan_from(&self.buffer, &needle, from, self.buffer.len(), &is_taken)
313            .or_else(|| scan_from(&self.buffer, &needle, 0, from, &is_taken));
314        if let Some((start, end)) = found {
315            self.selections.add_selection(start, end);
316            // Reveal the newly added cursor (FitForce — jump even if other
317            // cursors are already on screen).
318            self.request_reveal(RevealMode::FitForce);
319        }
320    }
321
322    /// Ctrl+Shift+\: move each caret to its matching bracket. Adjacent to
323    /// a matched bracket (the [`Brackets::active_pair`] rule — left of the
324    /// caret preferred), the caret crosses to the SAME side of the partner, so
325    /// a second press returns it. With no adjacent bracket it jumps to the
326    /// innermost enclosing pair's closer. No bracket in reach ⇒ the
327    /// caret stays. Collapses selections to carets (a plain motion).
328    pub fn jump_to_bracket(&mut self) {
329        self.reset_transient();
330        let targets: Vec<u32> = self
331            .selections
332            .all()
333            .iter()
334            .map(|s| {
335                let head = s.head();
336                if let Some((b, partner)) = self.brackets.active_pair(head) {
337                    if head == b + 1 {
338                        partner + 1
339                    } else {
340                        partner
341                    }
342                } else if let Some((_, close)) = self.brackets.enclosing_pair(head) {
343                    close
344                } else {
345                    head
346                }
347            })
348            .collect();
349        let moved =
350            targets.iter().zip(self.selections.all()).any(|(&t, s)| t != s.head() || !s.is_empty());
351        self.selections = SelectionSet::from_offsets(&targets);
352        for &t in &targets {
353            self.unfold_to_reveal(t); // a jump lands on a visible position
354        }
355        if moved {
356            self.request_reveal(RevealMode::Fit); // a bracket hop Fit-reveals
357        }
358    }
359
360    /// Shift+Alt+Right: grow every selection one structural step — caret
361    /// → word → bracket contents → bracket pair → outward, ending at the whole
362    /// document — pushing the previous set so [`Document::shrink_selection`]
363    /// can walk back down. Fully expanded is a no-op (nothing pushed). The
364    /// ladder clears on any other gesture or edit. Selection-only.
365    pub fn expand_selection(&mut self) {
366        self.reset_gesture();
367        let before: Vec<(u32, u32)> =
368            self.selections.all().iter().map(|s| (s.start(), s.end())).collect();
369        let ranges: Vec<(u32, u32)> =
370            before.iter().map(|&(s, e)| self.expanded_range(s, e)).collect();
371        if ranges == before {
372            return;
373        }
374        let newest_id = self.selections.newest().id;
375        let newest = self
376            .selections
377            .all()
378            .iter()
379            .position(|s| s.id == newest_id)
380            .expect("the newest selection is in its own set");
381        self.expand_stack.push(self.selections.clone());
382        self.selections = SelectionSet::from_ranges(&ranges, newest);
383        self.request_reveal(RevealMode::Fit); // keep the growing head in view
384    }
385
386    /// Shift+Alt+Left: step back down the expansion ladder — restore the
387    /// set exactly as it was before the matching expand. Empty ladder ⇒ no-op.
388    pub fn shrink_selection(&mut self) {
389        self.reset_gesture();
390        if let Some(prev) = self.expand_stack.pop() {
391            self.selections = prev;
392            self.request_reveal(RevealMode::Fit);
393        }
394    }
395
396    /// One structural expansion step for `[start, end]`: an empty caret
397    /// grows to its word; otherwise the innermost pair whose contents contain
398    /// the range supplies the rung — its contents first, then (already filled)
399    /// the pair including its brackets, whose own enclosing pair is one level
400    /// out. Outside every pair: the whole document.
401    fn expanded_range(&self, start: u32, end: u32) -> (u32, u32) {
402        if start == end {
403            if let Some((ws, we)) = movement::surrounding_word(&self.buffer, start) {
404                if ws != we {
405                    return (ws, we);
406                }
407            }
408        }
409        if let Some((open, close)) = self.brackets.enclosing_pair_of_range(start, end) {
410            let contents = (crate::row_layout::gap_left_edge(open), close);
411            if contents != (start, end) {
412                return contents;
413            }
414            return (open, close + 1);
415        }
416        (0, self.buffer.len())
417    }
418
419    /// Ctrl+Alt+↑/↓: add a caret one display row above/below EVERY
420    /// existing caret, keeping the current set — the stack-a-column gesture.
421    /// Each landing resolves like a click at that caret's visual column (the
422    /// `movement::caret_one_display_row` rule → `hit_row`), so tabs, chips,
423    /// and collapsed folds behave exactly as plain vertical movement; a caret
424    /// already on the first/last display row adds nothing, and a landing on an
425    /// existing caret merges via the set's rule. Selection-only. (Landings on
426    /// short lines clamp; the visual goal column is not carried across
427    /// presses — a deliberate simplification.)
428    pub fn add_caret_vertical(&mut self, down: bool) {
429        self.reset_transient();
430        let folds = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
431        let tab = self.tab_size();
432        let delta = if down { 1 } else { -1 };
433        let heads: Vec<u32> = self.selections.all().iter().map(Selection::head).collect();
434        let mut added = false;
435        for head in heads {
436            if let Some(off) = movement::caret_one_display_row(&self.buffer, &folds, tab, head, delta)
437            {
438                self.selections.add_caret(off);
439                added = true;
440            }
441        }
442        if added {
443            self.request_reveal(RevealMode::Fit); // keep the newest caret in view
444        }
445    }
446
447    /// Ctrl+Shift+L: select EVERY occurrence of the newest selection's
448    /// text as a multi-cursor set, seeding from the word under a bare caret
449    /// (like Ctrl+D) — the multi-cursor rename: select all, type once.
450    /// Single-pass: ONE scan collects every non-overlapping occurrence (the
451    /// same `match_indices` rule `find_next_occurrence` uses), skips
452    /// already-taken selection ranges, and adds the rest in cyclic order —
453    /// forward from the seed, wrapping — so the final `newest` (the reveal
454    /// target) is the last occurrence in that order. Selection-only, no edit.
455    pub fn select_all_occurrences(&mut self) {
456        self.reset_transient();
457        let newest = *self.selections.newest();
458        if newest.is_empty() {
459            let Some((ws, we)) = movement::surrounding_word(&self.buffer, newest.head()) else {
460                return;
461            };
462            if ws == we {
463                return;
464            }
465            self.selections.add_selection(ws, we);
466        }
467        let newest = *self.selections.newest();
468        // Existing selections stay in the set; an occurrence already covered by
469        // one merges into it (normalize dedups), so the taken filter only needs
470        // to skip re-listing them — an O(occurrences) HashSet probe, not an
471        // O(occurrences × selections) `Vec::contains`.
472        let existing: Vec<(u32, u32)> =
473            self.selections.all().iter().map(|s| (s.start(), s.end())).collect();
474        let taken: std::collections::HashSet<(u32, u32)> = existing.iter().copied().collect();
475        let occurrences: Vec<(u32, u32)> = {
476            let text = self.buffer.text();
477            let needle = &text[newest.start() as usize..newest.end() as usize];
478            if needle.is_empty() {
479                return;
480            }
481            let len = needle.len() as u32;
482            text.match_indices(needle)
483                .map(|(i, _)| (i as u32, i as u32 + len))
484                .filter(|occ| !taken.contains(occ))
485                .collect()
486        };
487        if occurrences.is_empty() {
488            // Every match is already selected — the set is unchanged; just reveal.
489            let head = self.selections.newest().head();
490            self.unfold_to_reveal(head);
491            self.request_reveal(RevealMode::Fit);
492            return;
493        }
494        // Build the whole set in one shot: `from_ranges` runs a SINGLE normalize
495        // (O(m log m)); adding occurrences one at a time would re-normalize on
496        // every insert (O(m²)). Reveal target = the last occurrence in the
497        // forward-cyclic order (`[wrap.., ..wrap]`), the one nearest before the
498        // seed, so the viewport reveal lands where the user expects.
499        let wrap = occurrences.partition_point(|&(s, _)| s < newest.end());
500        let reveal = (wrap + occurrences.len() - 1) % occurrences.len();
501        let base = existing.len();
502        let mut ranges = existing;
503        ranges.extend_from_slice(&occurrences);
504        self.selections = SelectionSet::from_ranges(&ranges, base + reveal);
505        // The newest occurrence may sit inside a collapsed fold — unfold it
506        // and Fit-reveal, so the reveal lands on a visible position rather
507        // than a hidden one the widget could not scroll to.
508        let head = self.selections.newest().head();
509        self.unfold_to_reveal(head);
510        self.request_reveal(RevealMode::Fit);
511    }
512
513    /// Every live find match becomes a selection (the find bar's
514    /// select-all-matches, Alt+Enter) — the multi-cursor "replace all": select
515    /// every match, type once. The active match
516    /// (or the first) becomes the newest selection, so the caret stays where
517    /// find navigation left it. No live matches ⇒ `false`, nothing changes.
518    pub fn select_find_matches(&mut self) -> bool {
519        let ranges: Vec<(u32, u32)> =
520            self.find_matches_in(0..u32::MAX).map(|(r, _)| (r.start, r.end)).collect();
521        if ranges.is_empty() {
522            return false;
523        }
524        let newest = (self.active_find_match().unwrap_or(0) as usize).min(ranges.len() - 1);
525        self.selections = SelectionSet::from_ranges(&ranges, newest);
526        self.reset_transient(); // a jump-class change: seals undo, clears box state
527        // The active match must be visible (matches hidden in folds stay as
528        // selections — typing into them expands via the edit path).
529        let head = self.selections.newest().head();
530        self.unfold_to_reveal(head);
531        self.request_reveal(RevealMode::Center); // find-family jumps center
532        true
533    }
534
535    /// Collapse a multi-cursor set (or a single range) to one caret — the primary
536    /// (oldest) cursor. The Escape gesture.
537    pub fn collapse_selections(&mut self) {
538        self.reset_transient();
539        self.selections.collapse_to_primary();
540    }
541
542    /// Select the whole document — one selection from the start to the end, head
543    /// at the end (Ctrl+A). Selection-only, no edit; an empty buffer stays a
544    /// single caret at 0.
545    pub fn select_all(&mut self) {
546        self.reset_transient();
547        let end = self.buffer.len();
548        self.selections.set_single(Selection::from_anchor(SelectionId(0), 0, end));
549    }
550
551    /// Clear transient per-gesture state — the column (box) anchor, the
552    /// expand-selection ladder, and the auto-close provenance — and **seal the
553    /// open undo group**: every caret move, click, or jump is an undo-group
554    /// boundary, so typing after it never merges with the run before it.
555    /// Called by every selection-changing verb that isn't a type/backspace (a
556    /// plain type keeps the provenance so overtype works, and stays mergeable).
557    /// One owner: a new selection verb gets the boundary by calling this, never
558    /// by sealing directly.
559    fn reset_transient(&mut self) {
560        self.expand_stack.clear();
561        self.reset_gesture();
562    }
563
564    /// [`reset_transient`](Self::reset_transient) minus the expand ladder —
565    /// for the expand/shrink verbs themselves, which own that stack but are
566    /// still selection gestures (box exit, provenance drop, undo boundary).
567    fn reset_gesture(&mut self) {
568        self.column = None;
569        self.clear_autoclose();
570        self.history.seal();
571    }
572
573    /// Record fresh auto-close provenance for the given `[open, close)`
574    /// pairs — one `AutoClosePair` decoration each, added in ONE batch (a
575    /// multi-caret keystroke pairs at every caret; per-item adds would be
576    /// quadratic). `AlwaysGrows` so typing at the caret (on the close boundary)
577    /// pushes the close along while the open stays put; the store then moves them
578    /// through every edit. The pairs live in their OWN [`autoclose`](Self::autoclose)
579    /// store (≤ caret-count items), so this batch add is O(pairs), never O(decorations).
580    pub(crate) fn add_autoclose_pairs(&mut self, pairs: impl IntoIterator<Item = (u32, u32)>) {
581        self.autoclose.add_sorted_batch(
582            pairs.into_iter().map(|(open, close)| open..close),
583            DecorationKind::AutoClosePair,
584            Stickiness::AlwaysGrows,
585        );
586    }
587
588    /// Every live provenance range `[open_char_start, close_char_start)`, in
589    /// ascending order — empty when no pair is active. The pairs are
590    /// disjoint, so the ends are ascending too (overtype relies on this).
591    /// O(pairs): the own store holds only `AutoClosePair`, so no kind filter is
592    /// needed.
593    pub(crate) fn autoclose_ranges(&self) -> Vec<Range<u32>> {
594        self.autoclose.iter().map(|r| r.range.clone()).collect()
595    }
596
597    /// Drop ALL auto-close provenance (its decorations). O(pairs): the own store
598    /// holds only the ≤(caret-count) pairs, so this empties it directly — no
599    /// whole-`decorations` retain, and no dirty flag to reset.
600    pub(crate) fn clear_autoclose(&mut self) {
601        if self.autoclose.is_empty() {
602            return; // no live pair — nothing to take
603        }
604        self.autoclose.take_matching_in(0..u32::MAX, |_| true);
605    }
606
607    /// Drop each provenance pair no caret still validly occupies — its line
608    /// changed, or no caret sits in `(open, close]`. Reads the *rebased* pairs,
609    /// so it runs after the commit mover. The carets are sorted, so each pair is
610    /// probed in O(log carets); the scan is over the own store (≤ caret-count),
611    /// so validate is O(pairs · log carets), independent of `decorations`.
612    pub(crate) fn validate_autoclose(&mut self) {
613        if self.autoclose.is_empty() {
614            return; // no live pair — nothing to validate
615        }
616        let carets: Vec<u32> = self.selections.all().iter().map(|s| s.head()).collect();
617        let Self { autoclose, buffer, .. } = self;
618        autoclose.take_matching_in(0..u32::MAX, |r| {
619            debug_assert!(
620                matches!(r.kind, DecorationKind::AutoClosePair),
621                "the autoclose store holds only AutoClosePair provenance",
622            );
623            let (start, end) = (r.range.start, r.range.end);
624            let one_line = buffer.offset_to_point(start).row == buffer.offset_to_point(end).row;
625            // A caret validly occupies the pair when it is in (open, close].
626            let occupied = one_line && {
627                let i = carets.partition_point(|&h| h <= start);
628                i < carets.len() && carets[i] <= end
629            };
630            !occupied // remove the pair no caret occupies
631        });
632    }
633
634    /// Drag-select from `origin` to `head` at the given `granularity`. Char
635    /// is a plain range; Word/Line extend by whole units and keep the origin unit
636    /// fully selected even when the drag reverses past it (the tail flips to the
637    /// far edge of the origin unit). `head == origin` selects just the origin
638    /// unit — the double/triple-click initial selection. Replaces the set.
639    pub fn drag_select(&mut self, granularity: Granularity, origin: u32, head: u32) {
640        self.reset_transient();
641        let (anchor, head) = match granularity {
642            Granularity::Char => (origin, head),
643            Granularity::Word => {
644                extend_by_unit(self.word_unit(origin), self.word_unit(head), origin, head)
645            }
646            Granularity::Line => {
647                extend_by_unit(self.line_unit(origin), self.line_unit(head), origin, head)
648            }
649        };
650        self.selections.set_single(Selection::from_anchor(SelectionId(0), anchor, head));
651    }
652
653    /// The word range surrounding `offset`, or a bare caret there in whitespace.
654    fn word_unit(&self, offset: u32) -> (u32, u32) {
655        movement::surrounding_word(&self.buffer, offset).unwrap_or((offset, offset))
656    }
657
658    /// The line range at `offset`, including the trailing newline (or to line
659    /// end on the final line). THE whole-line unit — triple-click drag and the
660    /// whole-line Copy/Cut/Paste verbs all read this one rule.
661    pub(crate) fn line_unit(&self, offset: u32) -> (u32, u32) {
662        let row = self.buffer.offset_to_point(offset).row;
663        let start = self.buffer.point_to_offset(Point::new(row, 0));
664        let end = if row + 1 < self.buffer.line_count() {
665            self.buffer.point_to_offset(Point::new(row + 1, 0))
666        } else {
667            self.buffer.point_to_offset(Point::new(row, self.buffer.line_len(row)))
668        };
669        (start, end)
670    }
671
672    /// Column (box) selection — Ctrl+Shift+Alt+Arrow. The first press anchors
673    /// at the primary caret; each press steps the *active* corner by `dir` and
674    /// rebuilds one selection per spanned row, from the anchor cell to the active
675    /// cell. Stepping the active corner back toward the anchor shrinks the box.
676    /// Display-space end to end: corners are display CELLS, so the box stays
677    /// visually rectangular across tabs and collapsed inline folds; vertical
678    /// steps walk display rows (a collapsed fold is one step, not one per hidden
679    /// row); and cells are virtual (unbounded right), so a box can reach past
680    /// short lines and still select the full width of longer ones. Selection-only;
681    /// any other action exits the mode, leaving the box as a multi-cursor set.
682    pub fn column_select(&mut self, dir: ColumnDir) {
683        // A box gesture is a selection change, so it is an undo-group boundary
684        // and it invalidates the expand ladder — but it must KEEP `self.column`,
685        // so it can't go through `reset_transient`.
686        self.history.seal();
687        self.expand_stack.clear();
688        let folds = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
689        let tab = self.tab_size();
690        let mut col = self.column.unwrap_or_else(|| {
691            let corner = self.caret_corner(&folds, tab);
692            ColumnSelection { anchor: corner, active: corner }
693        });
694        col.active = Self::step_corner(&folds, col.active, dir);
695        self.column = Some(col);
696        self.rebuild_column_box(&folds, tab, col);
697    }
698
699    /// Mouse box (column) selection — Shift+Alt+drag. Sets the box from an
700    /// `anchor` and `active` corner, each `(visible buffer row, display cell)`;
701    /// the widget derives the row from the one row inversion and the cell from
702    /// the one virtual-cell rounding rule ([`crate::row_layout::virtual_cell`]).
703    /// Cells are *virtual* (unclamped by line): the box clamps each row to its
704    /// own content, so a drag past short lines still selects the full width of
705    /// longer ones. Same selection-only semantics as `column_select`; any other
706    /// action exits the mode. Shares `Document.column`, so a keyboard box then
707    /// continues it.
708    pub fn column_drag(&mut self, anchor: (u32, u32), active: (u32, u32)) {
709        // Same boundary + ladder rule as `column_select`.
710        self.history.seal();
711        self.expand_stack.clear();
712        let folds = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
713        let col = ColumnSelection {
714            anchor: CellCorner { row: anchor.0, cell: anchor.1 },
715            active: CellCorner { row: active.0, cell: active.1 },
716        };
717        self.column = Some(col);
718        self.rebuild_column_box(&folds, self.tab_size(), col);
719    }
720
721    /// The primary caret's box corner: its rendered position — the one owner of
722    /// display geometry, [`FoldMap::display_position`] — as a `(visible buffer
723    /// row, display cell)`
724    /// pair. A caret on a collapsed fold's closing tail anchors on the header
725    /// row it renders on. (Fold-time ejection keeps carets visible, so the
726    /// hidden-offset fallback to the raw buffer point is belt and braces.)
727    fn caret_corner(&self, folds: &FoldMap, tab: u32) -> CellCorner {
728        let head = self.selections.newest().head();
729        match folds.display_position(&self.buffer, head, tab) {
730            Some(p) => CellCorner {
731                row: folds.to_buffer_row(p.row).0,
732                cell: crate::row_layout::virtual_cell(p.x.cells()),
733            },
734            None => {
735                let p = self.buffer.offset_to_point(head);
736                let layout = folds.row_layout(&self.buffer, BufferRow(p.row), tab);
737                CellCorner { row: p.row, cell: layout.display_cell(p.col) }
738            }
739        }
740    }
741
742    /// Move the selected line-block up or down one line (Alt+↑/↓). Swaps the
743    /// block with its neighbour; the selection rides the moved text. A block at
744    /// the document edge — or, moving down, against the trailing empty line — is
745    /// a no-op. Single selection. Own undo step; never re-indents.
746    pub fn move_line(&mut self, down: bool) {
747        let sel = *self.selections.newest();
748        let p0 = self.buffer.offset_to_point(sel.start());
749        let p1 = self.buffer.offset_to_point(sel.end());
750        let (r0, r1) = (p0.row, p1.row);
751        let line_count = self.buffer.line_count();
752        let ends_nl = self.buffer.char_before(self.buffer.len()) == Some('\n');
753        // The last row a real line occupies (the trailing empty line, if any, is
754        // not movable — moving into it would drop the terminator).
755        let last_movable = if ends_nl { line_count.saturating_sub(2) } else { line_count - 1 };
756        if (down && r1 >= last_movable) || (!down && r0 == 0) {
757            return;
758        }
759        let (a, b) = if down { (r0, r1 + 1) } else { (r0 - 1, r1) };
760        let has_nl = b + 1 < line_count; // row `b` carries a trailing newline
761        let region_start = self.buffer.point_to_offset(Point::new(a, 0));
762        let region_end = if has_nl {
763            self.buffer.point_to_offset(Point::new(b + 1, 0))
764        } else {
765            self.buffer.len()
766        };
767        let lines: Vec<Cow<str>> = (a..=b).map(|r| self.buffer.line(r)).collect();
768        let reordered: Vec<Cow<str>> = if down {
769            // The line below moves to the front; the block shifts down.
770            let mut v = vec![lines[lines.len() - 1].clone()];
771            v.extend_from_slice(&lines[..lines.len() - 1]);
772            v
773        } else {
774            // The line above moves to the back; the block shifts up.
775            let mut v = lines[1..].to_vec();
776            v.push(lines[0].clone());
777            v
778        };
779        let mut new_text = reordered.join("\n");
780        if has_nl {
781            new_text.push('\n');
782        }
783        let _ = self.edit(vec![EditOp::new(region_start..region_end, &new_text)]);
784        // Place the selection on the moved block (one line over).
785        let shift = |r: u32| if down { r + 1 } else { r - 1 };
786        let ns = self.buffer.point_to_offset(Point::new(shift(r0), p0.col));
787        let ne = self.buffer.point_to_offset(Point::new(shift(r1), p1.col));
788        let mut set = SelectionSet::new(0);
789        set.set_single(Selection::from_anchor(SelectionId(0), ns, ne));
790        self.selections = set;
791        // Drop provenance through the one owner so the `AutoClosePair`
792        // decoration is removed with it — the store is the single owner of the
793        // pair, with nothing else to keep in sync.
794        self.clear_autoclose();
795    }
796
797    /// Duplicate the selected line-block above (`down = false`) or below
798    /// (`down = true`) — Shift+Alt+↑/↓. The caret lands on the copy. Single
799    /// selection. Own undo step.
800    pub fn copy_line(&mut self, down: bool) {
801        let sel = *self.selections.newest();
802        let p0 = self.buffer.offset_to_point(sel.start());
803        let p1 = self.buffer.offset_to_point(sel.end());
804        let (r0, r1) = (p0.row, p1.row);
805        let height = r1 - r0 + 1;
806        let line_count = self.buffer.line_count();
807        let block: String = (r0..=r1).map(|r| self.buffer.line(r)).collect::<Vec<_>>().join("\n");
808        let (at, text) = if !down {
809            (self.buffer.point_to_offset(Point::new(r0, 0)), format!("{block}\n"))
810        } else if r1 + 1 < line_count {
811            (self.buffer.point_to_offset(Point::new(r1 + 1, 0)), format!("{block}\n"))
812        } else {
813            // Duplicating the final line (no trailing newline): prepend one.
814            (self.buffer.len(), format!("\n{block}"))
815        };
816        let _ = self.edit(vec![EditOp::new(at..at, &text)]);
817        let (nr0, nr1) = if down { (r0 + height, r1 + height) } else { (r0, r1) };
818        let ns = self.buffer.point_to_offset(Point::new(nr0, p0.col));
819        let ne = self.buffer.point_to_offset(Point::new(nr1, p1.col));
820        let mut set = SelectionSet::new(0);
821        set.set_single(Selection::from_anchor(SelectionId(0), ns, ne));
822        self.selections = set;
823        // Same as `move_line`: clear provenance through the owner so the store
824        // entry can't be orphaned.
825        self.clear_autoclose();
826    }
827
828    /// Step a box corner one unit in `dir`, in display space: vertical steps
829    /// walk display rows — a collapsed fold is hopped in one step — and clamp
830    /// to the document; the left cell saturates at 0; the right cell is
831    /// unbounded so it can reach past short lines.
832    fn step_corner(folds: &FoldMap, c: CellCorner, dir: ColumnDir) -> CellCorner {
833        let d = folds.to_display_row(BufferRow(c.row)).index();
834        let row_at = |d: u32| folds.to_buffer_row(DisplayRow(d)).0;
835        match dir {
836            ColumnDir::Up => CellCorner { row: row_at(d.saturating_sub(1)), cell: c.cell },
837            ColumnDir::Down => {
838                CellCorner { row: row_at((d + 1).min(folds.max_display_row().index())), cell: c.cell }
839            }
840            ColumnDir::Left => CellCorner { row: c.row, cell: c.cell.saturating_sub(1) },
841            ColumnDir::Right => CellCorner { row: c.row, cell: c.cell + 1 },
842        }
843    }
844
845    /// Install the box `col` as the selection set: one selection per spanned
846    /// *display* row (a collapsed fold's hidden rows get none), each corner cell
847    /// resolved to its byte offset through the one click inverse
848    /// ([`FoldMap::hit_row`]: tab snapping, chip resolution, header gap/tail) —
849    /// so the box selects exactly what its rectangle crosses on screen, clamped
850    /// to each row's content. The active row's selection is the newest
851    /// (autoscroll target).
852    fn rebuild_column_box(&mut self, folds: &FoldMap, tab: u32, col: ColumnSelection) {
853        let da = folds.to_display_row(BufferRow(col.anchor.row)).index();
854        let dv = folds.to_display_row(BufferRow(col.active.row)).index();
855        let (d0, d1) = (da.min(dv), da.max(dv));
856        let mut ranges = Vec::with_capacity((d1 - d0 + 1) as usize);
857        let mut newest = 0;
858        for d in d0..=d1 {
859            let row = folds.to_buffer_row(DisplayRow(d));
860            let anchor_off = folds.hit_row(&self.buffer, row, col.anchor.cell as f32, Bias::Left, tab);
861            let head_off = folds.hit_row(&self.buffer, row, col.active.cell as f32, Bias::Left, tab);
862            if d == dv {
863                newest = ranges.len();
864            }
865            ranges.push((anchor_off, head_off));
866        }
867        self.selections = SelectionSet::from_ranges(&ranges, newest);
868    }
869
870    /// The full document text (LF-only). Shorthand for `self.buffer().text()`
871    /// — a **cold-path** whole-text read: O(document), never call it per-frame
872    /// or per-keystroke.
873    #[must_use]
874    pub fn text(&self) -> Cow<'_, str> {
875        self.buffer.text()
876    }
877
878    /// The current revision.
879    #[must_use]
880    pub fn revision(&self) -> Revision {
881        self.buffer.revision()
882    }
883
884    /// An immutable snapshot for a background consumer (the compile thread).
885    /// O(1) — a rope clone; the consumer pays for what it reads.
886    #[must_use]
887    pub fn snapshot(&self) -> Snapshot {
888        self.buffer.snapshot()
889    }
890
891    /// Apply a batch of edits as one discrete transaction (its own undo step).
892    ///
893    /// This is the common programmatic path. For keystroke-level edits that
894    /// should merge into one undo unit, use [`Document::edit_grouped`].
895    /// Returns the [`Committed`] result (forward patch + change records); an
896    /// empty/no-op batch changes nothing and records no undo step.
897    pub fn edit(&mut self, ops: Vec<EditOp>) -> Result<Committed, TransactionError> {
898        self.edit_grouped(ops, GroupingHint::discrete())
899    }
900
901    /// Apply a batch of edits with an explicit [`GroupingHint`]: the
902    /// verbs layer uses this so a typing run collapses to one undo step.
903    pub fn edit_grouped(
904        &mut self,
905        ops: Vec<EditOp>,
906        hint: GroupingHint,
907    ) -> Result<Committed, TransactionError> {
908        self.column = None; // any text edit exits column-select mode…
909        self.expand_stack.clear(); // …and invalidates the expand ladder
910        // The (revision, fold generation) the fold cache would need to be at to
911        // shift it in place instead of rebuilding — captured before the edit.
912        let pre_fold_key = (self.buffer.revision().0, self.folds.generation());
913        let committed = apply(&mut self.buffer, ops)?;
914        if !committed.is_empty() {
915            // Whether the edit added or removed a bracket character — the
916            // reconcile gate below, read from the applied (forward) + inverse ops
917            // the transaction already owns, so no `ops.clone()` is needed to see
918            // the forward text. History records both (by move) at the end.
919            let bracket_edit = committed
920                .forward_ops()
921                .iter()
922                .chain(committed.inverse_ops())
923                .any(|op| op.text.bytes().any(crate::bracket::is_bracket_byte));
924            // Programmatic edits shift carets through the patch; the verbs layer
925            // overrides this with precise post-edit caret placement.
926            self.selections.rebase(committed.patch());
927
928            let tab = self.tab_size();
929            // Rebase every derived view through the patch — the single mover,
930            // shared verbatim with undo/redo (see `rebase_views`). The auto-close
931            // provenance is an `AutoClosePair` decoration, so it rides here too —
932            // no hand-rebasing; the fold reveal rides inside it too.
933            let region = rebase_views(
934                &mut Views {
935                    highlight: &mut self.highlight,
936                    brackets: &mut self.brackets,
937                    decorations: &mut self.decorations,
938                    autoclose: &mut self.autoclose,
939                    folds: &mut self.folds,
940                    find: &mut self.find,
941                },
942                &self.buffer,
943                tab,
944                &committed,
945            );
946
947            // Now that the provenance decoration has ridden the patch, drop it if
948            // the caret has left the pair — after the rebase, not before.
949            self.validate_autoclose();
950            // Drop folds the edit invalidated (e.g. a deleted closing brace) so a
951            // modification can't leave rows hidden with no chevron — but ONLY when
952            // a bracket CHARACTER was added or removed. If none was, the bracket
953            // sequence is unchanged (same chars, merely shifted), so the matching
954            // automaton produces the identical pairing — no fold can lose its
955            // pair, and reconcile would drop nothing. This skips the whole step
956            // for plain typing AND for Enter (a line change with no bracket char).
957            // When a bracket DID change, `region` (empty ⇒ shift-only, i.e. not
958            // structural) bounds the work to the folds the edit's re-matched rows
959            // could touch, not all folds. Any fold whose interior an edit touched
960            // was already expanded above.
961            if bracket_edit && !region.is_empty() {
962                self.reconcile_folds_in(region);
963            }
964            // Keep the memoized FoldMap current without the per-keystroke O(folds)
965            // rebuild: if the cache was current before this edit AND the edit
966            // added/removed no fold (generation unchanged — expand/reconcile can
967            // bump it), shift it through the patch in place. A line change (or a
968            // stale cache, or a fold add/remove) invalidates for a rebuild on the
969            // next `fold_map()`. The `pre_fold_key` guard makes a mid-edit
970            // `fold_map()` rebuild (which would move the key) fall back safely
971            // rather than double-apply the patch.
972            {
973                let mut cache = self.fold_cache.borrow_mut();
974                if cache.key == pre_fold_key
975                    && self.folds.generation() == pre_fold_key.1
976                    && cache.map.apply_patch(committed.patch(), &self.buffer)
977                {
978                    cache.key = (self.buffer.revision().0, pre_fold_key.1);
979                } else {
980                    cache.key = (u64::MAX, u64::MAX);
981                }
982            }
983            // Record history LAST, moving the forward + inverse op batches out of
984            // `committed` with no clone (rebase_views already used its borrow of
985            // the inverse; the fold-cache borrow above has been dropped, freeing
986            // `&mut self` for `history`). The returned `Committed` keeps only the
987            // patch — the sole part callers (the verbs layer) read.
988            let (patch, forward, inverse) = committed.into_ops();
989            self.history.record(forward, inverse, hint);
990            return Ok(Committed::from_patch(patch));
991        }
992        Ok(committed)
993    }
994
995    /// Undo the most recent undo unit. Returns `false` if there is nothing to
996    /// undo.
997    ///
998    /// Each reverted step threads its patch through `rebase_views` — the exact
999    /// mover forward edits use — so highlight, brackets, and every decoration
1000    /// (diagnostics, find matches, snippet stops) stay consistent with no
1001    /// undo-specific resync per feature. The fields are destructured so the
1002    /// per-step callback can borrow the views while history borrows the buffer.
1003    pub fn undo(&mut self) -> bool {
1004        self.reset_transient();
1005        let tab = self.tab_size();
1006        // Where to land the caret: the START of the reverted region, so undo takes
1007        // you TO the change (as mainstream editors do) instead of leaving the
1008        // caret wherever it happened to be. Steps revert newest→oldest, so the LAST
1009        // callback is the oldest edit — the region's start.
1010        let mut caret_home: Option<u32> = None;
1011        let Self {
1012            history, buffer, selections, highlight, brackets, decorations, autoclose, folds, find, ..
1013        } = self;
1014        let mut views = Views { highlight, brackets, decorations, autoclose, folds, find };
1015        let undone = history.undo(buffer, selections, |committed, buffer| {
1016            // The one mover — highlight, brackets, decorations, folds (position +
1017            // fold reveal) all ride it, so undo needs no per-feature resync.
1018            rebase_views(&mut views, buffer, tab, committed);
1019            if let Some(e) = committed.patch().edits().first() {
1020                caret_home = Some(e.new.start);
1021            }
1022        });
1023        // Reverting text can invalidate a fold's bracket pair too — reconcile once
1024        // the buffer/brackets are settled, same as the forward edit path.
1025        if undone {
1026            self.reconcile_folds();
1027            // Move the caret to the reverted edit (the widget's `moves_caret`
1028            // autoscroll then follows it into view). Overrides the mid-revert
1029            // rebase — the user wants to SEE what undo changed, not stay put.
1030            if let Some(off) = caret_home {
1031                self.selections = SelectionSet::new(off);
1032            }
1033        }
1034        undone
1035    }
1036
1037    /// Redo the most recently undone unit — the [`undo`](Document::undo) mirror,
1038    /// through the same `rebase_views` mover. Returns `false` if nothing to redo.
1039    pub fn redo(&mut self) -> bool {
1040        self.reset_transient();
1041        let tab = self.tab_size();
1042        // Symmetric to `undo`: land the caret at the END of the re-applied region,
1043        // where it would sit had the edit just been made. Steps replay oldest→newest,
1044        // so the LAST callback is the newest edit — the region's end.
1045        let mut caret_home: Option<u32> = None;
1046        let Self {
1047            history, buffer, selections, highlight, brackets, decorations, autoclose, folds, find, ..
1048        } = self;
1049        let mut views = Views { highlight, brackets, decorations, autoclose, folds, find };
1050        let redone = history.redo(buffer, selections, |committed, buffer| {
1051            // The same one mover as `undo` (folds ride it, position + fold reveal).
1052            rebase_views(&mut views, buffer, tab, committed);
1053            if let Some(e) = committed.patch().edits().last() {
1054                caret_home = Some(e.new.end);
1055            }
1056        });
1057        if redone {
1058            self.reconcile_folds();
1059            if let Some(off) = caret_home {
1060                self.selections = SelectionSet::new(off);
1061            }
1062        }
1063        redone
1064    }
1065
1066    /// Whether the document differs from the last [`Document::mark_saved`].
1067    #[must_use]
1068    pub fn is_dirty(&self) -> bool {
1069        self.history.is_dirty()
1070    }
1071
1072    /// Record the current state as saved (clears the dirty flag).
1073    pub fn mark_saved(&mut self) {
1074        self.history.mark_saved();
1075    }
1076
1077    /// Whether there is an edit to [`undo`](Document::undo).
1078    #[must_use]
1079    pub fn can_undo(&self) -> bool {
1080        self.history.can_undo()
1081    }
1082
1083    /// Whether there is an edit to [`redo`](Document::redo) along the current
1084    /// branch.
1085    #[must_use]
1086    pub fn can_redo(&self) -> bool {
1087        self.history.can_redo()
1088    }
1089
1090    /// How many redo branches diverge from the current position: 1 in the
1091    /// classic single-line case, ≥2 at a fork where an undo was followed by a
1092    /// *divergent* edit — the branching undo tree retains the branch you undid
1093    /// out of rather than discarding it, so it stays reachable.
1094    #[must_use]
1095    pub fn redo_branch_count(&self) -> usize {
1096        self.history.redo_branch_count()
1097    }
1098
1099    /// Steer the next [`redo`](Document::redo) to the `index`-th redo branch
1100    /// (0 = oldest), returning `false` if out of range. The chosen branch also
1101    /// becomes the default thereafter. Plain redo (without selecting) always
1102    /// follows the most-recent branch, so single-line undo is unchanged.
1103    pub fn select_redo_branch(&mut self, index: usize) -> bool {
1104        self.history.select_redo_branch(index)
1105    }
1106
1107    /// Cap undo history at `limit` units on the current line (`None` = the
1108    /// default, unbounded), dropping older units and any branch that diverged
1109    /// before the kept window. Opt-in: a host that wants bounded undo memory
1110    /// calls this; by default nothing is ever pruned.
1111    pub fn set_undo_limit(&mut self, limit: Option<usize>) {
1112        self.history.set_max_undo(limit);
1113    }
1114
1115    /// How many undo steps are currently reachable — the depth of `undo`
1116    /// (bounded by [`set_undo_limit`](Document::set_undo_limit) when set).
1117    #[must_use]
1118    pub fn undo_depth(&self) -> usize {
1119        self.history.undo_depth()
1120    }
1121
1122    /// Serialize for saving, re-expanding LF to the given flavor.
1123    #[must_use]
1124    pub fn serialize(&self, flavor: EolFlavor) -> String {
1125        self.buffer.serialize(flavor)
1126    }
1127
1128    /// Attach a syntax highlighter. The grammar and theme are app-supplied
1129    /// (scrive-core is language-agnostic). The cache starts sized to the current
1130    /// line count, every line dirty; call [`Document::tokenize_highlight`] before
1131    /// reading spans, and it re-splices on every edit. A mid-session grammar
1132    /// swap carries the previous retention-window aim forward, so the currently
1133    /// visible rows are re-highlighted immediately even though nothing visible
1134    /// moved to re-trigger the widget's viewport report.
1135    pub fn set_syntax(&mut self, syntax: SyntaxDef, theme: TokenTheme) {
1136        let aim = self.highlight.as_ref().map(HighlightCache::window_aim);
1137        let mut cache = HighlightCache::new(syntax, theme, self.buffer.line_count());
1138        if let Some(aim) = aim {
1139            cache.set_window(aim);
1140        }
1141        self.highlight = Some(cache);
1142    }
1143
1144    /// Swap the highlight theme, keeping the grammar and cache sizing. The
1145    /// whole cache invalidates; colors repaint on the next `tokenize_highlight`
1146    /// (old colors show meanwhile — see [`HighlightCache::set_theme`]). No-op
1147    /// without a highlighter.
1148    pub fn set_theme(&mut self, theme: TokenTheme) {
1149        if let Some(cache) = self.highlight.as_mut() {
1150            cache.set_theme(theme);
1151        }
1152    }
1153
1154    /// The tracked-range decoration store. Exposed so an app-level owner —
1155    /// e.g. a snippet session registering one range per tab stop — can
1156    /// drive it through the store's handle API. The store rides edits
1157    /// automatically: every transaction patches it before change events fire.
1158    #[must_use]
1159    pub fn decorations(&self) -> &DecorationStore {
1160        &self.decorations
1161    }
1162
1163    /// Mutable [`decorations`](Document::decorations) for a decoration owner.
1164    pub fn decorations_mut(&mut self) -> &mut DecorationStore {
1165        &mut self.decorations
1166    }
1167
1168    /// How many live auto-close provenance pairs the own store holds — the
1169    /// store-as-truth count, for tests that pin the provenance lifetime.
1170    #[cfg(test)]
1171    #[must_use]
1172    pub(crate) fn autoclose_pair_count(&self) -> usize {
1173        self.autoclose.len()
1174    }
1175
1176    /// The active code folds — read access for building a per-frame
1177    /// [`FoldMap`].
1178    #[must_use]
1179    pub fn folds(&self) -> &FoldSet {
1180        &self.folds
1181    }
1182
1183    /// Mutable access to the fold set (e.g. `clear`). Folds are view state:
1184    /// mutating them records nothing on the undo stack. Row-based fold/unfold go
1185    /// through the convenience methods below (they need the buffer too).
1186    pub fn folds_mut(&mut self) -> &mut FoldSet {
1187        &mut self.folds
1188    }
1189
1190    /// Fold the multi-line bracket pair opening on `header` and closing on
1191    /// `last`. A fold is keyed by that pair's opening-bracket offset. Nesting is
1192    /// allowed (folds may sit inside or around others). Returns `false` if no such
1193    /// foldable pair exists or it is already folded. Records nothing on the undo
1194    /// stack — folds are view state.
1195    pub fn fold(&mut self, header: BufferRow, last: BufferRow) -> bool {
1196        match self.opener_of(header.0, last.0) {
1197            Some(open) => self.folds.fold(open),
1198            None => false,
1199        }
1200    }
1201
1202    /// Unfold the fold whose header (opener row) is `header`. Returns whether one
1203    /// was removed.
1204    pub fn unfold(&mut self, header: BufferRow) -> bool {
1205        match self.folded_opener_on(header.0) {
1206            Some(open) => self.folds.unfold(open),
1207            None => false,
1208        }
1209    }
1210
1211    /// Fold if `header` has no active fold, else unfold it (the gutter-chevron /
1212    /// `Ctrl+Shift+[`,`]` action).
1213    pub fn toggle_fold(&mut self, header: BufferRow, last: BufferRow) -> bool {
1214        match self.folded_opener_on(header.0) {
1215            Some(open) => self.folds.unfold(open),
1216            None => self.fold(header, last),
1217        }
1218    }
1219
1220    /// The opening-bracket offset of the collapsed fold whose opener sits on
1221    /// buffer row `header`, if any. Openers are offset-sorted, so this is the
1222    /// first fold at/after the row start that still lies on the row — an O(log)
1223    /// probe, so a gutter unfold click never scans every fold.
1224    fn folded_opener_on(&self, header: u32) -> Option<u32> {
1225        let start = self.buffer.point_to_offset(Point::new(header, 0));
1226        let end = if header + 1 >= self.buffer.line_count() {
1227            self.buffer.len() + 1 // sentinel: an opener at the final byte is on the row
1228        } else {
1229            self.buffer.point_to_offset(Point::new(header + 1, 0))
1230        };
1231        self.folds.first_at_or_after(start).filter(|&o| o < end)
1232    }
1233
1234    /// The opening-bracket offset of the foldable pair spanning rows
1235    /// `(header, last)`, if one exists.
1236    fn opener_of(&self, header: u32, last: u32) -> Option<u32> {
1237        // A pair headed on `header` has its opener on that row — window there.
1238        self.foldable_pairs_in_rows(header..header + 1)
1239            .into_iter()
1240            .find(|&(_, _, h, l)| h == header && l == last)
1241            .map(|(o, ..)| o)
1242    }
1243
1244    /// Every foldable bracket pair as `(open_offset, close_offset, header_row,
1245    /// last_row)`, in document order — the bracket-anchored source for folds. A
1246    /// pair is foldable when it has a *non-empty* interior: multi-line (a block)
1247    /// or single-line with something between the brackets (an inline fold).
1248    fn foldable_pairs(&self) -> Vec<(u32, u32, u32, u32)> {
1249        self.foldable_pairs_from(&self.brackets.all())
1250    }
1251
1252    /// `Document::foldable_pairs` restricted to pairs whose **opening bracket**
1253    /// lies on a buffer row in `rows` — the windowed twin for the fold mouse
1254    /// hot paths (gutter hover/click, Ctrl+hover boxes), so they cost
1255    /// O(brackets in the window) instead of a whole-document scan per
1256    /// mouse-move on a large document. Since a pair's `header` is
1257    /// its opener's row, this returns exactly the pairs headed in `rows` (their
1258    /// closer may lie far below — that offset is carried in the bracket, no
1259    /// second scan). A pair *enclosing* `rows` from above is intentionally not
1260    /// returned (its opener is elsewhere); callers that only test `header ==
1261    /// row` (block_opener_on_row, the gutter chevron) are exact, and the
1262    /// Ctrl+hover affordance widens `rows` with a slack for near-enclosing
1263    /// blocks.
1264    ///
1265    /// Public because the widget's plain-hover chip hit-test
1266    /// (`collapsed_chip_at`) needs exactly this: every foldable pair — inline
1267    /// `[ … ]` and block `{ … }` alike — headed on the pointer's single row, so
1268    /// it can test that row's chips instead of scanning every fold in the
1269    /// document (an O(folds) `HeaderLayout` build per frame at scale).
1270    pub fn foldable_pairs_in_rows(&self, rows: Range<u32>) -> Vec<(u32, u32, u32, u32)> {
1271        let start = self.buffer.point_to_offset(Point::new(rows.start, 0));
1272        let end = if rows.end >= self.buffer.line_count() {
1273            self.buffer.len() + 1 // sentinel: a bracket at the final byte is inside
1274        } else {
1275            self.buffer.point_to_offset(Point::new(rows.end, 0))
1276        };
1277        self.foldable_pairs_from(&self.brackets.in_range(start..end))
1278    }
1279
1280    /// The shared body of `Document::foldable_pairs` and its windowed twin.
1281    fn foldable_pairs_from(&self, brackets: &[crate::bracket::Bracket]) -> Vec<(u32, u32, u32, u32)> {
1282        brackets
1283            .iter()
1284            .filter_map(|b| {
1285                let close = b.partner.filter(|_| b.open)?;
1286                // The shared foldability rule: a non-empty interior.
1287                crate::row_layout::pair_has_interior(b.offset, close).then(|| {
1288                    let header = self.buffer.offset_to_point(b.offset).row;
1289                    let last = self.buffer.offset_to_point(close).row;
1290                    (b.offset, close, header, last)
1291                })
1292            })
1293            .collect()
1294    }
1295
1296    /// Toggle the fold whose opening bracket is at byte offset `opener` (a
1297    /// gutter-chevron click or `Ctrl+Shift+[`/`]`). Returns whether it changed.
1298    /// When it *folds*, any caret the fold would hide is ejected to the gap's
1299    /// entry edge so it never sits on collapsed text: an inline pair
1300    /// pulls it to `opener+1` (just after `[`), a block to the header line's
1301    /// end (just before the `…` placeholder).
1302    pub fn toggle_fold_opener(&mut self, opener: u32) -> bool {
1303        let changed = self.folds.toggle(opener);
1304        if changed {
1305            // A fold toggle is a gesture that can MOVE carets (the ejection
1306            // below), so it takes the same boundary as every selection verb:
1307            // seal the undo group, drop the box anchor / expand ladder /
1308            // auto-close provenance — otherwise a stale expand ladder could
1309            // restore a caret into the collapsed fold.
1310            self.reset_transient();
1311        }
1312        if changed && self.folds.is_folded(opener) {
1313            if let Some(close) = self.brackets.foldable_partner(opener) {
1314                let single_line = self.buffer.offset_to_point(opener).row == self.buffer.offset_to_point(close).row;
1315                if single_line {
1316                    // The shared gap rule: a caret strictly inside the
1317                    // now-hidden interior pulls out to the left landable edge.
1318                    self.selections.map_each(|s| {
1319                        if crate::row_layout::gap_hides_caret(opener, close, s.head()) {
1320                            s.move_to_caret(crate::row_layout::gap_left_edge(opener));
1321                        }
1322                    });
1323                } else {
1324                    // Block: eject any caret the fold just hid to the gap's
1325                    // entry edge — the header line's end, just before the `…`
1326                    // placeholder (the block analog of the inline pull-out
1327                    // above), so typing can never edit invisible text. "Hidden"
1328                    // comes from the one owner: `display_position` is `None`
1329                    // exactly for offsets in a fold's gap.
1330                    // The one tab-width owner (hoisted above the destructure that
1331                    // borrows the fields — `self.tab_size()` can't be called after).
1332                    let tab = self.tab_size();
1333                    let Self { selections, buffer, folds, brackets, .. } = self;
1334                    let fold_map = crate::fold_map::FoldMap::new(folds, brackets, buffer);
1335                    let header = buffer.offset_to_point(opener).row;
1336                    let entry = buffer.point_to_offset(Point::new(header, buffer.line_len(header)));
1337                    selections.map_each(|s| {
1338                        let inside = s.head() > opener && s.head() < close;
1339                        if inside && fold_map.display_position(buffer, s.head(), tab).is_none() {
1340                            s.move_to_caret(entry);
1341                        }
1342                    });
1343                }
1344            }
1345        }
1346        changed
1347    }
1348
1349    /// The document's tab-stop width — the ONE owner every consumer (core
1350    /// projections and the widget's pixel layer alike) consults. Fixed at the
1351    /// [`crate::display_map::default_tab_size`] for now; when it becomes
1352    /// configurable, the change is one whole-document edit and no
1353    /// caller-side constant exists to drift.
1354    #[must_use]
1355    pub fn tab_size(&self) -> u32 {
1356        crate::display_map::default_tab_size()
1357    }
1358
1359    /// The opener offset of the widest *multi-line* pair whose header is buffer
1360    /// row `row` — the block a gutter chevron on that row folds.
1361    #[must_use]
1362    pub fn block_opener_on_row(&self, row: u32) -> Option<u32> {
1363        // Windowed to the row's brackets (a pair headed on `row` has its opener
1364        // on `row`) — O(brackets on the row), not a whole-document scan per
1365        // gutter click.
1366        self.foldable_pairs_in_rows(row..row + 1)
1367            .into_iter()
1368            .filter(|&(_, _, h, l)| h == row && l > h)
1369            .max_by_key(|&(_, _, h, l)| l - h)
1370            .map(|(o, ..)| o)
1371    }
1372
1373    /// The opener of the innermost foldable pair enclosing **or touching** the
1374    /// primary caret whose collapsed state matches `want_folded` — the
1375    /// `Ctrl+Shift+[` (fold, `false`) / `Ctrl+Shift+]` (unfold, `true`) target.
1376    /// A caret directly before the opening bracket or after the closing one
1377    /// counts (the bracket-highlight adjacency). Includes single-line inline
1378    /// pairs, disambiguated by the caret's byte offset (rows can't tell two
1379    /// `[..]` apart).
1380    #[must_use]
1381    pub fn fold_opener_at_caret(&self, want_folded: bool) -> Option<u32> {
1382        self.fold_opener_at(self.selections.newest().head(), want_folded)
1383    }
1384
1385    /// The innermost foldable pair enclosing or touching `caret` whose collapsed
1386    /// state matches `want_folded` — the per-caret core of the fold chords.
1387    /// Costs O(distance to the innermost target), NOT O(leftward siblings): a
1388    /// touching pair (the innermost possible) is found by an O(log) lookup, and
1389    /// otherwise the enclosing walk early-stops at the first match — so
1390    /// [`Self::fold_at_carets`] over N carets stays O(N · local), not O(N²).
1391    fn fold_opener_at(&self, caret: u32, want_folded: bool) -> Option<u32> {
1392        let is_target = |open: u32, close: u32| {
1393            crate::row_layout::pair_has_interior(open, close)
1394                && self.folds.is_folded(open) == want_folded
1395        };
1396        // Touching pairs are the innermost possible (an encloser is strictly
1397        // larger, since it contains the touch): the caret ON a foldable opener,
1398        // or just past a foldable closer. Pick the smaller-extent match.
1399        let touch_left = self.brackets.foldable_partner(caret).map(|close| (caret, close));
1400        let touch_right = caret
1401            .checked_sub(1)
1402            .and_then(|o| self.brackets.at(o))
1403            .filter(|b| !b.open)
1404            .and_then(|b| b.partner.map(|open| (open, b.offset)));
1405        if let Some((open, _)) = [touch_left, touch_right]
1406            .into_iter()
1407            .flatten()
1408            .filter(|&(o, c)| is_target(o, c))
1409            .min_by_key(|&(o, c)| c - o)
1410        {
1411            return Some(open);
1412        }
1413        // Otherwise the innermost ENCLOSING foldable pair, early-stopping.
1414        self.brackets.innermost_enclosing_where(caret, is_target)
1415    }
1416
1417    /// Fold (`unfold == false`) or unfold (`unfold == true`) the innermost
1418    /// foldable block enclosing or touching EACH caret — the multi-cursor
1419    /// `Ctrl+Shift+[` / `Ctrl+Shift+]`. Distinct target blocks act once (two
1420    /// carets in one block don't cancel each other), and every opener is resolved
1421    /// BEFORE any toggle — safe because folding moves no byte offset. Returns
1422    /// whether anything changed.
1423    pub fn fold_at_carets(&mut self, unfold: bool) -> bool {
1424        let mut openers: Vec<u32> = self
1425            .selections
1426            .all()
1427            .iter()
1428            .filter_map(|s| self.fold_opener_at(s.head(), unfold))
1429            .collect();
1430        openers.sort_unstable();
1431        openers.dedup();
1432        if openers.is_empty() {
1433            return false;
1434        }
1435        // A fold gesture is a boundary like every selection verb (seal undo, drop
1436        // the box anchor / expand ladder / auto-close provenance) — ONCE. Then
1437        // toggle ALL folds in one batch and eject carets in one pass. Per-fold
1438        // `toggle_fold_opener` is O(folds) each (a sorted-Vec insert plus a
1439        // FoldMap rebuild), so folding one at a time would make a document-scale
1440        // "collapse all" O(folds²); the batch keeps it linear.
1441        self.reset_transient();
1442        if unfold {
1443            self.folds.unfold_all(&openers);
1444        } else {
1445            self.folds.fold_all(openers.iter().copied());
1446        }
1447        self.eject_hidden_carets();
1448        true
1449    }
1450
1451    /// Pull every caret out of a newly-hidden fold gap to the fold's entry edge
1452    /// — the batched form of [`Self::toggle_fold_opener`]'s ejection: ONE
1453    /// FoldMap build, each caret probed in O(log folds).
1454    fn eject_hidden_carets(&mut self) {
1455        let Self { selections, buffer, folds, brackets, .. } = self;
1456        if folds.is_empty() {
1457            return;
1458        }
1459        let fold_map = crate::fold_map::FoldMap::new(folds, brackets, buffer);
1460        selections.map_each(|s| {
1461            if let Some(entry) = fold_map.entry_edge_if_hidden(buffer, s.head()) {
1462                s.move_to_caret(entry);
1463            }
1464        });
1465    }
1466
1467    /// Every *collapsible* bracket pair — foldable (non-empty interior) and not
1468    /// already collapsed — as `(open, close, header, last)`, document order. The
1469    /// candidate set for the Ctrl+hover affordance: a pair already folded is
1470    /// excluded (there is nothing left to collapse). Includes both multi-line
1471    /// blocks and single-line inline pairs, so one gesture reaches either.
1472    #[must_use]
1473    pub fn collapsible_pairs(&self) -> Vec<(u32, u32, u32, u32)> {
1474        self.foldable_pairs().into_iter().filter(|&(open, ..)| !self.folds.is_folded(open)).collect()
1475    }
1476
1477    /// [`Document::collapsible_pairs`] restricted to pairs headed on buffer rows
1478    /// in `rows` — the windowed query the Ctrl+hover affordance uses per
1479    /// mouse-move instead of a whole-document scan on a large document.
1480    /// The caller widens `rows` with a slack so a pair whose header sits just
1481    /// above the viewport still arms; a block taller than that slack enclosing
1482    /// the pointer is the accepted miss (fold it from its header chevron or the
1483    /// `Ctrl+Shift+[` chord instead).
1484    #[must_use]
1485    pub fn collapsible_pairs_in_rows(&self, rows: Range<u32>) -> Vec<(u32, u32, u32, u32)> {
1486        self.foldable_pairs_in_rows(rows)
1487            .into_iter()
1488            .filter(|&(open, ..)| !self.folds.is_folded(open))
1489            .collect()
1490    }
1491
1492    /// The opener of the innermost collapsible pair whose interior contains byte
1493    /// `offset` (`open < offset < close`) — the Ctrl+hover / Ctrl+Click target.
1494    /// Innermost = smallest byte span, so a pointer inside nested
1495    /// collapsibles resolves to the tightest one (an inline array over its
1496    /// enclosing block). `None` when the offset sits in no collapsible interior.
1497    #[must_use]
1498    pub fn collapsible_at(&self, offset: u32) -> Option<u32> {
1499        self.collapsible_pairs()
1500            .into_iter()
1501            .filter(|&(open, close, _, _)| open < offset && offset < close)
1502            .min_by_key(|&(open, close, _, _)| close - open)
1503            .map(|(o, ..)| o)
1504    }
1505
1506    /// Drop collapsed folds whose opener is no longer a live foldable open bracket
1507    /// — e.g. an edit deleted or unmatched a block's brackets. Called from the
1508    /// edit / undo / redo paths once brackets are re-matched. Because a fold's
1509    /// extent is re-derived from the live brackets (never stored), there is nothing
1510    /// to heal — a fold either still opens a foldable pair or it is dropped. No-op
1511    /// when nothing is folded.
1512    fn reconcile_folds(&mut self) {
1513        if self.folds.is_empty() {
1514            return;
1515        }
1516        // Check each folded opener DIRECTLY (a matched open bracket with a
1517        // non-empty interior) via an O(log) bracket lookup, instead of
1518        // building the whole-document `foldable_pairs()`. Reconcile runs on
1519        // every commit, so this keeps a keystroke while a fold is active off an
1520        // O(brackets) whole-document scan.
1521        let brackets = &self.brackets;
1522        self.folds.reconcile(|o| {
1523            brackets
1524                .foldable_partner(o)
1525                .is_some_and(|close| crate::row_layout::pair_has_interior(o, close))
1526        });
1527    }
1528
1529    /// Test hook: run the whole-set reconcile (the undo/redo path's version) so a
1530    /// test can assert the edit-path windowed reconcile left nothing for it to do.
1531    #[cfg(test)]
1532    pub(crate) fn force_whole_reconcile(&mut self) {
1533        self.reconcile_folds();
1534    }
1535
1536    /// Windowed reconcile for the edit path: drop only folds the edit's re-matched
1537    /// `region` could have broken. A fold's foldability changes only if its opener
1538    /// or closer bracket was re-scanned, and every re-scanned bracket lies in the
1539    /// bracket engine's replayed rows — so an affected fold's `[opener, closer]`
1540    /// must INTERSECT that span. `region` already extends its left edge down to the
1541    /// outermost enclosing opener (the seed stack — see `BracketOps::reconcile_lo`),
1542    /// so every such fold has its OPENER in `region`: a single binary-searched
1543    /// window, no separate enclosing walk. This is [`reconcile_folds`]'s O(local)
1544    /// twin — the whole-set version stays on the undo/redo path, which spans many
1545    /// steps and so has no single region.
1546    fn reconcile_folds_in(&mut self, region: core::ops::Range<u32>) {
1547        if self.folds.is_empty() {
1548            return;
1549        }
1550        let candidates: Vec<u32> = self.folds.openers_in(region);
1551        let brackets = &self.brackets;
1552        self.folds.reconcile_only(&candidates, |o| {
1553            brackets
1554                .foldable_partner(o)
1555                .is_some_and(|close| crate::row_layout::pair_has_interior(o, close))
1556        });
1557    }
1558
1559    /// Every foldable `(header, last)` row range — the source for gutter chevrons
1560    /// and fold commands. Derived from the matched-bracket pass: each *multi-line*
1561    /// matched pair (`{}`/`()`/`[]`) becomes a range from its opener row to its
1562    /// closer row. At most one range per header row (the widest), sorted by header.
1563    #[must_use]
1564    pub fn foldable_ranges(&self) -> Vec<(BufferRow, BufferRow)> {
1565        let mut ranges: Vec<(BufferRow, BufferRow)> = self
1566            .brackets
1567            .all()
1568            .iter()
1569            .filter_map(|br| {
1570                let close = br.partner.filter(|_| br.open)?;
1571                let header = self.buffer.offset_to_point(br.offset).row;
1572                let last = self.buffer.offset_to_point(close).row;
1573                (last > header).then_some((BufferRow(header), BufferRow(last)))
1574            })
1575            .collect();
1576        // Keep the widest range per header row (largest `last`).
1577        ranges.sort_by_key(|r| (r.0, core::cmp::Reverse(r.1)));
1578        ranges.dedup_by_key(|r| r.0);
1579        ranges
1580    }
1581
1582    /// [`Document::foldable_ranges`] restricted to headers on buffer rows in
1583    /// `rows` — the gutter chevrons' per-frame query: two partition points over
1584    /// the sorted bracket Vec ([`Brackets::in_range`]) instead of a
1585    /// whole-document scan per frame.
1586    /// Same widest-per-header dedup; hidden in-window headers are the
1587    /// caller's to cull (its `visible_y` already does).
1588    #[must_use]
1589    pub fn foldable_ranges_in_rows(&self, rows: Range<u32>) -> Vec<(BufferRow, BufferRow)> {
1590        let start = self.buffer.point_to_offset(Point::new(rows.start, 0));
1591        let end = if rows.end >= self.buffer.line_count() {
1592            self.buffer.len() + 1 // sentinel: a bracket at the final byte is inside
1593        } else {
1594            self.buffer.point_to_offset(Point::new(rows.end, 0))
1595        };
1596        let mut ranges: Vec<(BufferRow, BufferRow)> = self
1597            .brackets
1598            .in_range_iter(start..end)
1599            .filter_map(|br| {
1600                let close = br.partner.filter(|_| br.open)?;
1601                let header = self.buffer.offset_to_point(br.offset).row;
1602                let last = self.buffer.offset_to_point(close).row;
1603                (last > header).then_some((BufferRow(header), BufferRow(last)))
1604            })
1605            .collect();
1606        ranges.sort_by_key(|r| (r.0, core::cmp::Reverse(r.1)));
1607        ranges.dedup_by_key(|r| r.0);
1608        ranges
1609    }
1610
1611    /// Bring the highlight cache current up to buffer line `target` (a viewport's
1612    /// last line). Lazy and incremental — end-state convergence bounds the work
1613    /// to the edited lines. No-op without a highlighter.
1614    pub fn tokenize_highlight(&mut self, target: u32) {
1615        // `self.highlight` and `self.buffer` are disjoint fields, so both
1616        // borrow. The buffer is handed in as a line ACCESSOR — never an
1617        // all-lines Vec — and each call is budget-capped; the app's idle sweep
1618        // resumes at [`Document::highlight_frontier`] until convergence.
1619        let buffer = &self.buffer;
1620        let Some(cache) = self.highlight.as_mut() else { return };
1621        cache.tokenize_until(target, crate::highlight::HIGHLIGHT_MAX_LINES_PER_CALL, |r| {
1622            buffer.line(r)
1623        });
1624    }
1625
1626    /// The next row highlight work would touch — a dirty row, or a window
1627    /// row awaiting a refill after [`Document::set_highlight_window`] moved
1628    /// the retention window (highlight virtualization). `None` when there is
1629    /// nothing to do — or when no grammar is injected. The app's idle sweep
1630    /// polls this to drive budgeted [`Document::tokenize_highlight`] calls
1631    /// and to stop when idle (an idle document does zero highlight work).
1632    #[must_use]
1633    pub fn highlight_frontier(&self) -> Option<u32> {
1634        self.highlight.as_ref().and_then(HighlightCache::pending)
1635    }
1636
1637    /// Aim the highlight retention window at `rows` (the visible buffer rows;
1638    /// the cache pads by [`crate::highlight::HIGHLIGHT_WINDOW_SLACK`] and
1639    /// evicts outside it). Spans and per-line states are retained only there;
1640    /// elsewhere sparse checkpoints keep every row re-derivable, so a fully
1641    /// swept document holds memory proportional to the window, not the line
1642    /// count. Call on viewport change, then drive
1643    /// [`Document::tokenize_highlight`] as usual. No-op without a grammar.
1644    pub fn set_highlight_window(&mut self, rows: Range<u32>) {
1645        if let Some(cache) = self.highlight.as_mut() {
1646            cache.set_window(rows);
1647        }
1648    }
1649
1650    /// Cached highlight spans for a buffer row — `None` if untokenized or no
1651    /// highlighter is attached (the renderer falls back to the plain text color).
1652    #[must_use]
1653    pub fn highlight_line_spans(&self, row: u32) -> Option<&[HighlightSpan]> {
1654        self.highlight.as_ref().and_then(|c| c.line_spans(row))
1655    }
1656
1657    /// A `Send + Sync` handle to the highlighter's grammar + theme, for the
1658    /// app's off-thread parallel/speculative sweep. `None` without a grammar.
1659    /// Pair with [`Document::snapshot`] (an O(1) rope clone) and
1660    /// [`crate::tokenize_segment`] on a worker, then feed results back through
1661    /// [`Document::absorb_highlight`].
1662    #[must_use]
1663    pub fn highlight_engine(&self) -> Option<HighlightEngine> {
1664        self.highlight.as_ref().map(HighlightCache::engine)
1665    }
1666
1667    /// Ingest a segment tokenized off-thread (the parallel/speculative sweep).
1668    /// Returns `false` — absorbing **nothing** — if `revision` no longer
1669    /// matches the document (an edit landed since the snapshot the segment was
1670    /// computed from; the app drops the stale result and re-dispatches). On a
1671    /// match, forwards to `HighlightCache::absorb`:
1672    ///
1673    /// - `verified` — the coordinator chained this segment from row 0, so its
1674    ///   start state is TRUE: its checkpoints merge, its window spans/states are
1675    ///   written, and its rows leave the dirty frontier (replacing the
1676    ///   foreground walk).
1677    /// - `!verified` — viewport speculation from a guessed fresh start: only
1678    ///   still-dirty window rows get its spans, no checkpoints are planted, and
1679    ///   the frontier still re-verifies those rows per-line (converging in O(1)
1680    ///   if the guess was right).
1681    ///
1682    /// Already-absorbed work is never lost to a later edit: it lives in the
1683    /// cache and rides `rebase_views`' `on_commit` splices like every other
1684    /// derived fact — only *in-flight* results are dropped by the revision
1685    /// check.
1686    pub fn absorb_highlight(
1687        &mut self,
1688        revision: Revision,
1689        seg: crate::SegmentTokens,
1690        verified: bool,
1691    ) -> bool {
1692        if revision != self.buffer.revision() {
1693            return false;
1694        }
1695        let Some(cache) = self.highlight.as_mut() else { return false };
1696        cache.absorb(seg, verified);
1697        true
1698    }
1699
1700    /// The matched brackets — kept current on every edit. Drives
1701    /// bracket-pair colorization and the matching-bracket highlight.
1702    #[must_use]
1703    pub fn brackets(&self) -> &Brackets {
1704        &self.brackets
1705    }
1706
1707    /// Publish a diagnostic set from the app's debounced compile loop.
1708    ///
1709    /// The compiler ran against the snapshot at `revision`; if the buffer has
1710    /// since moved on this returns [`DiagnosticsOutcome::Stale`] having installed
1711    /// nothing — the previous squiggles keep riding edits via stickiness until
1712    /// the next publish (no stale set is placed). On a current revision the whole
1713    /// diagnostic set is replaced wholesale; spans are first clipped to the buffer
1714    /// length. Zero-width spans survive — the squiggle enforces its one-cell
1715    /// minimum at draw time.
1716    pub fn set_diagnostics(&mut self, revision: Revision, diags: Vec<Diagnostic>) -> DiagnosticsOutcome {
1717        let current = self.buffer.revision();
1718        if revision != current {
1719            return DiagnosticsOutcome::Stale { current: current.0 };
1720        }
1721        let len = self.buffer.len();
1722        let clipped: Vec<Diagnostic> = diags
1723            .into_iter()
1724            .map(|mut d| {
1725                let (s, e) = (d.span.start.min(len), d.span.end.min(len));
1726                d.span = s.min(e)..e; // clip to buffer, never inverted
1727                d
1728            })
1729            .collect();
1730        self.decorations.set_diagnostics(current.0, current.0, clipped)
1731    }
1732
1733    /// Diagnostics overlapping the byte `range`, for squiggle rendering:
1734    /// `(span, severity, message)`, position and content reunited from the store,
1735    /// in ascending `(start, id)` order. The renderer clips to its visible rows.
1736    pub fn diagnostics_in(
1737        &self,
1738        range: Range<u32>,
1739    ) -> impl Iterator<Item = (Range<u32>, Severity, std::sync::Arc<str>)> + '_ {
1740        // The store yields owned tracked ranges (its delta-gap encoding has no
1741        // absolute range to borrow), so the message rides out as its shared
1742        // `Arc<str>` — an owner bump, not a copy.
1743        self.decorations.decorations_in(range).filter_map(|r| {
1744            let TrackedRange { range, kind, .. } = r;
1745            match kind {
1746                DecorationKind::Diagnostic { severity, message, .. } => {
1747                    Some((range, severity, message))
1748                }
1749                _ => None,
1750            }
1751        })
1752    }
1753
1754    /// Set (or clear with `None`) the find query, scanning synchronously.
1755    /// Never scrolls and drops the active match; the app calls [`find_next`] after
1756    /// if it wants reveal-as-you-type. `now_ms` is the injected clock. This is the
1757    /// one whole-document find op — every subsequent edit repairs the set
1758    /// incrementally through the commit mover.
1759    ///
1760    /// [`find_next`]: Document::find_next
1761    pub fn set_find_query(&mut self, query: Option<FindQuery>, now_ms: u64) {
1762        self.find.set_query(query, &self.buffer, &mut self.decorations, now_ms);
1763    }
1764
1765    /// The active find query, or `None` if find is idle.
1766    #[must_use]
1767    pub fn find_query(&self) -> Option<&FindQuery> {
1768        self.find.query()
1769    }
1770
1771    /// Whole-word occurrences of the word under the newest caret WITHIN the
1772    /// byte range `within` — the passive occurrence wash (the reading aid for
1773    /// tracing a PID/label through a script). The window keeps this per-frame
1774    /// query O(viewport): the widget passes its visible byte range; tests pass
1775    /// `0..u32::MAX` (the scan clamps). Every
1776    /// match must be a WHOLE word, judged by the same
1777    /// `movement::surrounding_word` rule that seeds Ctrl+D — checked against
1778    /// the full buffer, so a window edge can never fake a word boundary. Empty
1779    /// when the newest selection is non-empty or the caret is off-word. A pure
1780    /// per-frame query (the `FoldMap` precedent): no tracked state, nothing to
1781    /// rebase on the undo path.
1782    #[must_use]
1783    pub fn caret_word_occurrences(&self, within: Range<u32>) -> Vec<Range<u32>> {
1784        let newest = self.selections.newest();
1785        if !newest.is_empty() {
1786            return Vec::new();
1787        }
1788        let Some((ws, we)) = movement::surrounding_word(&self.buffer, newest.head()) else {
1789            return Vec::new();
1790        };
1791        if ws == we {
1792            return Vec::new();
1793        }
1794        let word = self.buffer.slice(ws..we);
1795        let end = within.end.min(self.buffer.len());
1796        let base = within.start.min(end);
1797        let hay = self.buffer.slice(base..end);
1798        let mut out = Vec::new();
1799        let mut from = 0usize;
1800        while let Some(i) = hay[from..].find(&*word) {
1801            let start = base + (from + i) as u32;
1802            let match_end = start + word.len() as u32;
1803            // Whole-word: the match must BE its own surrounding word.
1804            if movement::surrounding_word(&self.buffer, start) == Some((start, match_end)) {
1805                out.push(start..match_end);
1806            }
1807            from = from + i + word.len();
1808        }
1809        out
1810    }
1811
1812    /// How many matches the current query has — the *live* count. The store IS
1813    /// the match set: there is no shadow handle list to disagree with it, so this
1814    /// is the O(1) root-summary `find_count`, not an O(matches) walk. Empty
1815    /// matches never persist (`FindMatch` is `EmptyPolicy::Drop`), so the count
1816    /// equals the rendered `start < end` set.
1817    #[must_use]
1818    pub fn find_match_count(&self) -> usize {
1819        self.decorations.find_count()
1820    }
1821
1822    /// The active match's position among the live matches, in document order, if
1823    /// one is active and still present — consistent with
1824    /// [`find_match_count`](Document::find_match_count) for an "N of M" display.
1825    /// O(log M) from the active handle's tracked start: its rank is the number of
1826    /// finds starting before it, not an O(M) `position` scan.
1827    #[must_use]
1828    pub fn active_find_match(&self) -> Option<u32> {
1829        self.find.active_start().map(|s| self.decorations.find_rank_before(s) as u32)
1830    }
1831
1832    /// Find matches overlapping the byte `range`, for highlight rendering:
1833    /// `(span, is_active)`, in ascending order. Collapsed (empty) matches are
1834    /// skipped — an edit may zero one before the next re-scan purges it.
1835    pub fn find_matches_in(&self, range: Range<u32>) -> impl Iterator<Item = (Range<u32>, bool)> + '_ {
1836        let active = self.find.active_id();
1837        self.decorations.decorations_in(range).filter_map(move |r| match &r.kind {
1838            DecorationKind::FindMatch if r.range.start < r.range.end => {
1839                Some((r.range.clone(), Some(r.id) == active))
1840            }
1841            _ => None,
1842        })
1843    }
1844
1845    /// Fill the scrollbar-overview lanes for the ascending byte-offset bucket
1846    /// `bounds`. The widget passes the `P + 1` boundaries it gets by inverting its
1847    /// own `round(y)` pixel map, so bucket `b` holds exactly the decorations whose
1848    /// mark lands on track pixel `b`. Per bucket `b`: `sev_out[b]` = `(encoded max
1849    /// Diagnostic severity, byte offset of the first severest one)` — `(0, 0)`
1850    /// when empty; `find_out[b]` = the start offset of the first
1851    /// [`DecorationKind::FindMatch`], or `None`. Both lanes are the O(P + log M)
1852    /// summary reduce, never a per-frame whole-store walk.
1853    /// `overview_reduce_equals_linear_scan` (decorations.rs) is the correctness
1854    /// authority.
1855    pub fn overview_marks(
1856        &self,
1857        bounds: &[u32],
1858        sev_out: &mut Vec<(u8, u32)>,
1859        find_out: &mut Vec<Option<u32>>,
1860    ) {
1861        self.decorations.diagnostic_overview(bounds, sev_out);
1862        self.decorations.find_overview(bounds, find_out);
1863    }
1864
1865    /// Refill a **capped** find set, debounced — driven from the widget's
1866    /// `update()` each event. Returns whether it scanned.
1867    ///
1868    /// The match set is repaired eagerly at every commit, so there is no stale
1869    /// set to rescan; the only remaining job is re-growing a capped set's
1870    /// coverage after matches inside it died. Anything else is a no-op — idle
1871    /// documents do zero work here.
1872    pub fn maybe_rescan_find(&mut self, now_ms: u64) -> bool {
1873        self.find.maybe_rescan(&self.buffer, &mut self.decorations, now_ms)
1874    }
1875
1876    /// Select the next find match from the caret, wrapping: sets the selection to
1877    /// the match (head at end), seals the undo group, and returns the match range
1878    /// for the widget to reveal (autoscroll). `None` when there are no matches.
1879    pub fn find_next(&mut self, now_ms: u64) -> Option<Range<u32>> {
1880        self.step_find(true, now_ms)
1881    }
1882
1883    /// Select the previous find match from the caret, wrapping — the
1884    /// [`find_next`](Document::find_next) mirror.
1885    pub fn find_prev(&mut self, now_ms: u64) -> Option<Range<u32>> {
1886        self.step_find(false, now_ms)
1887    }
1888
1889    /// Shared find-navigation body.
1890    fn step_find(&mut self, forward: bool, now_ms: u64) -> Option<Range<u32>> {
1891        // The set is always current at every commit; the one freshness concern
1892        // left is a capped set with room to refill, which the debounced refill
1893        // handles opportunistically before navigating.
1894        self.find.maybe_rescan(&self.buffer, &mut self.decorations, now_ms);
1895        let sel = self.selections.newest();
1896        let (head, extent) = (sel.head(), sel.start()..sel.end());
1897        let found = if forward {
1898            self.find.find_next(head, extent, &self.decorations)
1899        } else {
1900            self.find.find_prev(head, extent, &self.decorations)
1901        };
1902        if let Some(range) = &found {
1903            // Set the selection to the match, head at its end.
1904            self.selections
1905                .set_single(Selection::from_anchor(SelectionId(0), range.start, range.end));
1906            self.reset_transient(); // clears gesture state + seals (a jump is a boundary)
1907            // A match inside a collapsed fold unfolds its chain first — the
1908            // reveal below must land on a VISIBLE position.
1909            self.unfold_to_reveal(range.start);
1910            self.unfold_to_reveal(range.end);
1911            self.request_reveal(RevealMode::Center); // find navigation centers
1912        }
1913        found
1914    }
1915
1916    /// F8 / Shift+F8: select the next/previous diagnostic from the
1917    /// newest selection's start, wrapping — the compile-loop navigation:
1918    /// jump, read the message (hover), fix, F8 again. Selects the
1919    /// diagnostic's span, expands any fold hiding it, and bumps the reveal
1920    /// generation (a jump-class action, like find navigation). `None` — and
1921    /// no movement — when the document has no live diagnostics.
1922    pub fn next_diagnostic(&mut self, forward: bool) -> Option<Range<u32>> {
1923        // Walk in (start, end) lexicographic order from the CURRENT selection's
1924        // span, so two diagnostics sharing a start offset are both reachable —
1925        // a plain `start >` comparison would loop forever between them.
1926        let sel = self.selections.newest();
1927        let anchor = (sel.start(), sel.end());
1928        let mut all: Vec<Range<u32>> = self.diagnostics_in(0..u32::MAX).map(|(r, ..)| r).collect();
1929        all.sort_by_key(|r| (r.start, r.end));
1930        let target = if forward {
1931            all.iter().find(|r| (r.start, r.end) > anchor).or_else(|| all.first())
1932        } else {
1933            all.iter().rev().find(|r| (r.start, r.end) < anchor).or_else(|| all.last())
1934        }?
1935        .clone();
1936        self.selections
1937            .set_single(Selection::from_anchor(SelectionId(0), target.start, target.end));
1938        self.reset_transient(); // a jump is an undo-group boundary
1939        self.unfold_to_reveal(target.start);
1940        self.unfold_to_reveal(target.end);
1941        self.request_reveal(RevealMode::Center); // diagnostic jumps center
1942        Some(target)
1943    }
1944
1945    /// Unfold every collapsed fold hiding `offset`, innermost-first, until the
1946    /// offset renders: a jump-class caret placement — find navigation, bracket
1947    /// jump, select-all-matches — must land on a VISIBLE position. The jump twin
1948    /// of the edit path's `expand_folds_touched`. Visibility is judged by the one
1949    /// owner ([`FoldMap::display_position`]); already-visible offsets (a
1950    /// collapsed tail, a chip edge) unfold nothing.
1951    fn unfold_to_reveal(&mut self, offset: u32) -> bool {
1952        let tab = self.tab_size();
1953        let mut any = false;
1954        loop {
1955            let fm = FoldMap::new(&self.folds, &self.brackets, &self.buffer);
1956            // "Visible" for a JUMP is stricter than "renders somewhere":
1957            // `display_position` clips a chip-hidden column to the chip's
1958            // center, so an offset inside a collapsed INLINE fold still gets a
1959            // position — but the text itself is hidden, and a jump target must
1960            // be readable, so the gap rule is checked too.
1961            let renders = fm.display_position(&self.buffer, offset, tab).is_some();
1962            // Only the inline fold opening just before `offset` can hide it (roots
1963            // are disjoint) — an O(log) tree probe, not a full-set scan.
1964            let chip_hidden = fm.inline_fold_before(offset).is_some_and(|f| f.hides_caret_at(offset));
1965            if renders && !chip_hidden {
1966                return any;
1967            }
1968            // Peel the innermost collapsed pair containing the offset, then
1969            // re-check — an outer unfold can expose a collapsed inner fold.
1970            let target = self
1971                .folds
1972                .iter()
1973                .filter_map(|open| self.brackets.foldable_partner(open).map(|c| (open, c)))
1974                .filter(|&(open, close)| open < offset && offset < close)
1975                .min_by_key(|&(open, close)| close - open);
1976            let Some((open, _)) = target else {
1977                return any; // hidden for a non-fold reason — nothing to peel
1978            };
1979            self.folds.unfold(open);
1980            any = true;
1981        }
1982    }
1983
1984    /// The reveal-request generation. The view autoscrolls to the newest
1985    /// selection whenever this changes — the bridge that lets app-driven find
1986    /// navigation scroll a match into view without the widget's input path.
1987    #[must_use]
1988    pub fn reveal_seq(&self) -> u64 {
1989        self.reveal_seq
1990    }
1991}
1992
1993/// Expand every fold whose **hidden** content the committed edit touched:
1994/// an edit must never change text the user cannot see, so the fold opens to
1995/// reveal it. Edits in a fold's *visible* parts — a block header's line, its
1996/// closing tail, just outside an inline pair's brackets — leave it folded; an
1997/// edit that swallows a fold's brackets entirely is dropped by
1998/// `reconcile_folds` instead. Shared by the commit path and undo/redo (a free
1999/// fn for the same borrow reason as `rebase_views`), so undoing into a folded
2000/// region reveals the reverted text identically.
2001fn expand_folds_touched(
2002    folds: &mut FoldSet,
2003    brackets: &Brackets,
2004    buffer: &Buffer,
2005    tab: u32,
2006    committed: &Committed,
2007) {
2008    if folds.is_empty() || committed.patch().is_empty() {
2009        return;
2010    }
2011    // Only a fold that ENCLOSES or TOUCHES an edit endpoint can hide that edit.
2012    // Their openers lie in `[lo, last]`: `last` (the rightmost endpoint) bounds
2013    // them on the right since an enclosing opener is `<=` the point, and `lo` —
2014    // the outermost folded pair enclosing/touching the FIRST endpoint — bounds
2015    // them on the left (an enclosing fold can open far to the left, but no farther
2016    // than the first point's outermost encloser). One windowed enclosing walk
2017    // finds `lo`; the fold set is then a binary-searched slice, so the whole scan
2018    // is O(folds in the edit span), NOT O(carets · folds-before-each) — a
2019    // leftward enclosing walk per caret would make select-all → fold → type scale
2020    // with that product. The `FoldMap` built from just these folds is identical to
2021    // the full map for these offsets (a non-enclosing fold cannot hide them), so
2022    // the reveal decision is unchanged.
2023    let mut pts: Vec<u32> = Vec::with_capacity(committed.patch().edits().len() * 2);
2024    for e in committed.patch().edits() {
2025        pts.push(e.new.start);
2026        pts.push(e.new.end);
2027    }
2028    pts.sort_unstable();
2029    pts.dedup();
2030    let (first, last) = (pts[0], *pts.last().expect("edits() is non-empty here"));
2031    let lo = brackets
2032        .enclosing_or_touching(first)
2033        .into_iter()
2034        .filter(|&(o, _)| folds.is_folded(o))
2035        .map(|(o, _)| o)
2036        .min()
2037        .unwrap_or(first);
2038    // The folded pairs in the window that actually enclose/touch a point —
2039    // exactly the enclosing/touching set, found by one point probe per windowed
2040    // fold (`∃ p in [o, c+1]`) rather than a per-caret enclosing walk.
2041    let candidates: Vec<(u32, u32)> = folds
2042        .openers_in(lo..last.saturating_add(1))
2043        .iter()
2044        .filter_map(|&o| {
2045            let c = brackets.foldable_partner(o)?;
2046            let touches =
2047                pts.partition_point(|&p| p < o) < pts.partition_point(|&p| p <= c.saturating_add(1));
2048            touches.then_some((o, c))
2049        })
2050        .collect();
2051    if candidates.is_empty() {
2052        return;
2053    }
2054    let mut sub = crate::fold_map::FoldSet::new();
2055    for &(open, _) in &candidates {
2056        sub.fold(open);
2057    }
2058    let fold_map = crate::fold_map::FoldMap::new(&sub, brackets, buffer);
2059    // The block test — is any edit endpoint hidden in a collapsed gap? — reads only
2060    // the point and the map, never the candidate, so all block candidates share one
2061    // answer; compute it once (O(points · log folds)) instead of re-scanning every
2062    // edit for every candidate, which would be O(carets²) even when nothing is
2063    // hidden.
2064    let any_hidden = pts.iter().any(|&p| fold_map.display_position(buffer, p, tab).is_none());
2065    // Inline candidates test their own bracket span against the edit STARTS (only a
2066    // start reveals an inline pair): "∃ start in (opener, close]", binary-searched.
2067    let mut starts: Vec<u32> = committed.patch().edits().iter().map(|e| e.new.start).collect();
2068    starts.sort_unstable();
2069    let touched: Vec<u32> = candidates
2070        .iter()
2071        .filter(|&&(opener, close)| {
2072            let single = buffer.offset_to_point(opener).row == buffer.offset_to_point(close).row;
2073            if single {
2074                // Content edited/inserted anywhere inside the pair reveals it; at
2075                // `[` itself or past `]` doesn't — a start in (opener, close].
2076                starts.partition_point(|&s| s <= close) > starts.partition_point(|&s| s <= opener)
2077            } else {
2078                // Block: `display_position` is `None` exactly in the gap; an edit
2079                // straddling INTO the gap has an endpoint there, one swallowing the
2080                // whole fold broke the pair. Candidate-independent (see above).
2081                any_hidden
2082            }
2083        })
2084        .map(|&(opener, _)| opener)
2085        .collect();
2086    for opener in touched {
2087        folds.unfold(opener);
2088    }
2089}
2090
2091/// The derived views that ride a committed patch — bundled so `rebase_views`
2092/// and its callers name the set once instead of threading five loose `&mut`s
2093/// (and so undo/redo can build it from the destructured `Document` fields). Every
2094/// position-tracked fact the editor shows lives here; adding one is a field here
2095/// plus one line in `rebase_views`, and it is then consistent on all edit paths.
2096struct Views<'a> {
2097    highlight: &'a mut Option<HighlightCache>,
2098    brackets: &'a mut Brackets,
2099    decorations: &'a mut DecorationStore,
2100    /// The auto-close provenance store — a SEPARATE [`DecorationStore`] moved
2101    /// beside `decorations` so a forward edit's pairs rebase for free (and undo/redo
2102    /// inherit the move, though `reset_transient` empties it there first).
2103    autoclose: &'a mut DecorationStore,
2104    folds: &'a mut FoldSet,
2105    find: &'a mut FindState,
2106}
2107
2108/// The single position-mover for a committed edit — rebase every derived view
2109/// through its patch. Taking [`Views`] (not `&mut Document`) is what lets both
2110/// the commit path and undo/redo run the *same* update: undo/redo destructure
2111/// `self` so this can borrow the views while `history` borrows the buffer. A new
2112/// derived fact is added to `Views` and moved here once, staying consistent
2113/// across both paths with no per-feature resync on the undo path.
2114/// Returns whether the bracket structure changed (not a pure offset shift) — the
2115/// caller uses it to skip [`Document::reconcile_folds`] on a structure-neutral
2116/// edit, whose folds provably can't have lost their pair (see the reconcile call
2117/// site).
2118fn rebase_views(
2119    views: &mut Views,
2120    buffer: &Buffer,
2121    tab: u32,
2122    committed: &Committed,
2123) -> core::ops::Range<u32> {
2124    // Highlight cache: splice the transaction's per-edit line spans (built
2125    // below). The size invariant (new_size = old_size − old_count + new_count)
2126    // keeps each edit's `old_lines` provably in-bounds. Only the actually-edited
2127    // lines are invalidated — NOT the first-to-last covering range — so a
2128    // scattered multi-caret edit doesn't over-invalidate the lines between.
2129    if let Some(cache) = views.highlight.as_mut() {
2130        let edits = committed.patch().edits();
2131        if !edits.is_empty() {
2132            // Per-edit pre-edit line spans `(pre_start, old_lines, new_lines)`,
2133            // ascending and coalesced disjoint, so the highlight commit
2134            // invalidates only the actually-edited lines — not the whole
2135            // first-to-last covering range (which would over-invalidate the
2136            // lines between scattered multi-caret edits). `old_lines` comes from
2137            // each edit's replaced text (its inverse op); `new_lines` from the
2138            // post-edit buffer.
2139            let mut spans: Vec<(u32, u32, u32)> = Vec::with_capacity(edits.len());
2140            let mut acc: i64 = 0;
2141            for (e, inv) in edits.iter().zip(committed.inverse_ops()) {
2142                let post_sr = buffer.offset_to_point(e.new.start).row;
2143                let post_er = buffer.offset_to_point(e.new.end).row;
2144                let new_lines = post_er - post_sr + 1;
2145                let old_lines = inv.text.bytes().filter(|&b| b == b'\n').count() as u32 + 1;
2146                let pre_start = (i64::from(post_sr) - acc) as u32;
2147                acc += i64::from(new_lines) - i64::from(old_lines);
2148                match spans.last_mut() {
2149                    // Same-line / touching edits share a pre-edit line — coalesce
2150                    // so the span list stays disjoint (the merge walks need it).
2151                    Some(last) if pre_start < last.0 + last.1 => {
2152                        let merged_end = (last.0 + last.1).max(pre_start + old_lines);
2153                        let combined_delta = (i64::from(last.2) - i64::from(last.1))
2154                            + (i64::from(new_lines) - i64::from(old_lines));
2155                        last.1 = merged_end - last.0;
2156                        last.2 = (i64::from(last.1) + combined_delta) as u32;
2157                    }
2158                    _ => spans.push((pre_start, old_lines, new_lines)),
2159                }
2160            }
2161            // Checkpoints, dense window, and dirty runs all ride the per-edit
2162            // spans — the window stays aimed at the viewport (only edited rows
2163            // invalidated), so a scattered multi-caret edit's wide covering range
2164            // never drains or repositions it.
2165            debug_assert_eq!(
2166                spans.iter().map(|&(_, o, n)| i64::from(n) - i64::from(o)).sum::<i64>(),
2167                i64::from(buffer.line_count()) - i64::from(cache.line_count()),
2168                "per-edit line deltas must sum to the buffer's line-count change",
2169            );
2170            cache.on_commit_patch(&spans);
2171        }
2172    }
2173    // Brackets: splice through the patch (the incremental engine; `match_text`
2174    // remains the load-time constructor and the tests' oracle). It returns the
2175    // reconcile window — the re-matched byte span, or `0..0` when only offsets
2176    // shifted (no structure changed) — the reconcile-skip signal below.
2177    let reconcile_region = views.brackets.apply_edit(committed.patch(), buffer);
2178    // Decorations: the one eager mover for ALL bulk kinds — diagnostics, find
2179    // matches, snippet stops — ride here, so undo/redo need no per-decoration
2180    // handling.
2181    views.decorations.apply_patch(committed.patch());
2182    // Auto-close provenance rides the SAME mover on its own store (per-range
2183    // independence makes the split byte-identical to keeping the pairs in
2184    // `decorations`). On forward edits this rebases the live pairs for free; on
2185    // undo/redo `reset_transient`→`clear_autoclose` has emptied it first, so this
2186    // is a no-op there — keeping it in the one mover means no edit path can ever
2187    // leave a pair stranded at a stale offset.
2188    views.autoclose.apply_patch(committed.patch());
2189    // The find repair rides the same commit hook, AFTER the store move (it needs
2190    // post-patch positions) — the match set is re-verified in a window around
2191    // each edit, so it is always current and undo/redo inherit the repair with no
2192    // find-specific resync.
2193    views.find.on_commit(committed.patch(), buffer, views.decorations);
2194    // Folds ride the same mover: shift on edits above, drop when the interior
2195    // is deleted — so folds survive edits and undo/redo with zero fold-specific
2196    // code (they travel the undo/redo closure forward and backward like a
2197    // decoration).
2198    views.folds.apply_patch(committed.patch());
2199    // Fold reveal rides the one mover too (after the position shift above): an
2200    // edit into HIDDEN text expands its fold — an edit must never alter text the
2201    // user cannot see. Threaded here, not hand-called per edit path, so undo/redo
2202    // inherit it and no edit path can forget it. (`reconcile_folds` — dropping
2203    // folds whose pair an edit broke — stays a once-per-transaction step on each
2204    // path: it can't run per-step inside history's callback. Order is immaterial:
2205    // expand skips broken-pair folds, the only ones reconcile drops.)
2206    expand_folds_touched(views.folds, views.brackets, buffer, tab, committed);
2207    // The re-matched byte region (POST-edit) — the reconcile window (see
2208    // `reconcile_folds_in`). Empty (`0..0`) on a shift-only edit, which re-scanned
2209    // nothing, so `is_empty()` is exactly the "structure unchanged" signal.
2210    reconcile_region
2211}
2212
2213/// Extend a unit-granular drag: keep the origin unit fully selected, with the
2214/// head at the far edge of the unit containing `head`. A forward drag anchors at
2215/// the origin unit's start; a backward drag anchors at its end (the tail flips),
2216/// so the origin word/line is never partially deselected.
2217fn extend_by_unit(origin_unit: (u32, u32), head_unit: (u32, u32), origin: u32, head: u32) -> (u32, u32) {
2218    let (os, oe) = origin_unit;
2219    let (hs, he) = head_unit;
2220    if head >= origin {
2221        (os, he.max(oe))
2222    } else {
2223        (oe, hs.min(os))
2224    }
2225}
2226
2227/// First untaken literal (case-sensitive, non-overlapping like
2228/// `str::match_indices`) occurrence of `needle` whose global start lies in
2229/// `[lo, hi)`, or `None`. Windowed over the buffer's ranged reads with a
2230/// `k−1`-byte tail overlap so a match straddling a window seam is caught, and
2231/// **never materializes the document** (so each Ctrl+D press is O(scanned),
2232/// never O(document)). Byte-identical to a whole-text `match_indices` scan for
2233/// non-self-overlapping needles — the Ctrl+D case, where selections are
2234/// word-shaped and never self-overlap; a self-overlapping needle may differ in
2235/// match phase near a seam. Pinned by
2236/// `windowed_next_occurrence_equals_whole_text_scan`.
2237fn scan_from(
2238    buffer: &Buffer,
2239    needle: &str,
2240    lo: u32,
2241    hi: u32,
2242    is_taken: &impl Fn(u32) -> bool,
2243) -> Option<(u32, u32)> {
2244    let k = needle.len() as u64;
2245    if k == 0 || lo >= hi {
2246        return None;
2247    }
2248    let len = buffer.len();
2249    let window = u64::from(crate::buffer::SCAN_WINDOW).max(k * 2); // ≥ 2k ⇒ always advances
2250    let mut pos = lo;
2251    while u64::from(pos) + k <= u64::from(len) {
2252        let win_end = buffer
2253            .clip_offset((u64::from(pos) + window).min(u64::from(len)) as u32, Bias::Right);
2254        let slice = buffer.slice(pos..win_end);
2255        let mut last_end: Option<u32> = None;
2256        for (i, _) in slice.match_indices(needle) {
2257            let start = pos + i as u32;
2258            if u64::from(start) >= u64::from(hi) {
2259                return None; // matches only grow — nothing left in [lo, hi)
2260            }
2261            last_end = Some(start + k as u32);
2262            if !is_taken(start) {
2263                return Some((start, start + k as u32));
2264            }
2265        }
2266        if win_end >= len {
2267            break;
2268        }
2269        // Resume through the one owner of the seam rule ([`Buffer::scan_resume`])
2270        // — shared verbatim with find's `scan_buffer`.
2271        pos = buffer.scan_resume(pos, win_end, k as u32, last_end);
2272    }
2273    None
2274}
2275
2276/// The next untaken literal occurrence of `needle` in `text` at or after byte
2277/// `from`, wrapping to the start; skips ranges already in `taken` (every
2278/// occurrence shares `needle.len()`). `None` if `needle` is empty or every
2279/// occurrence is already selected. Non-overlapping (`match_indices`) — the
2280/// whole-text `#[cfg(test)]` oracle that [`scan_from`]'s windowed scan is
2281/// checked against (`windowed_next_occurrence_equals_whole_text_scan`).
2282#[cfg(test)]
2283fn find_next_occurrence(text: &str, needle: &str, from: u32, taken: &[(u32, u32)]) -> Option<(u32, u32)> {
2284    if needle.is_empty() {
2285        return None;
2286    }
2287    let len = needle.len() as u32;
2288    let is_taken = |start: u32| taken.iter().any(|&(s, e)| s == start && e == start + len);
2289    let mut wrapped: Option<u32> = None;
2290    for (idx, _) in text.match_indices(needle) {
2291        let start = idx as u32;
2292        if is_taken(start) {
2293            continue;
2294        }
2295        if start >= from {
2296            return Some((start, start + len));
2297        }
2298        if wrapped.is_none() {
2299            wrapped = Some(start); // first candidate before `from`, used on wrap
2300        }
2301    }
2302    wrapped.map(|start| (start, start + len))
2303}
2304
2305#[cfg(test)]
2306mod tests {
2307    use super::*;
2308    use crate::history::{GroupingHint, OpClass};
2309
2310    fn doc(s: &str) -> Document {
2311        Document::new(s).unwrap()
2312    }
2313
2314    fn typ(op: OpClass) -> GroupingHint {
2315        GroupingHint::mergeable(op)
2316    }
2317
2318    /// The incremental brackets must deep-equal a from-scratch match of the
2319    /// live text — the bracket oracle, at Document level.
2320    fn assert_brackets_fresh(d: &Document) {
2321        let fresh = crate::bracket::Brackets::match_text(&d.text());
2322        assert_eq!(d.brackets().all(), fresh.all(), "incremental brackets diverged from scratch");
2323    }
2324
2325    #[test]
2326    fn brackets_ride_edits_and_undo_redo_through_the_one_mover() {
2327        // A random walk of edits, undos, and redos: after EVERY step the
2328        // incremental bracket structure must equal a from-scratch match —
2329        // proving the one-mover claim (rebase_views runs the splice on the
2330        // forward path AND per undo/redo step; no bracket-specific resync
2331        // exists anywhere).
2332        let mut d = doc("fn a() {\n    x[0] = (1 + 2);\n}\n");
2333        let mut rng = 0xC0FFEEu64;
2334        let mut next = move |m: u64| {
2335            rng ^= rng << 13;
2336            rng ^= rng >> 7;
2337            rng ^= rng << 17;
2338            rng % m
2339        };
2340        let inserts = ["{", "}", "(", ")", "[", "]", "\n", "x", "{}", "([)", "\n}\n"];
2341        for i in 0..200 {
2342            match next(4) {
2343                0 | 1 => {
2344                    let at = next(d.buffer().len() as u64 + 1) as u32;
2345                    let at = d.buffer().clip_offset(at, crate::Bias::Left);
2346                    let text = inserts[next(inserts.len() as u64) as usize];
2347                    let _ = d.edit(vec![EditOp::insert(at, text)]);
2348                }
2349                2 => {
2350                    let a = next(d.buffer().len() as u64 + 1) as u32;
2351                    let a = d.buffer().clip_offset(a, crate::Bias::Left);
2352                    let b = (a + 1 + next(4) as u32).min(d.buffer().len());
2353                    let b = d.buffer().clip_offset(b, crate::Bias::Right);
2354                    if a < b {
2355                        let _ = d.edit(vec![EditOp::delete(a..b)]);
2356                    }
2357                }
2358                _ => {
2359                    if next(2) == 0 {
2360                        d.undo();
2361                    } else {
2362                        d.redo();
2363                    }
2364                }
2365            }
2366            assert_brackets_fresh(&d);
2367            let _ = i;
2368        }
2369    }
2370
2371    #[test]
2372    fn edit_undo_redo_round_trip() {
2373        let mut d = doc("hello");
2374        d.edit(vec![EditOp::new(0..5, "world")]).unwrap();
2375        assert_eq!(d.text(), "world");
2376        assert!(d.undo());
2377        assert_eq!(d.text(), "hello");
2378        assert!(d.redo());
2379        assert_eq!(d.text(), "world");
2380        // Nothing left to redo.
2381        assert!(d.undo());
2382        assert!(!d.undo()); // stack empty
2383        assert_eq!(d.text(), "hello");
2384    }
2385
2386    #[test]
2387    fn a_new_edit_invalidates_redo() {
2388        let mut d = doc("");
2389        d.edit(vec![EditOp::insert(0, "a")]).unwrap();
2390        d.edit(vec![EditOp::insert(1, "b")]).unwrap(); // "ab"
2391        d.undo(); // "a"
2392        d.edit(vec![EditOp::insert(1, "X")]).unwrap(); // "aX" — diverges
2393        assert_eq!(d.text(), "aX");
2394        assert!(!d.redo(), "redo must be cleared by the divergent edit");
2395        assert_eq!(d.text(), "aX");
2396    }
2397
2398    #[test]
2399    fn a_retained_branch_is_reachable_from_the_fork() {
2400        // Branching undo: the branch `a_new_edit_invalidates_redo` discards
2401        // is not gone — it is RETAINED as a sibling. Plain redo still does nothing
2402        // at the tip of the new branch, but undoing back to the fork exposes both
2403        // branches and either is selectable. A two-stack history could not do this
2404        // (a divergent edit would clear the redo stack); the branching tree keeps
2405        // both reachable.
2406        let mut d = doc("");
2407        d.edit(vec![EditOp::insert(0, "a")]).unwrap();
2408        d.edit(vec![EditOp::insert(1, "b")]).unwrap(); // "ab"
2409        d.undo(); // "a"
2410        d.edit(vec![EditOp::insert(1, "X")]).unwrap(); // "aX" — diverges, "b" kept
2411        assert!(!d.redo(), "plain redo does nothing at the tip of the new branch");
2412        // Undo back to the fork exposes BOTH branches.
2413        assert!(d.undo());
2414        assert_eq!(d.text(), "a");
2415        assert_eq!(d.redo_branch_count(), 2, "the 'b' and 'X' branches both survive");
2416        assert!(d.can_redo());
2417        // Steer to the older, retained branch (index 0 = 'b') and redo into it.
2418        assert!(d.select_redo_branch(0));
2419        assert!(d.redo());
2420        assert_eq!(d.text(), "ab", "the retained 'b' branch is fully reachable");
2421        // The newer branch is still there too.
2422        assert!(d.undo());
2423        assert!(d.select_redo_branch(1));
2424        assert!(d.redo());
2425        assert_eq!(d.text(), "aX");
2426    }
2427
2428    #[test]
2429    fn opt_in_undo_limit_bounds_reach_and_keeps_recent() {
2430        // Opt-in pruning: with a limit of 3, older units drop while the most
2431        // recent 3 stay reachable and the text is untouched. The default (no
2432        // limit) prunes nothing — every other undo test above relies on that.
2433        let mut d = doc("");
2434        d.set_undo_limit(Some(3));
2435        for (i, ch) in "abcde".chars().enumerate() {
2436            d.edit(vec![EditOp::insert(i as u32, ch.to_string())]).unwrap();
2437        }
2438        assert_eq!(d.text(), "abcde");
2439        assert_eq!(d.undo_depth(), 3, "only the last 3 units are retained");
2440        assert!(d.undo()); // "abcd"
2441        assert!(d.undo()); // "abc"
2442        assert!(d.undo()); // "ab"
2443        assert_eq!(d.text(), "ab");
2444        assert!(!d.undo(), "the pruned units ('a','b' inserts) are gone");
2445        assert!(!d.can_undo());
2446        // Redo still runs forward from the pruned base.
2447        assert!(d.redo());
2448        assert_eq!(d.text(), "abc");
2449    }
2450
2451    #[test]
2452    fn pruning_preserves_branches_within_the_window() {
2453        // The arena rebuild must remap children/preferred_child — a fork inside
2454        // the kept window survives pruning with both branches still selectable.
2455        let mut d = doc("");
2456        d.edit(vec![EditOp::insert(0, "a")]).unwrap(); // "a"
2457        d.edit(vec![EditOp::insert(1, "b")]).unwrap(); // "ab"
2458        d.undo(); // back to "a"; the "b" branch is retained
2459        d.edit(vec![EditOp::insert(1, "X")]).unwrap(); // "aX"; "a" is now a fork
2460        // Limit 1 makes the fork the new base, keeping BOTH of its child branches.
2461        d.set_undo_limit(Some(1));
2462        assert_eq!(d.undo_depth(), 1);
2463        assert!(d.undo()); // back to the fork ("a")
2464        assert_eq!(d.text(), "a");
2465        assert_eq!(d.redo_branch_count(), 2, "both branches survived the rebuild");
2466        assert!(d.select_redo_branch(0));
2467        assert!(d.redo());
2468        assert_eq!(d.text(), "ab", "the retained branch is still reachable after a prune");
2469    }
2470
2471    #[test]
2472    fn typing_run_merges_into_one_undo_step() {
2473        // Five keystrokes with mergeable Type hints → one undo removes them all.
2474        let mut d = doc("");
2475        for (i, ch) in "hello".chars().enumerate() {
2476            d.edit_grouped(vec![EditOp::insert(i as u32, ch.to_string())], typ(OpClass::Type))
2477                .unwrap();
2478        }
2479        assert_eq!(d.text(), "hello");
2480        assert!(d.undo());
2481        assert_eq!(d.text(), "", "the whole typing run undoes as one unit");
2482        assert!(d.redo());
2483        assert_eq!(d.text(), "hello", "and redoes as one unit");
2484    }
2485
2486    #[test]
2487    fn fold_toggle_seals_undo_and_clears_the_expand_ladder() {
2488        // A fold toggle moves carets (ejection), so it is a gesture boundary:
2489        // typing must not merge across it, and the expand ladder must clear so a
2490        // later shrink cannot restore a caret INTO the collapsed fold.
2491        let mut d = doc("m {\n  word\n}");
2492        d.set_selections(SelectionSet::new(7)); // inside "word"
2493        d.type_char('x');
2494        d.expand_selection(); // pushes the pre-expansion set on the ladder
2495        assert!(d.toggle_fold_opener(2));
2496        d.shrink_selection(); // ladder cleared by the toggle → no-op…
2497        let head = d.selections().newest().head();
2498        let fm = crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer());
2499        assert!(
2500            fm.display_position(d.buffer(), head, d.tab_size()).is_some(),
2501            "…so the caret cannot be restored into the collapsed fold"
2502        );
2503        // And the toggle sealed the typing group: the later run undoes alone.
2504        d.type_char('y');
2505        assert!(d.undo());
2506        assert!(d.text().contains('x'), "undo reverted only the post-toggle typing");
2507    }
2508
2509    #[test]
2510    fn find_navigation_expands_a_collapsed_inline_fold() {
2511        // display_position clips a chip-hidden column to the chip center, so an
2512        // offset inside a collapsed inline fold still reports a position;
2513        // unfold_to_reveal must expand the fold anyway so a find jump lands on
2514        // visible text, not inside the chip.
2515        let mut d = doc("x = [1, 2, 3]\nafter");
2516        assert!(d.toggle_fold_opener(4)); // the `[`
2517        d.set_find_query(Some(FindQuery { text: "2".into(), case_sensitive: false }), 0);
2518        d.find_next(0).expect("the match exists");
2519        assert!(!d.folds().is_folded(4), "the inline fold expanded to show the match");
2520    }
2521
2522    #[test]
2523    fn editing_hidden_text_expands_only_the_enclosing_fold() {
2524        // expand_folds_touched, windowed to the enclosing folds: an edit into
2525        // a collapsed block's hidden interior expands THAT block (never edit text
2526        // the user can't see) and leaves every other fold collapsed — the walk
2527        // visits only the folds enclosing the edit, not all of them.
2528        let text = "a {\n  keep\n}\nb {\n  body\n}\nc\n";
2529        let mut d = doc(text);
2530        let opens: Vec<u32> = text.match_indices('{').map(|(i, _)| i as u32).collect();
2531        // `keep` is the first block, `edited` the second. Both openers sit BEFORE
2532        // the edit, so their offsets don't shift — the checks stay valid.
2533        let (keep, edited) = (opens[0], opens[1]);
2534        assert!(d.toggle_fold_opener(keep));
2535        assert!(d.toggle_fold_opener(edited));
2536        assert!(d.folds().is_folded(keep) && d.folds().is_folded(edited));
2537        // Edit inside the SECOND block's hidden interior (its "body" row).
2538        let inside = text.find("body").unwrap() as u32 + 2;
2539        d.edit(vec![EditOp::insert(inside, "X")]).unwrap();
2540        assert!(!d.folds().is_folded(edited), "editing inside the second block expands it");
2541        assert!(d.folds().is_folded(keep), "the untouched first block stays folded");
2542    }
2543
2544    #[test]
2545    fn next_diagnostic_advances_past_a_shared_start_offset() {
2546        use crate::{Diagnostic, Severity};
2547        // Two diagnostics share a start offset: the (start, end) lexicographic
2548        // walk must reach both and then wrap, never cycle between them.
2549        let mut d = doc("abcdef");
2550        let rev = d.revision();
2551        d.set_diagnostics(
2552            rev,
2553            vec![
2554                Diagnostic::new(1..3, Severity::Error, "a"),
2555                Diagnostic::new(1..5, Severity::Warning, "b"),
2556            ],
2557        );
2558        assert_eq!(d.next_diagnostic(true), Some(1..3));
2559        assert_eq!(d.next_diagnostic(true), Some(1..5), "the same-start sibling is reachable");
2560        assert_eq!(d.next_diagnostic(true), Some(1..3), "and the walk wraps");
2561    }
2562
2563    #[test]
2564    fn reveal_classes_and_no_op_verbs() {
2565        use crate::{Diagnostic, Severity};
2566        // Find-family jumps request Center; bracket hops request Fit; verbs
2567        // that change nothing request nothing — a no-op F8 must not autoscroll
2568        // the viewport back to the caret.
2569        let mut d = doc("f( ab )");
2570        let seq0 = d.reveal_seq();
2571        d.next_diagnostic(true); // no diagnostics → no request
2572        assert_eq!(d.reveal_seq(), seq0, "a no-op F8 requests no reveal");
2573        d.set_selections(SelectionSet::new(3));
2574        d.jump_to_bracket(); // no adjacent bracket? offset 3 is inside ( ) → moves
2575        assert!(d.reveal_seq() > seq0, "a real bracket hop requests a reveal");
2576        assert_eq!(d.reveal_mode(), RevealMode::Fit, "…of the Fit class");
2577        let rev = d.revision();
2578        d.set_diagnostics(rev, vec![Diagnostic::new(1..2, Severity::Error, "e")]);
2579        let seq1 = d.reveal_seq();
2580        d.next_diagnostic(true);
2581        assert!(d.reveal_seq() > seq1);
2582        assert_eq!(d.reveal_mode(), RevealMode::Center, "a diagnostic jump centers");
2583    }
2584
2585    #[test]
2586    fn ctrl_d_force_reveals_while_select_all_holds() {
2587        // Ctrl+D jumps to the just-added cursor (FitForce); Ctrl+Shift+L reveals
2588        // with Fit (holds the viewport if a cursor is already visible).
2589        let mut d = doc("foo bar foo baz foo");
2590        d.set_selections(SelectionSet::new(0));
2591        d.add_next_occurrence(); // expand to "foo"
2592        d.add_next_occurrence(); // add the next "foo" — a new cursor to jump to
2593        assert_eq!(d.reveal_mode(), RevealMode::FitForce, "Ctrl+D force-reveals the new cursor");
2594        let mut e = doc("foo bar foo baz foo");
2595        e.set_selections(SelectionSet::new(0));
2596        e.select_all_occurrences();
2597        assert_eq!(e.reveal_mode(), RevealMode::Fit, "Ctrl+Shift+L holds if a cursor is visible");
2598    }
2599
2600    #[test]
2601    fn diagnostic_navigation_wraps_selects_and_reveals() {
2602        use crate::{Diagnostic, Severity};
2603        let mut d = doc("aa bb\ncc\ndd");
2604        let rev = d.revision();
2605        d.set_diagnostics(
2606            rev,
2607            vec![
2608                Diagnostic::new(3..5, Severity::Warning, "w"),
2609                Diagnostic::new(9..11, Severity::Error, "e"),
2610            ],
2611        );
2612        assert_eq!(d.next_diagnostic(true), Some(3..5));
2613        assert_eq!(d.next_diagnostic(true), Some(9..11));
2614        assert_eq!(d.next_diagnostic(true), Some(3..5), "wraps to the first");
2615        assert_eq!(d.next_diagnostic(false), Some(9..11), "prev wraps to the last");
2616        let s = d.selections().newest();
2617        assert_eq!((s.start(), s.end()), (9, 11), "the diagnostic's span is selected");
2618        // No diagnostics → None, caret untouched.
2619        let mut e = doc("x");
2620        e.set_selections(SelectionSet::new(1));
2621        assert!(e.next_diagnostic(true).is_none());
2622        assert_eq!(e.selections().newest().head(), 1);
2623        // A diagnostic hidden in a collapsed fold is revealed on arrival.
2624        let mut f = doc("aa\nfn {\nbb\n}\ncc");
2625        assert!(f.toggle_fold_opener(6));
2626        let rev = f.revision();
2627        f.set_diagnostics(rev, vec![Diagnostic::new(8..10, Severity::Error, "hidden")]);
2628        assert_eq!(f.next_diagnostic(true), Some(8..10));
2629        assert!(!f.folds().is_folded(6), "the fold expanded to show the diagnostic");
2630    }
2631
2632    #[test]
2633    fn caret_word_occurrences_are_whole_word_only() {
2634        let mut d = doc("foo foobar foo\nfoo");
2635        d.set_selections(SelectionSet::new(1)); // inside the first "foo"
2636        assert_eq!(
2637            d.caret_word_occurrences(0..u32::MAX),
2638            vec![0..3, 11..14, 15..18],
2639            "the foobar prefix is not a whole-word match"
2640        );
2641        // The window bounds the scan (the widget passes its viewport)…
2642        assert_eq!(d.caret_word_occurrences(4..15), vec![11..14], "only in-window matches");
2643        // …a non-empty selection produces no occurrence wash…
2644        let mut set = SelectionSet::new(0);
2645        set.set_single(Selection::from_anchor(SelectionId(0), 0, 3));
2646        d.set_selections(set);
2647        assert!(d.caret_word_occurrences(0..u32::MAX).is_empty());
2648        // …and neither does a caret in open whitespace.
2649        let mut w = doc("a  b");
2650        w.set_selections(SelectionSet::new(2));
2651        assert!(w.caret_word_occurrences(0..u32::MAX).is_empty());
2652    }
2653
2654    #[test]
2655    fn find_navigation_expands_a_collapsed_fold_to_reveal_the_match() {
2656        // A match hidden inside a collapsed block: find_next must unfold it —
2657        // the caret can never land on an invisible position.
2658        let mut d = doc("aa\nfn {\nneedle\n}\nbb");
2659        let opener = 6; // the `{` (pair 6..15, hides rows 2..=3)
2660        assert!(d.toggle_fold_opener(opener));
2661        d.set_find_query(Some(FindQuery { text: "needle".into(), case_sensitive: false }), 0);
2662        let m = d.find_next(0).expect("the match exists");
2663        assert!(!d.folds().is_folded(opener), "the fold expanded to reveal the match");
2664        let fm = crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer());
2665        assert!(
2666            fm.display_position(d.buffer(), m.end, d.tab_size()).is_some(),
2667            "the match head renders"
2668        );
2669        // A match on VISIBLE ground (the header line) leaves folds alone.
2670        let mut v = doc("aa\nfn {\nx\n}\nbb");
2671        assert!(v.toggle_fold_opener(6));
2672        v.set_find_query(Some(FindQuery { text: "fn".into(), case_sensitive: false }), 0);
2673        v.find_next(0).expect("match on the header");
2674        assert!(v.folds().is_folded(6), "a visible match keeps the fold collapsed");
2675    }
2676
2677    #[test]
2678    fn jump_to_bracket_crosses_and_returns() {
2679        // `f( ab )` — ( at 1, ) at 6. Right of `(`: cross to the same side of
2680        // the partner (after `)`), and a second press returns.
2681        let mut d = doc("f( ab )");
2682        d.set_selections(SelectionSet::new(2));
2683        d.jump_to_bracket();
2684        assert_eq!(d.selections().newest().head(), 7);
2685        d.jump_to_bracket();
2686        assert_eq!(d.selections().newest().head(), 2);
2687        // No adjacent bracket: jump to the enclosing pair's closer.
2688        d.set_selections(SelectionSet::new(4)); // inside "ab"
2689        d.jump_to_bracket();
2690        assert_eq!(d.selections().newest().head(), 6, "lands before `)`");
2691        // No bracket in reach: the caret stays put.
2692        let mut e = doc("plain");
2693        e.set_selections(SelectionSet::new(3));
2694        e.jump_to_bracket();
2695        assert_eq!(e.selections().newest().head(), 3);
2696    }
2697
2698    #[test]
2699    fn expand_selection_climbs_the_bracket_ladder_and_shrinks_back() {
2700        // `m { a(bb) }` — offsets: m0 ␠1 {2 ␠3 a4 (5 b6 b7 )8 ␠9 }10.
2701        let mut d = doc("m { a(bb) }");
2702        d.set_selections(SelectionSet::new(7)); // caret in "bb"
2703        let sel = |d: &Document| (d.selections().newest().start(), d.selections().newest().end());
2704        d.expand_selection();
2705        assert_eq!(sel(&d), (6, 8), "word first");
2706        d.expand_selection();
2707        assert_eq!(sel(&d), (5, 9), "word == () contents, so the pair incl. brackets");
2708        d.expand_selection();
2709        assert_eq!(sel(&d), (3, 10), "the brace contents");
2710        d.expand_selection();
2711        assert_eq!(sel(&d), (2, 11), "the brace pair incl. brackets");
2712        d.expand_selection();
2713        assert_eq!(sel(&d), (0, 11), "the whole document");
2714        d.expand_selection();
2715        assert_eq!(sel(&d), (0, 11), "fully expanded is a no-op");
2716        // Shrink walks back down the exact ladder.
2717        d.shrink_selection();
2718        assert_eq!(sel(&d), (2, 11));
2719        d.shrink_selection();
2720        assert_eq!(sel(&d), (3, 10));
2721        // Any other gesture clears the ladder — shrink becomes a no-op.
2722        d.move_carets(Motion::Right, false);
2723        let caret = sel(&d);
2724        d.shrink_selection();
2725        assert_eq!(sel(&d), caret, "the ladder cleared on the caret move");
2726    }
2727
2728    #[test]
2729    fn add_caret_vertical_stacks_and_stops_at_edges() {
2730        // Stack carets down a column: every caret gains a neighbour, landings
2731        // on existing carets merge, and typing edits the whole column.
2732        let mut d = doc("aaa\nbbb\nccc");
2733        d.set_selections(SelectionSet::new(5)); // row 1, col 1
2734        d.add_caret_vertical(false); // above → row 0
2735        d.add_caret_vertical(true); // below both → rows 1 (merges) and 2
2736        assert_eq!(d.selections().len(), 3);
2737        d.type_char('X');
2738        assert_eq!(d.text(), "aXaa\nbXbb\ncXcc");
2739        // On the top display row, Up adds nothing (no clamp-to-doc-start caret).
2740        let mut e = doc("aa\nbb");
2741        e.set_selections(SelectionSet::new(1));
2742        e.add_caret_vertical(false);
2743        assert_eq!(e.selections().len(), 1);
2744    }
2745
2746    #[test]
2747    fn add_caret_vertical_skips_a_collapsed_fold() {
2748        let mut d = doc("aa\nfn {\nbb\ncc\n}\ndd");
2749        assert!(d.toggle_fold_opener(6)); // hides rows 2..=4
2750        d.set_selections(SelectionSet::new(16)); // "dd", display row 2
2751        d.add_caret_vertical(false);
2752        let rows: Vec<u32> =
2753            d.selections().all().iter().map(|s| d.buffer().offset_to_point(s.head()).row).collect();
2754        assert_eq!(rows, vec![1, 5], "the new caret lands on the fold header, not a hidden row");
2755    }
2756
2757    #[test]
2758    fn select_all_occurrences_takes_every_match_from_a_word_seed() {
2759        // Bare caret in "foo": seed the word, then take ALL occurrences —
2760        // typing then replaces every one (the multi-cursor rename).
2761        let mut d = doc("foo bar foo baz foo");
2762        d.set_selections(SelectionSet::new(1));
2763        d.select_all_occurrences();
2764        assert_eq!(d.selections().len(), 3);
2765        d.type_char('X');
2766        assert_eq!(d.text(), "X bar X baz X");
2767        // No word under the caret ⇒ no change.
2768        let mut e = doc("   ");
2769        e.set_selections(SelectionSet::new(1));
2770        e.select_all_occurrences();
2771        assert_eq!(e.selections().len(), 1);
2772        assert!(e.selections().newest().is_empty());
2773    }
2774
2775    #[test]
2776    fn select_find_matches_turns_matches_into_selections() {
2777        let mut d = doc("foo bar foo baz foo");
2778        d.set_find_query(Some(FindQuery { text: "foo".into(), case_sensitive: false }), 0);
2779        d.find_next(0); // activate the first match (caret there)
2780        assert!(d.select_find_matches());
2781        assert_eq!(d.selections().len(), 3);
2782        assert_eq!(d.selections().newest().start(), 0, "the ACTIVE match is newest");
2783        // No query ⇒ no live matches ⇒ no-op.
2784        let mut e = doc("abc");
2785        assert!(!e.select_find_matches());
2786        assert_eq!(e.selections().len(), 1);
2787    }
2788
2789    #[test]
2790    fn caret_moves_seal_the_typing_group() {
2791        // A caret move, click, or box gesture closes the open typing run —
2792        // type → move → type undoes as TWO steps, not one.
2793        let mut d = doc("");
2794        d.type_char('f');
2795        d.type_char('o');
2796        d.move_carets(Motion::Left, false); // arrow-key boundary
2797        d.type_char('x'); // "fxo"
2798        assert!(d.undo());
2799        assert_eq!(d.text(), "fo", "undo reverts only the run typed after the move");
2800
2801        // A mouse click (set_selections) is the same boundary…
2802        let mut c = doc("");
2803        c.type_char('a');
2804        c.type_char('b');
2805        c.set_selections(SelectionSet::new(0));
2806        c.type_char('z'); // "zab"
2807        assert!(c.undo());
2808        assert_eq!(c.text(), "ab", "the click sealed the run before it");
2809
2810        // …and so is a column-box gesture (which seals WITHOUT reset_transient,
2811        // since it must keep the box anchor).
2812        let mut b = doc("one\ntwo");
2813        b.set_selections(SelectionSet::new(0));
2814        b.type_char('z'); // "zone\ntwo"
2815        b.column_drag((0, 2), (1, 2));
2816        b.type_char('q');
2817        assert!(b.undo());
2818        assert_eq!(b.text(), "zone\ntwo", "the box gesture sealed the run before it");
2819    }
2820
2821    /// A line-duplication verb must drop auto-close provenance through the one
2822    /// owner (`clear_autoclose`), so its `AutoClosePair` decoration (which has
2823    /// `EmptyPolicy::Keep`, so the store never self-drops it) is removed with it
2824    /// rather than orphaned in the store with a lost id.
2825    #[test]
2826    fn line_duplication_drops_autoclose_provenance() {
2827        // Provenance lives in its own auto-close store.
2828        let ac_count = |d: &Document| d.autoclose_pair_count();
2829        let mut d = doc("x");
2830        d.move_carets(Motion::Right, false); // caret after 'x' (offset 1)
2831        d.type_char('('); // auto-close → "x()", provenance armed over the pair
2832        assert_eq!(d.text(), "x()");
2833        assert_eq!(ac_count(&d), 1, "typing '(' arms one AutoClosePair decoration");
2834        // Duplicate the line above: the caret stays inside the (one-line) pair,
2835        // so `validate_autoclose` keeps the provenance — and copy_line must clear
2836        // the decoration rather than leave the rebased one orphaned.
2837        d.copy_line(false);
2838        assert_eq!(d.text(), "x()\nx()");
2839        assert_eq!(
2840            ac_count(&d),
2841            0,
2842            "copy_line must clear the provenance decoration, not orphan it in the store",
2843        );
2844    }
2845
2846    /// Provenance in its OWN store must be untouched by a document-scale
2847    /// diagnostic set living in the bulk `decorations` store: arm a pair, publish
2848    /// far diagnostics, type inside the pair — the pair survives, overtype still
2849    /// works, and the diagnostics ride on independently. The two stores stay
2850    /// fully decoupled.
2851    #[test]
2852    fn type_bracket_then_far_diagnostics_pair_survives() {
2853        use crate::{Diagnostic, Severity};
2854        let mut d = doc("hello\nworld\n");
2855        let end = d.text().len() as u32; // 12 — EOL of the empty final line
2856        d.set_selections(SelectionSet::new(end));
2857        d.type_char('('); // "hello\nworld\n()" — provenance armed, caret between ( )
2858        assert_eq!(d.autoclose_pair_count(), 1);
2859        // Diagnostics FAR from the pair (line 0), in the SEPARATE bulk store.
2860        d.set_diagnostics(
2861            d.revision(),
2862            vec![
2863                Diagnostic::new(0..1, Severity::Error, "e0"),
2864                Diagnostic::new(2..3, Severity::Warning, "w0"),
2865            ],
2866        );
2867        // A plain char inside the pair — the pair must survive (caret stays in it),
2868        // undisturbed by the diagnostics in the other store.
2869        d.type_char('x');
2870        assert!(d.text().ends_with("(x)"));
2871        assert_eq!(d.autoclose_pair_count(), 1, "far diagnostics don't disturb the pair");
2872        // The diagnostics still live in the bulk store, rebased past the edit.
2873        assert_eq!(d.diagnostics_in(0..u32::MAX).count(), 2, "the diagnostics ride on");
2874        // Overtype still works — provenance is intact and readable from its store.
2875        d.type_char(')');
2876        assert!(d.text().ends_with("(x)"), "overtype consumes the tracked close, no doubled )");
2877        assert_eq!(d.autoclose_pair_count(), 0, "overtype consumed the pair");
2878    }
2879
2880    /// Oracle: the own auto-close store must MOVE identically to keeping the pairs
2881    /// in the unified `decorations` store (per-range independence makes the split
2882    /// byte-identical). A shadow set of `AutoClosePair` decorations is planted in
2883    /// the bulk store (interleaved with diagnostics, a realistic mixed store) at
2884    /// the same ranges; then plain chars are typed *inside* every pair (carets
2885    /// stay in, so `validate_autoclose` reaps nothing), and after each commit the
2886    /// own store's ranges must equal the bulk store's `AutoClosePair` ranges —
2887    /// through BOTH the multi-edit naive mover and the single-edit windowed mover.
2888    /// A wiring slip (a missed rebase site, a wrong patch, a bias drift) breaks
2889    /// it.
2890    #[test]
2891    fn autoclose_store_moves_identically_to_unified_store() {
2892        use crate::decorations::{DecorationKind, Stickiness};
2893        use crate::{Diagnostic, Severity};
2894        // Run one scenario: arm a pair at each caret, mirror the pairs into the
2895        // bulk store, then type `steps` plain chars at all carets, asserting the two
2896        // stores stay range-identical after every commit.
2897        let scenario = |text: &str, carets: &[u32], steps: usize| {
2898            let mut d = doc(text);
2899            d.set_selections(SelectionSet::from_offsets(carets));
2900            d.type_char('('); // arm one one-line pair per caret
2901            let pairs = d.autoclose_ranges();
2902            assert_eq!(pairs.len(), carets.len(), "one pair per caret armed");
2903            // Shadow the pairs into the bulk store (the "unified" reference) and
2904            // interleave a diagnostic so the mover sees a realistic mixed store.
2905            for r in &pairs {
2906                d.decorations_mut().add_decoration(
2907                    r.clone(),
2908                    DecorationKind::AutoClosePair,
2909                    Stickiness::AlwaysGrows,
2910                );
2911            }
2912            d.set_diagnostics(d.revision(), vec![Diagnostic::new(0..1, Severity::Warning, "d")]);
2913            let unified = |d: &Document| -> Vec<Range<u32>> {
2914                d.decorations()
2915                    .iter()
2916                    .filter(|r| matches!(r.kind, DecorationKind::AutoClosePair))
2917                    .map(|r| r.range.clone())
2918                    .collect()
2919            };
2920            assert_eq!(d.autoclose_ranges(), unified(&d), "shadow starts equal");
2921            for step in 0..steps {
2922                d.type_char('z'); // grows every pair; carets stay inside
2923                assert_eq!(
2924                    d.autoclose_ranges(),
2925                    unified(&d),
2926                    "own store diverged from the unified store at step {step}",
2927                );
2928            }
2929        };
2930        // Multi-caret typing keeps all pairs occupied → the multi-edit naive mover.
2931        scenario("aa\nbb\ncc\ndd\nee\n", &[2, 5, 8, 11, 14], 8);
2932        // Single caret → the single-edit windowed mover (one pair, kept occupied).
2933        scenario("solo line here\n", &[14], 10);
2934    }
2935
2936    #[test]
2937    fn different_classes_do_not_merge() {
2938        let mut d = doc("ab");
2939        d.edit_grouped(vec![EditOp::insert(2, "c")], typ(OpClass::Type)).unwrap(); // "abc"
2940        d.edit_grouped(vec![EditOp::delete(0..1)], typ(OpClass::Delete)).unwrap(); // "bc"
2941        assert_eq!(d.text(), "bc");
2942        assert!(d.undo()); // undo the delete only
2943        assert_eq!(d.text(), "abc");
2944        assert!(d.undo()); // undo the type
2945        assert_eq!(d.text(), "ab");
2946    }
2947
2948    #[test]
2949    fn dirty_tracking_survives_undo_and_save() {
2950        let mut d = doc("x");
2951        assert!(!d.is_dirty());
2952        d.edit(vec![EditOp::insert(1, "y")]).unwrap(); // "xy"
2953        assert!(d.is_dirty());
2954        d.undo(); // back to "x" == the saved (initial) state
2955        assert!(!d.is_dirty(), "undo back to the save point reads clean");
2956        d.redo(); // "xy"
2957        assert!(d.is_dirty());
2958        d.mark_saved();
2959        assert!(!d.is_dirty());
2960    }
2961
2962    #[test]
2963    fn ctrl_d_selects_word_then_adds_occurrences_then_stops() {
2964        let mut d = doc("foo bar foo baz foo"); // "foo" at 0, 8, 16
2965        // First press: the caret at 0 expands to the surrounding word.
2966        d.add_next_occurrence();
2967        assert_eq!(d.selections().len(), 1);
2968        let s = d.selections().newest();
2969        assert_eq!((s.start(), s.end()), (0, 3));
2970        // Next presses add each following "foo" as a new (newest) selection.
2971        d.add_next_occurrence();
2972        assert_eq!(d.selections().len(), 2);
2973        assert_eq!((d.selections().newest().start(), d.selections().newest().end()), (8, 11));
2974        d.add_next_occurrence();
2975        assert_eq!(d.selections().len(), 3);
2976        assert_eq!(d.selections().newest().start(), 16);
2977        // All three found; the next press wraps, finds only taken ranges → no-op.
2978        d.add_next_occurrence();
2979        assert_eq!(d.selections().len(), 3, "no untaken occurrence remains");
2980    }
2981
2982    #[test]
2983    fn windowed_next_occurrence_equals_whole_text_scan() {
2984        // The windowed `scan_from` (Ctrl+D's engine, never materializes the
2985        // rope) must match `find_next_occurrence` (the whole-text `match_indices`
2986        // oracle) for non-self-overlapping needles, across random texts, seeds,
2987        // and taken sets — including the wrap path.
2988        let mut state: u64 = 0x243F_6A88_85A3_08D3;
2989        let mut next = |n: u32| {
2990            state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
2991            ((state >> 33) as u32) % n
2992        };
2993        // Alphabet {a,b,c,space}; needles below have no proper prefix == suffix.
2994        let needles = ["ab", "ba", "abc", "cab"];
2995        for _ in 0..300 {
2996            let n = 12 + next(60); // text length
2997            let text: String = (0..n)
2998                .map(|_| [b'a', b'b', b'c', b' '][next(4) as usize] as char)
2999                .collect();
3000            let d = doc(&text);
3001            let needle = needles[next(needles.len() as u32) as usize];
3002            let nlen = needle.len() as u32;
3003            // A random subset of the actual occurrences becomes "taken".
3004            let all: Vec<(u32, u32)> = text
3005                .match_indices(needle)
3006                .map(|(i, _)| (i as u32, i as u32 + nlen))
3007                .collect();
3008            let taken: Vec<(u32, u32)> = all.iter().copied().filter(|_| next(2) == 0).collect();
3009            let is_taken = |s: u32| taken.iter().any(|&(st, en)| st == s && en == s + nlen);
3010            let from = next(n + 1);
3011            let got = scan_from(d.buffer(), needle, from, d.buffer().len(), &is_taken)
3012                .or_else(|| scan_from(d.buffer(), needle, 0, from, &is_taken));
3013            let want = find_next_occurrence(&text, needle, from, &taken);
3014            assert_eq!(got, want, "text {text:?} needle {needle:?} from {from} taken {taken:?}");
3015        }
3016    }
3017
3018    #[test]
3019    fn column_select_grows_widens_and_shrinks_back() {
3020        use crate::movement::ColumnDir::{Down, Left, Right, Up};
3021        // Three aligned rows; caret at (0, 1).
3022        let mut d = doc("abcd\nabcd\nabcd");
3023        d.set_selections(SelectionSet::new(1));
3024        // Down twice → a caret per row at column 1 (empty box, 3 rows).
3025        d.column_select(Down);
3026        d.column_select(Down);
3027        assert_eq!(d.selections().len(), 3);
3028        assert!(d.selections().all().iter().all(|s| s.is_empty() && s.start() % 5 == 1));
3029        // Right twice → each row selects columns 1..3.
3030        d.column_select(Right);
3031        d.column_select(Right);
3032        assert_eq!(d.selections().len(), 3);
3033        assert!(d.selections().all().iter().all(|s| s.end() - s.start() == 2));
3034        // Up twice → the box shrinks back to the anchor row only.
3035        d.column_select(Up);
3036        d.column_select(Up);
3037        assert_eq!(d.selections().len(), 1, "shrinks back toward the anchor");
3038        // Left twice → back to a bare caret at the anchor.
3039        d.column_select(Left);
3040        d.column_select(Left);
3041        assert_eq!(d.selections().len(), 1);
3042        assert!(d.selections().newest().is_empty());
3043    }
3044
3045    #[test]
3046    fn column_select_clamps_each_row_to_its_length() {
3047        use crate::movement::ColumnDir::{Down, Right};
3048        // A long row over a short one; a wide box clamps the short row.
3049        let mut d = doc("abcdef\nab"); // line 0 len 6, line 1 len 2
3050        d.set_selections(SelectionSet::new(0)); // caret at (0,0)
3051        d.column_select(Down); // box rows 0..1, col 0..0
3052        for _ in 0..4 {
3053            d.column_select(Right); // active col → 4
3054        }
3055        let rows = d.selections().all();
3056        assert_eq!(rows.len(), 2);
3057        assert_eq!((rows[0].start(), rows[0].end()), (0, 4)); // "abcd" on line 0
3058        // Line 1 ("ab", offsets 7..9) clamps to its end: 7..9.
3059        assert_eq!((rows[1].start(), rows[1].end()), (7, 9));
3060    }
3061
3062    #[test]
3063    fn column_drag_builds_a_box_from_two_corners() {
3064        let mut d = doc("abcd\nabcd\nabcd");
3065        d.column_drag((0, 1), (2, 3)); // 3 rows, cols 1..3
3066        assert_eq!(d.selections().len(), 3);
3067        assert!(d.selections().all().iter().all(|s| s.end() - s.start() == 2));
3068        // Virtual columns past a short line clamp per row.
3069        let mut r = doc("abcdef\nab"); // line0 len 6, line1 len 2
3070        r.column_drag((0, 0), (1, 5)); // active col 5 > line1 len
3071        let rows = r.selections().all();
3072        assert_eq!((rows[0].start(), rows[0].end()), (0, 5)); // line0 → 5
3073        assert_eq!((rows[1].start(), rows[1].end()), (7, 9)); // line1 clamps to its end
3074    }
3075
3076    #[test]
3077    fn column_box_is_display_cells_across_tabs() {
3078        // Row 0 leads with a tab (4 cells); row 1 is plain. A box over cells
3079        // 4..6 must select the same VISUAL slice — the two chars after the tab
3080        // — not reuse the cells as byte columns (which lands at row 0's EOL).
3081        let mut d = doc("\tabcd\nwxyz"); // row 1 starts at offset 6
3082        d.column_drag((0, 4), (1, 6));
3083        let rows = d.selections().all();
3084        assert_eq!(rows.len(), 2);
3085        assert_eq!((rows[0].start(), rows[0].end()), (1, 3), "cells 4..6 are bytes 1..3 after the tab");
3086        assert_eq!((rows[1].start(), rows[1].end()), (10, 10), "row 1 ends at cell 4; both corners clamp");
3087    }
3088
3089    #[test]
3090    fn column_box_resolves_through_a_collapsed_inline_fold() {
3091        // `f([a, b]) x` with the [..] pair collapsed: display cells right of
3092        // the chip sit one left of their byte columns. A box edge at cell 9
3093        // must land on `x` (byte 10), not the byte-9 space.
3094        let mut d = doc("f([a, b]) x");
3095        assert!(d.toggle_fold_opener(2)); // the `[`
3096        d.column_drag((0, 6), (0, 9));
3097        let s = d.selections().newest();
3098        assert_eq!((s.start(), s.end()), (7, 10), "cells 6..9 are `]) `, one left of the byte columns");
3099    }
3100
3101    #[test]
3102    fn column_box_spans_visible_rows_only() {
3103        // A box dragged across a collapsed block fold selects only what is on
3104        // screen: the rows hidden inside the fold get no selection.
3105        let mut d = doc("aa\nfn {\nbb\ncc\n}\ndd");
3106        assert!(d.toggle_fold_opener(6)); // the `{` — hides rows 2..=4
3107        d.column_drag((0, 0), (5, 1));
3108        let rows: Vec<u32> =
3109            d.selections().all().iter().map(|s| d.buffer().offset_to_point(s.start()).row).collect();
3110        assert_eq!(rows, vec![0, 1, 5], "one selection per VISIBLE row");
3111    }
3112
3113    #[test]
3114    fn column_select_steps_display_rows_over_a_fold() {
3115        use crate::movement::ColumnDir::Up;
3116        // Caret below a collapsed fold; growing the box upward hops the hidden
3117        // rows in ONE step — corners walk display rows, not buffer rows.
3118        let mut d = doc("aa\nfn {\nbb\ncc\n}\ndd");
3119        assert!(d.toggle_fold_opener(6));
3120        d.set_selections(SelectionSet::new(16)); // caret at (5, 0) — on "dd"
3121        d.column_select(Up);
3122        let rows: Vec<u32> =
3123            d.selections().all().iter().map(|s| d.buffer().offset_to_point(s.start()).row).collect();
3124        assert_eq!(rows, vec![1, 5], "the step lands on the fold header, skipping hidden rows");
3125    }
3126
3127    #[test]
3128    fn column_select_anchors_at_the_display_cell_after_a_tab() {
3129        use crate::movement::ColumnDir::Down;
3130        // A caret just after row 0's leading tab renders at cell 4; the box
3131        // column is that CELL, so on the plain row below it lands at byte 4
3132        // (visually aligned), not byte 1.
3133        let mut d = doc("\tab\nwwwwww"); // row 1 starts at offset 4
3134        d.set_selections(SelectionSet::new(1)); // (0, 1): just after the tab
3135        d.column_select(Down);
3136        assert_eq!(d.selections().newest().head(), 8, "cell 4 on row 1 is byte 4 (offset 8)");
3137    }
3138
3139    #[test]
3140    fn any_action_exits_column_mode() {
3141        use crate::movement::ColumnDir::Down;
3142        let mut d = doc("abcd\nabcd\nabcd");
3143        d.set_selections(SelectionSet::new(0));
3144        d.column_select(Down); // 2-row box
3145        assert_eq!(d.selections().len(), 2);
3146        d.move_carets(Motion::Right, false); // a plain move exits the mode…
3147        // …so the next column_select re-anchors from the current caret, not the
3148        // stale box (a fresh 2-row box, not a 3-row one).
3149        d.column_select(Down);
3150        assert_eq!(d.selections().len(), 2, "re-anchored, not continuing the old box");
3151    }
3152
3153    fn range(d: &Document) -> (u32, u32) {
3154        (d.selections().newest().start(), d.selections().newest().end())
3155    }
3156
3157    #[test]
3158    fn drag_select_word_granularity_keeps_the_origin_word() {
3159        let mut d = doc("foo bar baz\nqux"); // foo 0..3, bar 4..7, baz 8..11
3160        // Double-click "bar" (head == origin) selects just the word.
3161        d.drag_select(Granularity::Word, 5, 5);
3162        assert_eq!(range(&d), (4, 7));
3163        // Drag right into "baz": whole words, "bar".."baz".
3164        d.drag_select(Granularity::Word, 5, 9);
3165        assert_eq!(range(&d), (4, 11));
3166        // Drag back-left into "foo": origin word "bar" stays fully selected.
3167        d.drag_select(Granularity::Word, 5, 1);
3168        assert_eq!(range(&d), (0, 7));
3169        // A double-click surrounded by whitespace is a bare caret.
3170        let mut w = doc("a  b");
3171        w.drag_select(Granularity::Word, 2, 2);
3172        assert!(w.selections().newest().is_empty());
3173    }
3174
3175    #[test]
3176    fn drag_select_line_and_char_granularity() {
3177        let mut d = doc("aa\nbb\ncc"); // line0 0..2/\n2, line1 3..5/\n5, line2 6..8
3178        d.drag_select(Granularity::Line, 1, 1); // triple-click line 0 → incl \n
3179        assert_eq!(range(&d), (0, 3));
3180        d.drag_select(Granularity::Line, 1, 4); // drag into line 1
3181        assert_eq!(range(&d), (0, 6));
3182        d.drag_select(Granularity::Line, 7, 7); // last line → to its end
3183        assert_eq!(range(&d), (6, 8));
3184        d.drag_select(Granularity::Char, 2, 8); // char = a plain range
3185        assert_eq!(range(&d), (2, 8));
3186    }
3187
3188    fn head_row(d: &Document) -> u32 {
3189        d.buffer().offset_to_point(d.selections().newest().head()).row
3190    }
3191
3192    #[test]
3193    fn move_line_swaps_with_neighbour_and_rides() {
3194        let mut d = doc("aaa\nbbb\nccc");
3195        d.set_selections(SelectionSet::new(4)); // "bbb" (row 1)
3196        d.move_line(true); // down
3197        assert_eq!(d.text(), "aaa\nccc\nbbb");
3198        assert_eq!(head_row(&d), 2); // the caret rode down with the line
3199        d.move_line(false); // up → back
3200        assert_eq!(d.text(), "aaa\nbbb\nccc");
3201        assert_eq!(head_row(&d), 1);
3202    }
3203
3204    #[test]
3205    fn move_line_is_a_noop_at_the_edges() {
3206        let mut d = doc("aaa\nbbb");
3207        d.set_selections(SelectionSet::new(0));
3208        d.move_line(false); // up at the top
3209        assert_eq!(d.text(), "aaa\nbbb");
3210        d.set_selections(SelectionSet::new(4)); // last content line
3211        d.move_line(true); // down at the bottom
3212        assert_eq!(d.text(), "aaa\nbbb");
3213    }
3214
3215    #[test]
3216    fn move_line_down_respects_the_trailing_empty_line() {
3217        let mut d = doc("aaa\nbbb\n"); // trailing \n → an empty final row
3218        d.set_selections(SelectionSet::new(4)); // "bbb", the last *content* line
3219        d.move_line(true); // must not move into/past the empty final line
3220        assert_eq!(d.text(), "aaa\nbbb\n");
3221    }
3222
3223    #[test]
3224    fn copy_line_duplicates_below_and_above() {
3225        let mut d = doc("aaa\nbbb");
3226        d.set_selections(SelectionSet::new(0));
3227        d.copy_line(true); // down → caret on the lower copy
3228        assert_eq!(d.text(), "aaa\naaa\nbbb");
3229        assert_eq!(head_row(&d), 1);
3230        let mut d2 = doc("aaa\nbbb");
3231        d2.set_selections(SelectionSet::new(0));
3232        d2.copy_line(false); // up → caret on the upper copy
3233        assert_eq!(d2.text(), "aaa\naaa\nbbb");
3234        assert_eq!(head_row(&d2), 0);
3235    }
3236
3237    #[test]
3238    fn copy_line_duplicates_the_final_line() {
3239        let mut d = doc("aaa\nbbb"); // no trailing \n
3240        d.set_selections(SelectionSet::new(4)); // "bbb"
3241        d.copy_line(true);
3242        assert_eq!(d.text(), "aaa\nbbb\nbbb");
3243        assert_eq!(head_row(&d), 2);
3244    }
3245
3246    #[test]
3247    fn add_caret_adds_a_second_cursor() {
3248        let mut d = doc("abcdef");
3249        assert_eq!(d.selections().len(), 1);
3250        d.add_caret(3);
3251        assert_eq!(d.selections().len(), 2);
3252    }
3253
3254    #[test]
3255    fn collapse_returns_to_the_primary_caret() {
3256        let mut d = doc("foo foo foo");
3257        d.add_next_occurrence(); // select "foo"
3258        d.add_next_occurrence(); // add the next
3259        assert!(d.selections().len() >= 2);
3260        d.collapse_selections();
3261        assert_eq!(d.selections().len(), 1);
3262        assert!(d.selections().newest().is_empty());
3263    }
3264
3265    #[test]
3266    fn find_next_occurrence_scans_forward_then_wraps() {
3267        // "aXaYa": 'a' at 0, 2, 4. From 1, skipping the (0,1) already-taken one.
3268        assert_eq!(super::find_next_occurrence("aXaYa", "a", 1, &[(0, 1)]), Some((2, 3)));
3269        // From past the last match, wrap to the first untaken.
3270        assert_eq!(super::find_next_occurrence("aXaYa", "a", 5, &[(2, 3), (4, 5)]), Some((0, 1)));
3271        // Every occurrence taken → None.
3272        assert_eq!(super::find_next_occurrence("aa", "a", 0, &[(0, 1), (1, 2)]), None);
3273    }
3274
3275    #[test]
3276    fn dirty_after_undo_then_divergent_edit() {
3277        // Silent-data-loss guard: save, undo, retype something different → the
3278        // dirty flag must NOT read clean just because the stack depth matches.
3279        let mut d = doc("abc");
3280        d.edit(vec![EditOp::delete(2..3)]).unwrap(); // "ab"
3281        d.mark_saved();
3282        d.undo(); // "abc"
3283        assert!(d.is_dirty(), "undone away from the save point → dirty");
3284        d.edit(vec![EditOp::delete(0..1)]).unwrap(); // "bc" — diverges from "ab"
3285        assert!(d.is_dirty(), "divergent edit is still dirty, never falsely clean");
3286    }
3287
3288    #[test]
3289    fn highlight_cache_splice_tracks_line_count_across_edits() {
3290        // Tiny grammar/theme via the same app-injection path scratch uses.
3291        const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n  main:\n    - match: '\\w+'\n      scope: keyword.t\n";
3292        const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
3293<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3294<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
3295        let mut d = doc("ab\ncd");
3296        d.set_syntax(
3297            crate::SyntaxDef::from_sublime_syntax(G).unwrap(),
3298            crate::TokenTheme::from_tm_theme(TH).unwrap(),
3299        );
3300        d.tokenize_highlight(d.buffer().line_count());
3301        assert!(d.highlight_line_spans(1).is_some());
3302        // Enter inserts a line: the cache must grow (splice old=1 -> new=2).
3303        d.set_selections(SelectionSet::new(2)); // end of "ab"
3304        d.enter();
3305        d.tokenize_highlight(d.buffer().line_count());
3306        assert_eq!(d.buffer().line_count(), 3);
3307        assert!(d.highlight_line_spans(2).is_some(), "the new last line is tokenized");
3308        // Backspace merges it back: the cache must shrink (splice old=2 -> new=1).
3309        d.backspace();
3310        d.tokenize_highlight(d.buffer().line_count());
3311        assert_eq!(d.buffer().line_count(), 2);
3312        assert!(d.highlight_line_spans(1).is_some());
3313        assert!(d.highlight_line_spans(2).is_none(), "no spans past the buffer");
3314    }
3315
3316    #[test]
3317    fn multi_op_transaction_highlight_equals_a_fresh_document() {
3318        // A multi-caret transaction commits ALL its edits at once; rebase_views
3319        // must derive each edit's line span (old lines from the inverse text,
3320        // new lines from the post buffer) and coalesce same-line edits, then
3321        // invalidate exactly those lines. The swept highlight must equal a fresh
3322        // document on the identical text — a coordinate or coalescing error would
3323        // surface as a stale (un-re-tokenized) row here.
3324        const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n  main:\n    - match: '\\w+'\n      scope: keyword.t\n";
3325        const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
3326<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3327<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
3328        let make = |text: &str| -> Document {
3329            let mut d = doc(text);
3330            d.set_syntax(
3331                crate::SyntaxDef::from_sublime_syntax(G).unwrap(),
3332                crate::TokenTheme::from_tm_theme(TH).unwrap(),
3333            );
3334            d.tokenize_highlight(u32::MAX);
3335            d
3336        };
3337        // Lines: "aa bb cc"(0-7) \n(8) "dd ee ff"(9-16) \n(17) "gg hh ii"(18-25)
3338        //        \n(26) "jj kk ll"(27-34) \n(35) "mm nn oo"(36-43).
3339        let mut d = make("aa bb cc\ndd ee ff\ngg hh ii\njj kk ll\nmm nn oo");
3340        // One transaction: an edit on line 0, TWO edits on line 1 (must coalesce
3341        // to one line span), and a newline-bearing insert (a +1 line delta) —
3342        // all disjoint and ascending.
3343        d.edit_grouped(
3344            vec![
3345                EditOp::new(0..2, "XYZ"),     // line 0
3346                EditOp::new(9..11, "Q"),      // line 1, col 0
3347                EditOp::new(15..17, "RR"),    // line 1, col 6 (same line ⇒ coalesce)
3348                EditOp::insert(27, "NEW\n"),  // start of line 3 ⇒ inserts a line
3349            ],
3350            typ(OpClass::Type),
3351        )
3352        .unwrap();
3353        d.tokenize_highlight(u32::MAX);
3354
3355        let text = d.text();
3356        let f = make(&text);
3357        assert_eq!(d.buffer().line_count(), f.buffer().line_count(), "line count");
3358        for r in 0..d.buffer().line_count() {
3359            assert_eq!(
3360                d.highlight_line_spans(r),
3361                f.highlight_line_spans(r),
3362                "row {r} live (per-edit invalidation) vs fresh"
3363            );
3364        }
3365    }
3366
3367    /// A mid-session grammar swap must keep the retention window aimed at the
3368    /// viewport: the widget's deduped viewport report never re-fires when
3369    /// nothing visible moved, so the swapped cache must retain the currently
3370    /// visible rows rather than reset to the top and leave them fallback-styled.
3371    #[test]
3372    fn set_syntax_preserves_the_highlight_window_aim() {
3373        const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n  main:\n    - match: '\\w+'\n      scope: keyword.t\n";
3374        const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
3375<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3376<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
3377        let syntax = || crate::SyntaxDef::from_sublime_syntax(G).unwrap();
3378        let theme = || crate::TokenTheme::from_tm_theme(TH).unwrap();
3379        let mut d = doc(&"word\n".repeat(3_000));
3380        d.set_syntax(syntax(), theme());
3381        // The app scrolled deep: the window is aimed well past the default.
3382        d.set_highlight_window(2_000..2_040);
3383        // Grammar swap mid-session (re-highlight when the host swaps the language).
3384        d.set_syntax(syntax(), theme());
3385        while d.highlight_frontier().is_some() {
3386            d.tokenize_highlight(u32::MAX);
3387        }
3388        assert!(
3389            d.highlight_line_spans(2_020).is_some(),
3390            "the swapped cache must still retain the viewport rows"
3391        );
3392    }
3393
3394    // Off-thread parallel/speculative highlight ingest. A stateless grammar
3395    // suffices at the Document seam — the stitch correctness lives in
3396    // highlight.rs's stateful oracle; here we check the seam's contract:
3397    // revision gating, dirt clearing, and no-checkpoint speculation.
3398    const HL_G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n  main:\n    - match: '\\bkw\\b'\n      scope: keyword.t\n";
3399    const HL_TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
3400<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3401<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
3402
3403    fn doc_with_syntax(n: usize) -> Document {
3404        let mut d = doc(&"kw word\n".repeat(n));
3405        d.set_syntax(
3406            crate::SyntaxDef::from_sublime_syntax(HL_G).unwrap(),
3407            crate::TokenTheme::from_tm_theme(HL_TH).unwrap(),
3408        );
3409        d
3410    }
3411
3412    #[test]
3413    fn absorb_highlight_drops_a_stale_revision() {
3414        let mut d = doc_with_syntax(2_000);
3415        let engine = d.highlight_engine().unwrap();
3416        let snap = d.snapshot();
3417        let stale_rev = d.revision();
3418        let seg = crate::tokenize_segment(&engine, &snap, 0..2_001, crate::SegmentStart::Fresh, None, None);
3419        // An edit lands after the snapshot was taken…
3420        d.set_selections(SelectionSet::new(0));
3421        d.type_char('x');
3422        assert_ne!(d.revision(), stale_rev);
3423        // …so the in-flight segment is dropped, absorbing nothing.
3424        assert!(!d.absorb_highlight(stale_rev, seg, true), "stale segment must be dropped");
3425        assert_eq!(d.highlight_frontier(), Some(0), "the frontier is untouched by a dropped absorb");
3426    }
3427
3428    #[test]
3429    fn absorb_highlight_verified_clears_the_frontier() {
3430        let mut d = doc_with_syntax(5_000);
3431        let engine = d.highlight_engine().unwrap();
3432        let snap = d.snapshot();
3433        let rev = d.revision();
3434        let n = d.buffer().line_count();
3435        // A single verified segment over the whole document, carrying spans for
3436        // the whole range so no window gap remains either — then the frontier
3437        // (dirt ∨ window gap) is fully quiet, proving dirt was cleared.
3438        let seg =
3439            crate::tokenize_segment(&engine, &snap, 0..n, crate::SegmentStart::Fresh, Some(0..n), None);
3440        assert!(d.absorb_highlight(rev, seg, true));
3441        assert_eq!(d.highlight_frontier(), None, "verified absorb clears every dirty row");
3442    }
3443
3444    #[test]
3445    fn absorb_highlight_speculative_shows_spans_but_keeps_dirt() {
3446        let mut d = doc_with_syntax(2_000);
3447        d.set_highlight_window(900..940);
3448        let engine = d.highlight_engine().unwrap();
3449        let snap = d.snapshot();
3450        let rev = d.revision();
3451        // A viewport-first speculative segment (Fresh guess, spans for the win).
3452        let seg = crate::tokenize_segment(
3453            &engine,
3454            &snap,
3455            772..940,
3456            crate::SegmentStart::Fresh,
3457            Some(900..940),
3458            None,
3459        );
3460        assert!(d.absorb_highlight(rev, seg, false));
3461        // Spans render immediately…
3462        assert!(d.highlight_line_spans(920).is_some(), "speculative spans are visible");
3463        // …but nothing is verified: the frontier still starts at row 0.
3464        assert_eq!(d.highlight_frontier(), Some(0), "speculation keeps the frontier");
3465        // Driving the sync frontier still converges to the correct colors.
3466        while d.highlight_frontier().is_some() {
3467            d.tokenize_highlight(u32::MAX);
3468        }
3469        assert!(d.highlight_line_spans(920).is_some());
3470    }
3471
3472    #[test]
3473    fn diagnostics_install_then_ride_a_forward_edit() {
3474        use crate::{Diagnostic, DiagnosticsOutcome, Severity};
3475        let mut d = doc("let x = 1;");
3476        let rev = d.revision();
3477        let out = d.set_diagnostics(rev, vec![Diagnostic::new(4..5, Severity::Error, "unused")]);
3478        assert!(matches!(out, DiagnosticsOutcome::Applied { count: 1 }));
3479        let got: Vec<_> = d.diagnostics_in(0..100).collect();
3480        assert_eq!((got.len(), got[0].0.clone(), got[0].1), (1, 4..5, Severity::Error));
3481        // Insert 4 chars at the top → the squiggle (NeverGrows) rides to 8..9.
3482        d.edit(vec![EditOp::insert(0, "abcd")]).unwrap();
3483        assert_eq!(d.diagnostics_in(0..100).next().unwrap().0, 8..9, "rode via apply_patch");
3484    }
3485
3486    #[test]
3487    fn stale_diagnostic_set_is_dropped_and_the_prior_kept() {
3488        use crate::{Diagnostic, DiagnosticsOutcome, Severity};
3489        let mut d = doc("abcdef");
3490        let rev = d.revision();
3491        d.set_diagnostics(rev, vec![Diagnostic::new(0..1, Severity::Warning, "w")]);
3492        d.edit(vec![EditOp::insert(0, "z")]).unwrap(); // revision advances
3493        let out = d.set_diagnostics(rev, vec![Diagnostic::new(3..4, Severity::Error, "e")]);
3494        assert!(matches!(out, DiagnosticsOutcome::Stale { .. }), "old-revision set dropped");
3495        let got: Vec<_> = d.diagnostics_in(0..100).collect();
3496        assert_eq!((got.len(), got[0].0.clone(), got[0].1), (1, 1..2, Severity::Warning));
3497    }
3498
3499    #[test]
3500    fn diagnostic_spans_clip_to_the_buffer_length() {
3501        use crate::{Diagnostic, Severity};
3502        let mut d = doc("abc"); // len 3
3503        let rev = d.revision();
3504        d.set_diagnostics(rev, vec![Diagnostic::new(2..99, Severity::Error, "past eof")]);
3505        assert_eq!(d.diagnostics_in(0..100).next().unwrap().0, 2..3, "clipped to buffer len");
3506    }
3507
3508    #[test]
3509    fn rend04_diagnostic_render_row_shifts_when_lines_inserted_above() {
3510        // A diagnostic deep in the document rides the code as whole lines are
3511        // inserted above it — its RENDER row (offset → point) shifts by the
3512        // inserted line count, still glued to the same text.
3513        use crate::{Diagnostic, Point, Severity};
3514        let mut d = doc("L0\nL1\nL2\nL3\nL4\nL5\nL6\nL7\nL8\nL9");
3515        let row5 = d.buffer().point_to_offset(Point { row: 5, col: 0 });
3516        d.set_diagnostics(d.revision(), vec![Diagnostic::new(row5..row5 + 2, Severity::Error, "e")]);
3517        let start = d.diagnostics_in(0..u32::MAX).next().unwrap().0.start;
3518        assert_eq!(d.buffer().offset_to_point(start).row, 5);
3519        // Insert three lines at the very top.
3520        d.edit(vec![EditOp::insert(0, "a\nb\nc\n")]).unwrap();
3521        let span = d.diagnostics_in(0..u32::MAX).next().unwrap().0;
3522        assert_eq!(d.buffer().offset_to_point(span.start).row, 8, "5 + 3 inserted lines");
3523        assert_eq!(&d.text()[span.start as usize..span.end as usize], "L5", "still glued to its code");
3524    }
3525
3526    /// The windowed fold queries (the fold-gutter mouse hot paths) must agree
3527    /// with the whole-document ones on every row / window — else a hover/click
3528    /// near a fold would mis-decide. A windowed query that drops a headed pair or
3529    /// mis-clamps the byte range diverges here. Uses nested + inline + adjacent
3530    /// blocks so the partition-point windowing is exercised at boundaries.
3531    #[test]
3532    fn windowed_fold_queries_match_the_whole_document() {
3533        let mut d = doc(
3534            "top\n\
3535             a {\n  b [1, 2]\n  c {\n    d\n  }\n}\n\
3536             mid\n\
3537             e (x,\n  y)\n\
3538             f {\n  g\n}\n\
3539             end",
3540        );
3541        let n = d.buffer().line_count();
3542        // block_opener_on_row(r) windowed == the whole-document oracle.
3543        for row in 0..n {
3544            let want = d
3545                .foldable_pairs()
3546                .into_iter()
3547                .filter(|&(_, _, h, l)| h == row && l > h)
3548                .max_by_key(|&(_, _, h, l)| l - h)
3549                .map(|(o, ..)| o);
3550            assert_eq!(d.block_opener_on_row(row), want, "block_opener_on_row row {row}");
3551        }
3552        // collapsible_pairs_in_rows(0..n) == collapsible_pairs() (whole doc).
3553        assert_eq!(d.collapsible_pairs_in_rows(0..n), d.collapsible_pairs());
3554        // A sub-window returns exactly the whole-doc pairs headed inside it.
3555        for (a, b) in [(0u32, 3u32), (2, 6), (3, 4), (7, n)] {
3556            let want: Vec<_> =
3557                d.collapsible_pairs().into_iter().filter(|&(_, _, h, _)| a <= h && h < b).collect();
3558            assert_eq!(d.collapsible_pairs_in_rows(a..b), want, "collapsible_pairs_in_rows {a}..{b}");
3559        }
3560        // foldable_ranges_in_rows(r..r+1) headers agree with foldable_ranges().
3561        for row in 0..n {
3562            let windowed: Vec<_> =
3563                d.foldable_ranges_in_rows(row..row + 1).into_iter().filter(|(h, _)| h.0 == row).collect();
3564            let whole: Vec<_> =
3565                d.foldable_ranges().into_iter().filter(|(h, _)| h.0 == row).collect();
3566            assert_eq!(windowed, whole, "foldable_ranges row {row}");
3567        }
3568        // And it still holds after a fold collapses (folded pairs drop out).
3569        let opener = d.block_opener_on_row(1).unwrap();
3570        assert!(d.toggle_fold_opener(opener));
3571        assert_eq!(d.collapsible_pairs_in_rows(0..n), d.collapsible_pairs());
3572    }
3573
3574    #[test]
3575    fn fold_survives_undo_redo() {
3576        // A fold rides the commit patch exactly like a decoration: an edit
3577        // above shifts it, undo restores its original rows, redo re-shifts — with
3578        // no fold-specific undo handling. The block is bracketed so it stays a
3579        // valid foldable range across the reconcile that runs on every edit.
3580        use crate::{fold_map::FoldMap, BufferRow};
3581        let mut d = doc("L0\nL1\nL2\nL3\nL4\nblk {\n  x\n  y\n}\nL9");
3582        assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(5, 8)]);
3583        assert!(d.fold(BufferRow(5), BufferRow(8)));
3584        let header = |d: &Document| FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(5));
3585        let header8 = |d: &Document| FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(8));
3586        assert_eq!(header(&d), Some(BufferRow(8)));
3587        // Insert three lines at the top → fold shifts to header 8, last 11.
3588        d.edit(vec![EditOp::insert(0, "a\nb\nc\n")]).unwrap();
3589        assert_eq!(header8(&d), Some(BufferRow(11)));
3590        // Undo → fold back at 5..8. Redo → shifted again.
3591        assert!(d.undo());
3592        assert_eq!(header(&d), Some(BufferRow(8)));
3593        assert!(d.redo());
3594        assert_eq!(header8(&d), Some(BufferRow(11)));
3595    }
3596
3597    #[test]
3598    fn pasting_after_the_collapsed_line_keeps_the_outer_fold() {
3599        use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
3600        // Outer block (0,3) around a nested inner block (1,2).
3601        let mut d = doc("{\n\t{\n\t}\n}\n");
3602        assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(0, 3), (1, 2)]);
3603        assert!(d.fold(BufferRow(0), BufferRow(3))); // fold the outer bracket
3604        // Copy the whole collapsed block `{…}` (offsets [0,9)) and paste it at the
3605        // end of the collapsed line (offset 9, just after the `}` on row 3).
3606        let block = d.buffer().text()[0..9].to_string();
3607        d.set_selections(SelectionSet::new(9));
3608        d.insert_text(&block);
3609        // The outer block's `{`…`}` is still on rows 0..3, so the fold must STAY on
3610        // (0,3) — not drop, and not grow onto a bogus range.
3611        assert_eq!(d.folds().len(), 1, "the outer fold survives the paste");
3612        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(3)));
3613    }
3614
3615    #[test]
3616    fn editing_after_the_brace_keeps_a_folded_block() {
3617        use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
3618        let mut d = doc("{\n}");
3619        assert!(d.fold(BufferRow(0), BufferRow(1)));
3620        // Type arbitrary text after the `}` — the `{`…`}` block is intact.
3621        d.set_selections(SelectionSet::new(d.buffer().len()));
3622        d.insert_text("abc");
3623        assert_eq!(d.buffer().text(), "{\n}abc");
3624        assert_eq!(d.folds().len(), 1, "fold survives typing after the brace");
3625        // …and delete it back.
3626        let end = d.buffer().len();
3627        d.edit(vec![EditOp::delete(end - 3..end)]).unwrap();
3628        assert_eq!(d.buffer().text(), "{\n}");
3629        assert_eq!(d.folds().len(), 1, "fold survives deleting after the brace");
3630        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(1)));
3631    }
3632
3633    #[test]
3634    fn pressing_return_after_a_folded_eof_block_keeps_it_folded() {
3635        use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
3636        // A fold created at EOF: `}` is the last line, so its interior has no
3637        // trailing `\n`.
3638        let mut d = doc("{\n}");
3639        assert!(d.fold(BufferRow(0), BufferRow(1)));
3640        // Caret at the very end (after `}`); press Enter (insert `\n`). The block
3641        // is untouched — a blank line is added after it — so the fold must STAY.
3642        d.set_selections(SelectionSet::new(d.buffer().len()));
3643        d.insert_text("\n");
3644        assert_eq!(d.buffer().text(), "{\n}\n");
3645        assert_eq!(d.folds().len(), 1, "the intact block stays folded");
3646        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(1)));
3647    }
3648
3649    #[test]
3650    fn cutting_trailing_content_keeps_an_intact_fold() {
3651        use crate::{fold_map::FoldMap, BufferRow};
3652        // rows: 0 "{" 1 "}" 2 "X"
3653        let mut d = doc("{\n}\nX");
3654        assert!(d.fold(BufferRow(0), BufferRow(1))); // fold the set
3655        // Select from after X (offset 5) to the end of the `}` (offset 3) and cut:
3656        // deletes the `}` line's trailing `\n` + `X`. The `{`…`}` block is intact,
3657        // so the fold must STAY.
3658        let a = d.buffer().point_to_offset(crate::Point::new(1, 1)); // 3
3659        let b = d.buffer().point_to_offset(crate::Point::new(2, 1)); // 5
3660        d.edit(vec![EditOp::delete(a..b)]).unwrap();
3661        assert_eq!(d.buffer().text(), "{\n}", "only the trailing content was removed");
3662        assert_eq!(d.folds().len(), 1, "the intact block stays folded");
3663        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), Some(BufferRow(1)));
3664    }
3665
3666    #[test]
3667    fn cutting_across_a_folds_boundary_drops_it_not_drifts_it() {
3668        use crate::{fold_map::FoldMap, BufferRow};
3669        // Three sibling blocks. rows: 0 "{" 1 "}" 2 "{" 3 "}" 4 "{" 5 "}" 6 ""
3670        let mut d = doc("{\n}\n{\n}\n{\n}\n");
3671        assert!(d.fold(BufferRow(2), BufferRow(3))); // fold the MIDDLE set
3672        // Select from the end of the first `}` (offset 3) to the end of the middle
3673        // `}` (offset 7) and cut it — a delete that crosses the fold's interior
3674        // start. The whole middle block is removed.
3675        let a = d.buffer().point_to_offset(crate::Point::new(1, 1)); // 3
3676        let b = d.buffer().point_to_offset(crate::Point::new(3, 1)); // 7
3677        d.edit(vec![EditOp::delete(a..b)]).unwrap();
3678        // The fold must DROP — not drift onto the first set (which is itself a
3679        // valid foldable range, so reconcile can't catch the drift).
3680        assert!(d.folds().is_empty(), "fold dropped, not drifted onto the first block");
3681        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).fold_at_header(BufferRow(0)), None, "first set not folded");
3682    }
3683
3684    #[test]
3685    fn typing_at_many_carets_over_folds_stays_linear() {
3686        use crate::bracket::ENCLOSING_WALKS;
3687        use crate::row_layout::DISPLAY_POSITION_PROBES;
3688        use crate::{Motion, SelectionSet};
3689        // Select every `fn pid` occurrence, fold each block, then type.
3690        // `expand_folds_touched` is windowed, so per commit it does ONE enclosing
3691        // walk (for the first edit point) and O(edit points) display-position
3692        // probes, independent of caret count — never O(carets) leftward enclosing
3693        // walks plus an O(carets²) per-candidate edit scan. Gates both: an
3694        // un-windowed implementation trips these by a factor of N.
3695        let mut src = String::new();
3696        for i in 0..400 {
3697            src.push_str(&format!(
3698                "// block {i}\nfn pid_{i}(pid: u8) -> u8 {{\n    switch pid {{\n        0x0D => {{ return 88; }}\n    }}\n}}\n\n"
3699            ));
3700        }
3701        let mut d = doc(&src);
3702        let first = d.buffer().text().find("fn pid").unwrap() as u32;
3703        d.set_selections(SelectionSet::from_ranges(&[(first, first + 6)], 0));
3704        d.select_all_occurrences();
3705        let carets = d.selections().all().len();
3706        assert!(carets >= 400, "select-all found every block ({carets} carets)");
3707        d.move_carets(Motion::LineEnd, false);
3708        d.fold_at_carets(false);
3709        d.move_carets(Motion::LineEnd, false);
3710
3711        // Measure ONLY the commit for the typed character.
3712        ENCLOSING_WALKS.with(|c| c.set(0));
3713        DISPLAY_POSITION_PROBES.with(|c| c.set(0));
3714        d.insert_text("a");
3715        let walks = ENCLOSING_WALKS.with(std::cell::Cell::get);
3716        let probes = DISPLAY_POSITION_PROBES.with(std::cell::Cell::get);
3717        assert!(walks <= 2, "one enclosing walk per commit, not O(carets): {walks} for {carets} carets");
3718        assert!(
3719            probes <= 4 * carets as u64,
3720            "display-position probes are O(edit points), not O(carets²): {probes} for {carets} carets"
3721        );
3722    }
3723
3724    #[test]
3725    fn folded_keystroke_never_resolves_bracket_depth() {
3726        use crate::bracket_tree::BRACKET_VIEW_CALLS;
3727        use crate::SelectionSet;
3728        // Fold every function block, then type. The edit-path fold queries —
3729        // expand-on-hidden-edit, reconcile, the FoldMap rebuild — must resolve
3730        // each fold's partner via the depth-free `foldable_partner`, NEVER
3731        // `bracket_view`, whose per-opener prefix-stack Vec (allocated at every
3732        // tree level) would turn a fold-heavy keystroke into an O(folds · depth)
3733        // allocation storm. `bracket_view` is a DRAW-path (colorization)
3734        // primitive only; a single fold-edit commit must leave the
3735        // depth-resolution canary at zero regardless of how many folds are open.
3736        // Resolving partners through `at().partner` → `bracket_view` would read
3737        // ~2·folds; the depth-free path pins it to zero.
3738        let mut src = String::new();
3739        for i in 0..300 {
3740            src.push_str(&format!("fn f_{i}() {{\n    body\n}}\n"));
3741        }
3742        let mut d = doc(&src);
3743        // The block `{` of each function (the `()` params are single-line, not folds).
3744        let openers: Vec<u32> = d
3745            .brackets()
3746            .all()
3747            .iter()
3748            .filter(|b| b.open && d.buffer().char_at(b.offset) == Some('{'))
3749            .map(|b| b.offset)
3750            .collect();
3751        assert_eq!(openers.len(), 300, "one foldable block opener each");
3752        for &o in &openers {
3753            assert!(d.toggle_fold_opener(o));
3754        }
3755        // A caret just after each (visible) header `{` — the "hit END, type" position.
3756        let carets: Vec<(u32, u32)> = openers.iter().map(|&o| (o + 1, o + 1)).collect();
3757        d.set_selections(SelectionSet::from_ranges(&carets, 0));
3758        let _ = d.fold_map(); // warm the map so the measured commit is steady-state
3759
3760        BRACKET_VIEW_CALLS.with(|c| c.set(0));
3761        d.insert_text("a");
3762        let _ = d.fold_map(); // the per-keystroke refresh rides the same lookups
3763        let depth_resolves = BRACKET_VIEW_CALLS.with(std::cell::Cell::get);
3764        assert_eq!(
3765            depth_resolves, 0,
3766            "a fold-heavy keystroke resolved bracket depth {depth_resolves} times over 300 folds \
3767             — the edit path must use foldable_partner, not at()/bracket_view"
3768        );
3769    }
3770
3771    #[test]
3772    fn single_line_pair_folds_inline() {
3773        use crate::{fold_map::FoldMap, SelectionSet};
3774        // one line: `[` at 4, `]` at 12.
3775        let mut d = doc("x = [1, 2, 3]");
3776        d.set_selections(SelectionSet::new(6)); // caret inside the array
3777        let opener = d.fold_opener_at_caret(false).expect("an enclosing foldable pair");
3778        assert_eq!(opener, 4);
3779        assert!(d.toggle_fold_opener(opener));
3780        let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
3781        assert_eq!(m.display_row_count(), 1, "an inline fold hides no rows");
3782        assert_eq!(m.inline_folds().len(), 1);
3783        assert_eq!((m.inline_folds()[0].row, m.inline_folds()[0].open, m.inline_folds()[0].close), (0, 4, 12));
3784        // Unfold via the same opener.
3785        assert!(d.toggle_fold_opener(4));
3786        assert!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).inline_folds().is_empty());
3787    }
3788
3789    #[test]
3790    fn fold_at_carets_acts_on_every_caret() {
3791        use crate::{fold_map::FoldMap, SelectionSet};
3792        // Two inline blocks: {x} at 1..3 and {y} at 6..8.
3793        let mut d = doc("a{x}\nb{y}");
3794        d.set_selections(SelectionSet::from_offsets(&[2, 7])); // a caret inside each
3795        assert!(d.fold_at_carets(false), "Ctrl+Shift+[ folds at every caret");
3796        let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
3797        assert_eq!(m.inline_folds().len(), 2, "both blocks collapsed, not just the primary");
3798        // …and unfold at every caret.
3799        assert!(d.fold_at_carets(true), "Ctrl+Shift+] unfolds at every caret");
3800        assert!(
3801            FoldMap::new(d.folds(), d.brackets(), d.buffer()).inline_folds().is_empty(),
3802            "both blocks expanded",
3803        );
3804    }
3805
3806    #[test]
3807    fn fold_map_cache_matches_a_fresh_build_across_changes() {
3808        // The memoized fold_map() must DEEP-EQUAL a from-scratch build. It is
3809        // shifted in place on a no-line-change edit, so the drift risk is an
3810        // inline fold's offsets diverging from a fresh resolve — this exercises
3811        // both a block and an inline fold across folds, plain typing (in-place
3812        // shift, cumulative), and a newline (line change → rebuild fallback).
3813        fn consistent(d: &Document) -> bool {
3814            *d.fold_map() == crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer())
3815        }
3816        let mut d = doc("a {\nx\n}\nb = [1, 2, 3]\nc\n");
3817        assert!(consistent(&d), "empty folds");
3818        // Fold a block AND an inline pair (the inline offsets are the shift risk).
3819        let block = d.text().find('{').unwrap() as u32;
3820        let inline = d.text().find('[').unwrap() as u32;
3821        d.toggle_fold_opener(block);
3822        d.toggle_fold_opener(inline);
3823        assert!(consistent(&d), "after folds — rebuilt on the generation");
3824        // A plain char at offset 0 (visible, before both folds) shifts the inline
3825        // fold's offsets; the cache is shifted in place, not rebuilt.
3826        d.edit(vec![EditOp::insert(0, "Z")]).unwrap();
3827        assert!(consistent(&d), "after a no-line-change edit — shifted in place");
3828        d.edit(vec![EditOp::insert(0, "Y")]).unwrap();
3829        assert!(consistent(&d), "after a second in-place shift (cumulative)");
3830        // A newline changes the line count → the cache falls back to a rebuild.
3831        d.edit(vec![EditOp::insert(0, "\n")]).unwrap();
3832        assert!(consistent(&d), "after a line change — rebuilt");
3833        d.edit(vec![EditOp::insert(0, "W")]).unwrap();
3834        assert!(consistent(&d), "after an edit following a rebuild");
3835    }
3836
3837    #[test]
3838    fn deleting_into_a_folded_interior_rebuilds_not_drifts() {
3839        // A deletion whose OLD range reached a folded block's hidden interior
3840        // (the newline ending the header line) is NOT caught by `expand` (which
3841        // probes only the collapsed new endpoint, landing on a visible row), so
3842        // the incremental mover must detect the span change and rebuild rather
3843        // than rigidly shift — a rigid shift would diverge from a fresh build, and
3844        // underflow `vgap` for a fold headed on row 0.
3845        let consistent = |d: &Document| {
3846            *d.fold_map() == crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer())
3847        };
3848        for prefix in ["xx\n", ""] {
3849            // With "xx\n" the fold heads on row 1; with "" it heads on row 0 (vgap 0,
3850            // the underflow variant).
3851            let mut d = doc(&format!("{prefix}fn foo() {{\n  body\n}}\nafter\n"));
3852            let open = d.text().find('{').unwrap() as u32;
3853            d.toggle_fold_opener(open);
3854            let _ = d.fold_map();
3855            assert!(consistent(&d), "{prefix:?}: folded");
3856            let nl = open + d.text()[open as usize..].find('\n').unwrap() as u32;
3857            d.edit(vec![EditOp::delete(nl..nl + 1)]).unwrap(); // delete the header newline
3858            assert!(consistent(&d), "{prefix:?}: after deleting the header-terminating newline");
3859        }
3860    }
3861
3862    #[test]
3863    fn fold_map_cache_matches_fresh_build_under_random_edits() {
3864        // The drift oracle as a random walk: the incrementally-shifted fold cache
3865        // must DEEP-EQUAL a from-scratch build after EVERY edit — line inserts and
3866        // deletes at arbitrary offsets (the new O(log) block-region reanchor + the
3867        // inline row/offset shift), multi-line inserts, and edits that land in a
3868        // fold interior (expand → generation rebuild). All paths must stay consistent.
3869        let consistent = |d: &Document| {
3870            *d.fold_map() == crate::fold_map::FoldMap::new(d.folds(), d.brackets(), d.buffer())
3871        };
3872        let mut state = 0xF01D_5EEDu32;
3873        let mut next = || {
3874            state ^= state << 13;
3875            state ^= state >> 17;
3876            state ^= state << 5;
3877            state
3878        };
3879        let mut d = doc(&"fn f() {\n  x = [1, 2]\n  y\n}\n".repeat(6));
3880        for open in d.collapsible_pairs().iter().map(|&(o, ..)| o).collect::<Vec<_>>() {
3881            d.toggle_fold_opener(open);
3882        }
3883        assert!(consistent(&d), "after collapse-all");
3884        for step in 0..1500 {
3885            let len = d.buffer().len();
3886            if len == 0 {
3887                break;
3888            }
3889            let at = next() % (len + 1);
3890            let _ = match next() % 6 {
3891                0 => d.edit(vec![EditOp::insert(at, "\n")]),       // single-line insert
3892                1 => d.edit(vec![EditOp::insert(at, "z")]),        // plain char, no line change
3893                2 => d.edit(vec![EditOp::insert(at, "ab\ncd")]),   // multi-line insert
3894                3 => d.edit(vec![EditOp::insert(at, "  ")]),       // whitespace, no line change
3895                _ if at < len => {
3896                    let end = (at + 1 + next() % 5).min(len); // delete (may cross a newline)
3897                    d.edit(vec![EditOp::delete(at..end)])
3898                }
3899                _ => d.edit(vec![EditOp::insert(at, "q")]),
3900            };
3901            assert!(consistent(&d), "step {step}: cache drifted from a fresh build (at {at})");
3902        }
3903    }
3904
3905    #[test]
3906    fn keystroke_and_arrow_do_not_rebuild_the_fold_map_at_scale() {
3907        // The per-keystroke fold GATE (the analog of the widget draw-budget gate):
3908        // with a document-scale set collapsed, a plain typed character and an
3909        // arrow key must NOT rebuild the whole FoldMap (O(folds)) — the cache is
3910        // shifted in place / read, never rebuilt. A per-keystroke `FoldMap::new`
3911        // (a common source of typing lag) would trip this.
3912        use crate::fold_map::FOLD_BUILDS;
3913        let mut text = String::new();
3914        for i in 0..300 {
3915            text.push_str(&format!("fn f{i}() {{\n    body\n}}\n"));
3916        }
3917        let mut d = doc(&text);
3918        let opens: Vec<u32> = d.collapsible_pairs().into_iter().map(|(o, ..)| o).collect();
3919        assert!(opens.len() >= 300, "every block is foldable");
3920        d.set_selections(SelectionSet::from_offsets(&opens));
3921        d.fold_at_carets(false); // collapse everything
3922        d.set_selections(SelectionSet::new(0)); // caret at the visible top
3923        let _ = d.fold_map(); // warm the cache
3924
3925        // A plain keystroke at a visible position, then a render-style map read.
3926        let base = FOLD_BUILDS.with(std::cell::Cell::get);
3927        d.type_char('x');
3928        let _ = d.fold_map();
3929        assert_eq!(
3930            FOLD_BUILDS.with(std::cell::Cell::get) - base,
3931            0,
3932            "a keystroke over {} folds must not rebuild the FoldMap",
3933            opens.len()
3934        );
3935
3936        // An arrow key (movement reads the memoized map, not a throwaway build).
3937        let base = FOLD_BUILDS.with(std::cell::Cell::get);
3938        d.move_carets(Motion::Down, false);
3939        let _ = d.fold_map();
3940        assert_eq!(
3941            FOLD_BUILDS.with(std::cell::Cell::get) - base,
3942            0,
3943            "an arrow key over {} folds must not rebuild the FoldMap",
3944            opens.len()
3945        );
3946
3947        // Adding a NEWLINE at the start changes the line count — the
3948        // "insert lines at the top with everything folded" case. The incremental
3949        // region-tree reanchor shifts every fold's rows in O(log) at one seam, so
3950        // it must NOT rebuild. A line-change fallback that rebuilt the whole
3951        // O(folds) FoldMap every keystroke would trip this.
3952        d.set_selections(SelectionSet::new(0));
3953        let base = FOLD_BUILDS.with(std::cell::Cell::get);
3954        d.edit(vec![EditOp::insert(0, "\n")]).unwrap();
3955        let _ = d.fold_map();
3956        assert_eq!(
3957            FOLD_BUILDS.with(std::cell::Cell::get) - base,
3958            0,
3959            "a newline at the top over {} folds must not rebuild the FoldMap",
3960            opens.len()
3961        );
3962    }
3963
3964    #[test]
3965    fn keystroke_with_document_scale_decorations_does_not_resort_the_store() {
3966        // The per-keystroke DECORATION gate: typing with a document-scale
3967        // decoration set present must NOT re-sort the whole store — apply_patch
3968        // shifts in place (a monotonic patch keeps order) and the autoclose scans
3969        // hit only the (here empty) own auto-close store. An O(D log D) full-store
3970        // sort every commit would trip this.
3971        use crate::decorations::DECORATION_SORTS;
3972        use crate::{Diagnostic, Severity};
3973        let mut d = doc(&"let x = 1;\n".repeat(2000)); // 22k bytes
3974        let rev = d.revision();
3975        let diags: Vec<Diagnostic> = (0..2000u32)
3976            .map(|i| Diagnostic::new(i * 10..i * 10 + 3, Severity::Warning, "w"))
3977            .collect();
3978        d.set_diagnostics(rev, diags); // publishes (sorts once, here — not per keystroke)
3979        d.set_selections(SelectionSet::new(0));
3980
3981        let base = DECORATION_SORTS.with(std::cell::Cell::get);
3982        d.type_char('z');
3983        assert_eq!(
3984            DECORATION_SORTS.with(std::cell::Cell::get) - base,
3985            0,
3986            "a keystroke with 2000 diagnostics must not re-sort the decoration store"
3987        );
3988    }
3989
3990    #[test]
3991    fn reconcile_scans_the_fold_set_only_when_a_fold_actually_breaks() {
3992        // reconcile drops folds whose pair an edit broke. Two tightenings are gated
3993        // here. (1) A non-bracket edit — a letter, or an Enter (a line change with
3994        // no bracket char) — leaves every pairing intact, so it must not touch the
3995        // fold set at all. (2) Even a BRACKET edit that breaks no existing fold must
3996        // not scan (retain) the whole fold set: the windowed reconcile checks only
3997        // the folds its re-matched region covers and mutates nothing when none
3998        // broke. Only an edit that actually breaks a fold pays the O(folds) removal.
3999        use crate::fold_map::RECONCILE_SCANS;
4000        let mut d = doc("fn f() {\n  body\n}\ntail\n");
4001        let open = d.text().find('{').unwrap() as u32;
4002        assert!(d.toggle_fold_opener(open));
4003        d.set_selections(SelectionSet::new(0));
4004
4005        // "a"/"\n": no bracket char → reconcile never runs. "{": a bracket edit
4006        // that re-pairs but breaks no fold (the block's own pair stays matched) →
4007        // the windowed reconcile inspects the fold and drops nothing, so still no
4008        // whole-set scan.
4009        for text in ["a", "\n", "{"] {
4010            let base = RECONCILE_SCANS.with(std::cell::Cell::get);
4011            d.edit(vec![EditOp::insert(0, text)]).unwrap();
4012            assert_eq!(
4013                RECONCILE_SCANS.with(std::cell::Cell::get) - base,
4014                0,
4015                "inserting {text:?} breaks no fold, so it must not scan the fold set"
4016            );
4017        }
4018
4019        // Deleting the block's `}` breaks its pair → the fold must drop → one scan.
4020        let close = d.text().rfind('}').unwrap() as u32;
4021        let base = RECONCILE_SCANS.with(std::cell::Cell::get);
4022        d.edit(vec![EditOp::delete(close..close + 1)]).unwrap();
4023        assert!(
4024            RECONCILE_SCANS.with(std::cell::Cell::get) - base >= 1,
4025            "deleting a brace that breaks a fold must reconcile the fold set"
4026        );
4027        assert!(d.folds().is_empty(), "the broken fold was dropped");
4028    }
4029
4030    #[test]
4031    fn windowed_reconcile_matches_the_whole_set_reconcile_under_random_edits() {
4032        use crate::SelectionSet;
4033        // The edit-path windowed reconcile must drop EXACTLY the folds the whole-set
4034        // reconcile would. Oracle: after each random bracket-heavy edit, an extra
4035        // whole-set reconcile must be a no-op (find nothing more to drop). A window
4036        // that misses a broken fold — e.g. an enclosing fold whose partner an edit
4037        // deletes far from its opener, so the post-edit brackets no longer flag it
4038        // as enclosing — diverges here immediately. A naive window that omits the
4039        // seed-stack (enclosing) left edge would fail this.
4040        let mut src = String::new();
4041        for i in 0..40 {
4042            src.push_str(&format!("block{i} {{\n  a\n  b\n}}\n"));
4043        }
4044        let mut d = doc(&src);
4045        for open in d.collapsible_pairs().iter().map(|&(o, ..)| o).collect::<Vec<_>>() {
4046            d.toggle_fold_opener(open);
4047        }
4048        // Deterministic LCG — no rand crate, no Date::now (both banned in tests).
4049        let mut state: u32 = 0x1234_5678;
4050        let mut rng = || {
4051            state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
4052            state
4053        };
4054        for _ in 0..300 {
4055            let len = d.buffer().len();
4056            if len == 0 {
4057                break;
4058            }
4059            let pos = rng() % len;
4060            match rng() % 5 {
4061                0 => {
4062                    d.set_selections(SelectionSet::new(pos));
4063                    d.insert_text("{");
4064                }
4065                1 => {
4066                    d.set_selections(SelectionSet::new(pos));
4067                    d.insert_text("}");
4068                }
4069                2 if len > 1 => {
4070                    // Delete one byte, char-snapped (the doc is ASCII, so pos is a
4071                    // boundary), biased toward braces to break pairs often.
4072                    let s = pos.min(len - 1);
4073                    d.edit(vec![EditOp::delete(s..s + 1)]).unwrap();
4074                }
4075                _ => {
4076                    d.set_selections(SelectionSet::new(pos));
4077                    d.insert_text("x");
4078                }
4079            }
4080            let before: Vec<u32> = d.folds().iter().collect();
4081            d.force_whole_reconcile();
4082            let after: Vec<u32> = d.folds().iter().collect();
4083            assert_eq!(before, after, "windowed reconcile missed a fold the whole-set pass caught");
4084            // Every surviving fold is a genuine foldable pair — no orphan left behind.
4085            for o in after {
4086                let foldable = d
4087                    .brackets()
4088                    .at(o)
4089                    .filter(|b| b.open)
4090                    .and_then(|b| b.partner)
4091                    .is_some_and(|c| crate::row_layout::pair_has_interior(o, c));
4092                assert!(foldable, "a surviving fold at {o} is not a foldable pair");
4093            }
4094        }
4095    }
4096
4097    #[test]
4098    fn fold_at_carets_ejects_carets_hidden_by_a_block() {
4099        use crate::SelectionSet;
4100        let mut d = doc("a {\nhidden\n}\nb"); // block: { at 2, } at 11 (rows 0..=2)
4101        let inside = d.buffer().point_to_offset(Point::new(1, 3)); // inside "hidden"
4102        d.set_selections(SelectionSet::new(inside));
4103        assert!(d.fold_at_carets(false), "the block folds");
4104        // The caret sat on a now-hidden interior row — the batched ejection must
4105        // pull it to the visible header row, never leave it on invisible text.
4106        let row = d.buffer().offset_to_point(d.selections().newest().head()).row;
4107        assert_eq!(row, 0, "caret ejected to the header row");
4108    }
4109
4110    #[test]
4111    fn horizontal_movement_hops_an_inline_fold() {
4112        use crate::{Motion, SelectionSet};
4113        let mut d = doc("x = [1, 2, 3]"); // `[` at 4, `]` at 12
4114        d.set_selections(SelectionSet::new(6)); // inside the array
4115        let op = d.fold_opener_at_caret(false).unwrap();
4116        assert!(d.toggle_fold_opener(op));
4117        assert_eq!(d.selections().newest().head(), 5, "caret snapped just after `[`");
4118        d.move_carets(Motion::Right, false);
4119        assert_eq!(d.selections().newest().head(), 12, "Right hops the collapsed interior to `]`");
4120        d.move_carets(Motion::Left, false);
4121        assert_eq!(d.selections().newest().head(), 5, "Left hops back to just after `[`");
4122    }
4123
4124    #[test]
4125    fn collapsible_at_prefers_the_innermost() {
4126        // A block `{ … }` containing an inline `[ … ]` — nested collapsibles.
4127        let d = doc("fn m() {\n    let x = [1, 2, 3];\n}");
4128        let mut pairs = d.collapsible_pairs();
4129        pairs.sort();
4130        assert_eq!(pairs, vec![(7, 32, 0, 2), (21, 29, 1, 1)]);
4131        // Inside the array → the tighter inline pair wins over its enclosing block.
4132        assert_eq!(d.collapsible_at(24), Some(21));
4133        // Inside the block but left of the array → the block.
4134        assert_eq!(d.collapsible_at(12), Some(7));
4135        // On the `fn` header, before the `{` → nothing collapsible.
4136        assert_eq!(d.collapsible_at(0), None);
4137    }
4138
4139    #[test]
4140    fn collapsible_excludes_the_already_folded() {
4141        let mut d = doc("fn m() {\n    let x = [1, 2, 3];\n}");
4142        // Fold the block; it drops out of the candidate set (nothing left to
4143        // collapse), but the inline array inside stays reachable.
4144        assert!(d.toggle_fold_opener(7));
4145        let openers: Vec<u32> = d.collapsible_pairs().iter().map(|&(o, ..)| o).collect();
4146        assert_eq!(openers, vec![21]);
4147        assert_eq!(d.collapsible_at(12), None, "the folded block is no longer a target");
4148        assert_eq!(d.collapsible_at(24), Some(21), "the array inside it still is");
4149    }
4150
4151    #[test]
4152    fn nested_inline_folds_reduce_to_the_outer() {
4153        use crate::fold_map::FoldMap;
4154        // `(` at 10, `[` at 14, `]` at 22, `)` at 23 — the params contain the array.
4155        let mut d = doc("outer_call(a, [1, 2, 3])");
4156        assert!(d.toggle_fold_opener(14)); // collapse the array first
4157        assert!(d.toggle_fold_opener(10)); // then the outer_call params around it
4158        let inline = |d: &Document| {
4159            FoldMap::new(d.folds(), d.brackets(), d.buffer()).inline_folds().iter().map(|f| f.open).collect::<Vec<_>>()
4160        };
4161        // Only the outer `( … )` chip renders; the array is hidden inside it.
4162        assert_eq!(inline(&d), vec![10]);
4163        // Expanding the params reveals the array, still collapsed (state preserved).
4164        assert!(d.toggle_fold_opener(10));
4165        assert_eq!(inline(&d), vec![14]);
4166    }
4167
4168    #[test]
4169    fn horizontal_movement_hops_the_collapsed_gap() {
4170        use crate::{BufferRow, Motion, Point, SelectionSet};
4171        // rows: 0 "m {", 1 "  a", 2 "  b", 3 "}", 4 "after"
4172        let mut d = doc("m {\n  a\n  b\n}\nafter");
4173        assert!(d.fold(BufferRow(0), BufferRow(3))); // collapse; the tail is row 3 `}`
4174        let caret = |d: &Document| d.buffer().offset_to_point(d.selections().newest().head());
4175        // Caret at the end of the header line "m {" (row 0, col 3).
4176        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(0, 3))));
4177        d.move_carets(Motion::Right, false);
4178        assert_eq!(caret(&d), Point::new(3, 0), "Right hops the hidden interior to the closing bracket");
4179        d.move_carets(Motion::Left, false);
4180        assert_eq!(caret(&d), Point::new(0, 3), "Left hops back to the header end");
4181    }
4182
4183    #[test]
4184    fn vertical_move_skips_folds() {
4185        use crate::{BufferRow, Motion, Point, SelectionSet};
4186        let mut d = doc("L0\n{\nL2\nL3\n}\nL5");
4187        assert!(d.fold(BufferRow(1), BufferRow(4))); // { on row 1 … } on row 4 — hide rows 2,3,4
4188        let row = |d: &Document| d.buffer().offset_to_point(d.selections().newest().head()).row;
4189        // Caret on the header row L1; Down lands on L5 (the fold interior is skipped).
4190        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 0))));
4191        d.move_carets(Motion::Down, false);
4192        assert_eq!(row(&d), 5);
4193        // Up from L5 lands back on the header L1, never inside the fold.
4194        d.move_carets(Motion::Up, false);
4195        assert_eq!(row(&d), 1);
4196    }
4197
4198    #[test]
4199    fn typing_into_an_inline_folds_gap_expands_it() {
4200        use crate::SelectionSet;
4201        // `x = [1, 2, 3]` — fold the pair; the caret is pulled to just after
4202        // `[` (offset 5). Typing there would insert a CHIP-HIDDEN character,
4203        // so the edit expands the fold to reveal itself.
4204        let mut d = doc("x = [1, 2, 3]");
4205        d.set_selections(SelectionSet::new(6));
4206        assert!(d.toggle_fold_opener(4));
4207        assert_eq!(d.selections().newest().head(), 5, "caret ejected to the left edge");
4208        d.type_char('9');
4209        assert!(!d.folds().is_folded(4), "editing hidden text expands the fold");
4210        assert_eq!(d.text(), "x = [91, 2, 3]");
4211    }
4212
4213    #[test]
4214    fn typing_at_a_collapsed_headers_end_keeps_it_folded() {
4215        use crate::{Point, SelectionSet};
4216        // Rows: 0 "m {", 1 "  a", 2 "}" — fold, caret ejected to the header's
4217        // end (a VISIBLE position, just before the `…`): typing there edits
4218        // the header line in plain sight, so the fold stays collapsed.
4219        let mut d = doc("m {\n  a\n}");
4220        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 0))));
4221        let opener = 2;
4222        assert!(d.toggle_fold_opener(opener));
4223        d.type_char('x'); // header line becomes "m {x"... still one visible edit
4224        assert!(d.folds().is_folded(opener), "a visible-part edit leaves the fold alone");
4225        assert!(d.text().starts_with("m {x\n"));
4226    }
4227
4228    #[test]
4229    fn undo_into_a_folded_region_expands_it() {
4230        use crate::{Point, SelectionSet};
4231        // Type inside the (unfolded) block, then fold, then undo: the revert
4232        // touches now-hidden text, so the fold expands to show it — the same
4233        // reveal rule as forward edits, through the same seam.
4234        let mut d = doc("m {\n  a\n}");
4235        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 3))));
4236        d.type_char('b'); // "  ab" on row 1
4237        let opener = 2;
4238        assert!(d.toggle_fold_opener(opener));
4239        assert!(d.folds().is_folded(opener));
4240        assert!(d.undo());
4241        assert!(!d.folds().is_folded(opener), "undoing hidden text reveals it");
4242        assert!(d.text().contains("\n  a\n"), "the typed char was reverted");
4243    }
4244
4245    #[test]
4246    fn folding_a_block_ejects_a_hidden_caret_to_the_header_end() {
4247        use crate::{Point, SelectionSet};
4248        // Rows: 0 "m {", 1 "  a", 2 "  }" — the `{` at offset 2.
4249        let mut d = doc("m {\n  a\n  }");
4250        let opener = 2;
4251        // A caret on an interior row is ejected to the header line's end…
4252        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 2))));
4253        assert!(d.toggle_fold_opener(opener));
4254        assert_eq!(
4255            d.selections().newest().head(),
4256            d.buffer().point_to_offset(Point::new(0, 3)),
4257            "the hidden caret ejects to just before the placeholder"
4258        );
4259        assert!(d.toggle_fold_opener(opener)); // unfold
4260        // …a caret in the tail row's leading whitespace (also hidden) ejects…
4261        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(2, 1))));
4262        assert!(d.toggle_fold_opener(opener));
4263        assert_eq!(d.selections().newest().head(), d.buffer().point_to_offset(Point::new(0, 3)));
4264        assert!(d.toggle_fold_opener(opener)); // unfold
4265        // …but a caret ON the visible tail (the `}`) stays exactly put.
4266        let on_tail = d.buffer().point_to_offset(Point::new(2, 2));
4267        d.set_selections(SelectionSet::new(on_tail));
4268        assert!(d.toggle_fold_opener(opener));
4269        assert_eq!(d.selections().newest().head(), on_tail, "the visible tail is not ejected");
4270    }
4271
4272    #[test]
4273    fn fold_chord_works_when_the_caret_touches_a_bracket() {
4274        use crate::SelectionSet;
4275        // `x = [1, 2, 3]` — `[` at 4, `]` at 12.
4276        let mut d = doc("x = [1, 2, 3]");
4277        // Caret just BEFORE the `[` (offset 4): touching the opener folds.
4278        d.set_selections(SelectionSet::new(4));
4279        assert_eq!(d.fold_opener_at_caret(false), Some(4), "touching the opener from the left");
4280        // Caret just AFTER the `]` (offset 13): touching the closer folds.
4281        d.set_selections(SelectionSet::new(13));
4282        assert_eq!(d.fold_opener_at_caret(false), Some(4), "touching the closer from the right");
4283        // …and once collapsed, the same touching positions unfold it.
4284        assert!(d.toggle_fold_opener(4));
4285        assert_eq!(d.fold_opener_at_caret(true), Some(4), "touching unfolds too");
4286        // Away from the pair entirely: nothing to fold.
4287        assert!(d.toggle_fold_opener(4));
4288        d.set_selections(SelectionSet::new(1));
4289        assert_eq!(d.fold_opener_at_caret(false), None);
4290    }
4291
4292    #[test]
4293    fn fold_chord_at_a_shared_boundary_picks_the_touched_inner_pair() {
4294        use crate::SelectionSet;
4295        // `{a [b] c}` — outer `{` 0 / `}` 8, inner `[` 3 / `]` 5. A caret just
4296        // after the inner `]` (offset 6) is inside the block AND touching the
4297        // inline pair — the tighter, touched pair wins.
4298        let d = {
4299            let mut d = doc("{a [b] c}");
4300            d.set_selections(SelectionSet::new(6));
4301            d
4302        };
4303        assert_eq!(d.fold_opener_at_caret(false), Some(3), "the touched inline pair beats the enclosing block");
4304    }
4305
4306    #[test]
4307    fn vertical_goal_is_a_display_cell_across_tab_stops() {
4308        use crate::{Motion, Point, SelectionSet};
4309        // Row 0: a tab then "ab" — the caret after the tab sits at CELL 4.
4310        // Row 1: plain "xxxxxxxxxx". The vertical goal is the visual column, so
4311        // it lands at cell 4 = col 4, not the byte column (col 1) far to the left.
4312        let mut d = doc("\tab\nxxxxxxxxxx");
4313        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(0, 1))));
4314        d.move_carets(Motion::Down, false);
4315        assert_eq!(
4316            d.buffer().offset_to_point(d.selections().newest().head()),
4317            Point::new(1, 4),
4318            "the goal is the visual column, not the byte column"
4319        );
4320        // And back up: cell 4 re-resolves to just after the tab (col 1).
4321        d.move_carets(Motion::Up, false);
4322        assert_eq!(d.buffer().offset_to_point(d.selections().newest().head()), Point::new(0, 1));
4323    }
4324
4325    #[test]
4326    fn vertical_move_through_a_collapsed_tail_stays_visual() {
4327        use crate::{BufferRow, Motion, Point, SelectionSet};
4328        // Rows: 0 "m {", 1 "a", 2 "} t", 3 "abcdefgh"; fold rows 0..=2. The
4329        // collapsed line reads `m { … } t` — the tail renders at cells 7…10.
4330        let mut d = doc("m {\na\n} t\nabcdefgh");
4331        assert!(d.fold(BufferRow(0), BufferRow(2)));
4332        // Caret at the tail's end (row 2, col 3) — display cell 10.
4333        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(2, 3))));
4334        d.move_carets(Motion::Down, false);
4335        assert_eq!(
4336            d.buffer().offset_to_point(d.selections().newest().head()),
4337            Point::new(3, 8),
4338            "Down from the far-right tail lands at the row's visual end, not byte col 3"
4339        );
4340        // Up from cell 8 lands back ON the visible tail (row 2), not the header text.
4341        d.move_carets(Motion::Up, false);
4342        assert_eq!(
4343            d.buffer().offset_to_point(d.selections().newest().head()).row,
4344            2,
4345            "a goal over the placeholder tail resolves onto the tail"
4346        );
4347    }
4348
4349    #[test]
4350    fn vertical_landing_snaps_out_of_a_collapsed_inline_gap() {
4351        use crate::{Motion, Point, SelectionSet};
4352        // Row 0 plain; row 1 has `[123456]` collapsed — its chip spans cells
4353        // 2..5. A goal cell on the chip snaps to the landable left edge,
4354        // never a hidden interior slot.
4355        let mut d = doc("aaaaaaaaaa\nx[123456]y");
4356        let opener = d.buffer().text().find('[').unwrap() as u32;
4357        assert!(d.toggle_fold_opener(opener));
4358        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(0, 3))));
4359        d.move_carets(Motion::Down, false);
4360        assert_eq!(
4361            d.selections().newest().head(),
4362            opener + 1,
4363            "a landing on the chip snaps to just after the opening bracket"
4364        );
4365    }
4366
4367    #[test]
4368    fn home_and_end_operate_on_the_collapsed_display_line() {
4369        use crate::{BufferRow, Motion, Point, SelectionSet};
4370        // Rows: 0 "L0", 1 "  m {", 2 "a", 3 "  }" — the collapsed display line
4371        // reads `  m { … }`, with the tail `}` a real position on row 3.
4372        let mut d = doc("L0\n  m {\na\n  }");
4373        assert!(d.fold(BufferRow(1), BufferRow(3)));
4374        let head = |d: &Document| d.selections().newest().head();
4375        // End on the collapsed HEADER goes past the visible tail — the last
4376        // row's end — not to the header text's own end mid-placeholder.
4377        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 2))));
4378        d.move_carets(Motion::LineEnd, false);
4379        assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(3, 3)), "End lands after the tail brace");
4380        // Home from the TAIL goes to the display line's start: the header's
4381        // first non-whitespace…
4382        d.move_carets(Motion::LineStart, false);
4383        assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(1, 2)), "Home jumps to the header's smart start");
4384        // …and a second Home (now on the header) toggles to column 0.
4385        d.move_carets(Motion::LineStart, false);
4386        assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(1, 0)));
4387        // Unfolded, End is the plain line end again.
4388        assert!(d.unfold(BufferRow(1)));
4389        d.set_selections(SelectionSet::new(d.buffer().point_to_offset(Point::new(1, 2))));
4390        d.move_carets(Motion::LineEnd, false);
4391        assert_eq!(head(&d), d.buffer().point_to_offset(Point::new(1, 5)), "plain End at the header's own end");
4392    }
4393
4394    #[test]
4395    fn foldable_ranges_from_multiline_brackets() {
4396        // Rows: 0 `mod m {`, 1 `fn f() {`, 2 `if x {`, 3 `a`, 4 `}`, 5 `}`,
4397        // 6 `g()` (single-line), 7 `}`.
4398        let d = doc("mod m {\n    fn f() {\n        if x {\n            a\n        }\n    }\n    g()\n}");
4399        let pairs: Vec<(u32, u32)> = d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect();
4400        // Every multi-line pair; the single-line `q()` is not foldable.
4401        assert_eq!(pairs, vec![(0, 7), (1, 5), (2, 4)]);
4402    }
4403
4404    #[test]
4405    fn fold_dropped_when_its_bracket_pair_is_broken() {
4406        use crate::{fold_map::FoldMap, BufferRow};
4407        // 0 "m {", 1 "  a", 2 "  b", 3 "}", 4 "after"
4408        let mut d = doc("m {\n  a\n  b\n}\nafter");
4409        assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(0, 3)]);
4410        assert!(d.fold(BufferRow(0), BufferRow(3)));
4411        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_row_count(), 2); // header + "after"
4412
4413        // Delete only the `}` line (row 3). Its interior rows 1,2 are untouched, so
4414        // the fold rides the patch and would survive — orphaning rows 1,2 behind a
4415        // header that no longer draws a chevron. reconcile_folds must drop it.
4416        let s = d.buffer().point_to_offset(crate::Point::new(3, 0));
4417        let e = d.buffer().point_to_offset(crate::Point::new(4, 0));
4418        d.edit(vec![EditOp::delete(s..e)]).unwrap();
4419        assert!(d.foldable_ranges().is_empty(), "no closing brace ⇒ nothing foldable");
4420        assert!(d.folds().is_empty(), "invalidated fold dropped, not left orphaning hidden rows");
4421        assert_eq!(
4422            FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_row_count(),
4423            d.buffer().line_count(),
4424            "every row is visible again"
4425        );
4426    }
4427
4428    #[test]
4429    fn valid_fold_survives_edit_above() {
4430        use crate::{fold_map::FoldMap, BufferRow, SelectionSet};
4431        let mut d = doc("m {\n  a\n  b\n}\nafter");
4432        assert!(d.fold(BufferRow(0), BufferRow(3)));
4433        // Insert a line above the block: the fold stays valid (it still matches a
4434        // foldable range, now shifted down one row) — reconcile must NOT drop it.
4435        d.set_selections(SelectionSet::new(0));
4436        d.insert_text("top\n");
4437        assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(1, 4)]);
4438        assert_eq!(d.folds().len(), 1, "the still-valid fold is kept");
4439        let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
4440        assert_eq!(m.fold_at_header(BufferRow(1)), Some(BufferRow(4)));
4441        assert!(m.is_folded(BufferRow(2)) && m.is_folded(BufferRow(3)) && m.is_folded(BufferRow(4)));
4442    }
4443
4444    #[test]
4445    fn breaking_one_block_keeps_a_sibling_fold() {
4446        use crate::{fold_map::FoldMap, BufferRow};
4447        // Two sibling blocks: A rows 0-2, B rows 3-5, then "after".
4448        // 0 "a {", 1 "  x", 2 "}", 3 "b {", 4 "  y", 5 "}", 6 "after"
4449        let mut d = doc("a {\n  x\n}\nb {\n  y\n}\nafter");
4450        assert_eq!(d.foldable_ranges().iter().map(|(h, l)| (h.0, l.0)).collect::<Vec<_>>(), vec![(0, 2), (3, 5)]);
4451        assert!(d.fold(BufferRow(0), BufferRow(2)));
4452        assert!(d.fold(BufferRow(3), BufferRow(5)));
4453        assert_eq!(d.folds().len(), 2);
4454        // Delete block B's `}` (row 5). B is no longer foldable; A is untouched —
4455        // only B's fold should drop.
4456        let s = d.buffer().point_to_offset(crate::Point::new(5, 0));
4457        let e = d.buffer().point_to_offset(crate::Point::new(6, 0));
4458        d.edit(vec![EditOp::delete(s..e)]).unwrap();
4459        assert_eq!(d.folds().len(), 1, "only the broken block's fold drops");
4460        let m = FoldMap::new(d.folds(), d.brackets(), d.buffer());
4461        assert_eq!(m.fold_at_header(BufferRow(0)), Some(BufferRow(2)), "sibling A still collapsed");
4462        assert_eq!(m.fold_at_header(BufferRow(3)), None, "B is no longer folded");
4463    }
4464
4465    #[test]
4466    fn fold_drops_when_its_interior_is_deleted() {
4467        use crate::{fold_map::FoldMap, BufferRow};
4468        let mut d = doc("L0\n{\nL2\nL3\n}\nL5");
4469        assert!(d.fold(BufferRow(1), BufferRow(4))); // hide rows 2,3,4 (incl. the `}`)
4470        // Delete rows 2..=4 (their whole byte span, incl. the closing `}`).
4471        let start = d.buffer().point_to_offset(crate::Point::new(2, 0));
4472        let end = d.buffer().point_to_offset(crate::Point::new(5, 0));
4473        d.edit(vec![EditOp::delete(start..end)]).unwrap();
4474        assert!(d.folds().is_empty(), "fold dropped when its interior text is deleted");
4475        assert_eq!(FoldMap::new(d.folds(), d.brackets(), d.buffer()).display_row_count(), d.buffer().line_count());
4476    }
4477
4478    #[test]
4479    fn intel05_diagnostic_keeps_byte_offsets_on_a_multibyte_line() {
4480        // The core stores and returns diagnostics as BYTE spans, never
4481        // pre-converted to cells — so the render does the byte→cell mapping
4482        // exactly once (display_map::expand). "aé=x": é is 2 bytes / 1 cell, so
4483        // "x" is byte 4 but cell 3.
4484        use crate::{Diagnostic, Severity};
4485        let mut d = doc("aé=x");
4486        d.set_diagnostics(d.revision(), vec![Diagnostic::new(4..5, Severity::Error, "e")]);
4487        let span = d.diagnostics_in(0..100).next().unwrap().0;
4488        assert_eq!(span, 4..5, "diagnostics_in returns raw byte offsets, not cells");
4489        // The single byte→cell mapping places "x" at cell 3: the multibyte é
4490        // shifted the column by one, not two.
4491        assert_eq!(crate::display_map::expand(&d.buffer().line(0), span.start, 4), 3);
4492    }
4493
4494    #[test]
4495    fn undo_resyncs_brackets_and_highlight_to_the_reverted_text() {
4496        const G: &str = "%YAML 1.2\n---\nname: T\nscope: source.t\ncontexts:\n  main:\n    - match: '\\w+'\n      scope: keyword.t\n";
4497        const TH: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
4498<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4499<plist version="1.0"><dict><key>settings</key><array><dict><key>settings</key><dict><key>foreground</key><string>#FFFFFF</string></dict></dict></array></dict></plist>"#;
4500        let mut d = doc("()");
4501        d.set_syntax(
4502            crate::SyntaxDef::from_sublime_syntax(G).unwrap(),
4503            crate::TokenTheme::from_tm_theme(TH).unwrap(),
4504        );
4505        d.tokenize_highlight(d.buffer().line_count());
4506        d.set_selections(SelectionSet::new(1)); // between ( and )
4507        d.edit(vec![EditOp::insert(1, "[]\n")]).unwrap(); // "([]\n)" — 2 lines, 4 brackets
4508        d.tokenize_highlight(d.buffer().line_count());
4509        assert_eq!((d.buffer().line_count(), d.brackets().all().len()), (2, 4));
4510        assert!(d.undo(), "there was something to undo");
4511        assert_eq!(d.buffer().text(), "()");
4512        // The resync ran: brackets re-matched, highlight cache back to one line.
4513        assert_eq!((d.buffer().line_count(), d.brackets().all().len()), (1, 2));
4514        d.tokenize_highlight(d.buffer().line_count());
4515        assert!(d.highlight_line_spans(0).is_some());
4516        assert!(d.highlight_line_spans(1).is_none(), "no phantom line after undo");
4517    }
4518
4519    #[test]
4520    fn select_all_spans_the_whole_buffer_with_head_at_end() {
4521        let mut d = doc("abc\ndef"); // 7 bytes
4522        d.select_all();
4523        assert_eq!(d.selections().len(), 1);
4524        let s = d.selections().newest();
4525        assert_eq!((s.start(), s.end(), s.head()), (0, 7, 7));
4526    }
4527
4528    fn query(text: &str) -> crate::FindQuery {
4529        crate::FindQuery { text: text.into(), case_sensitive: true }
4530    }
4531
4532    #[test]
4533    fn find_matches_ride_an_edit_above_them() {
4534        // The handle-tracked match set remaps across an edit above it.
4535        let mut d = doc("xx target"); // "target" at 3..9
4536        d.set_find_query(Some(query("target")), 0);
4537        assert_eq!(d.find_matches_in(0..100).collect::<Vec<_>>(), vec![(3..9, false)]);
4538        d.set_selections(SelectionSet::new(0));
4539        d.edit(vec![EditOp::insert(0, "ab")]).unwrap(); // 2 chars above the match
4540        // Rode via apply_patch to 5..11 (the repair window near the edit then
4541        // re-verifies it in place).
4542        assert_eq!(d.find_matches_in(0..100).collect::<Vec<_>>(), vec![(5..11, false)]);
4543    }
4544
4545    #[test]
4546    fn find_next_prev_start_from_the_caret_and_wrap() {
4547        // Nearest-from-caret ordering and wrap-around.
4548        let mut d = doc("foo bar foo baz foo"); // "foo" at 0..3, 8..11, 16..19
4549        d.set_find_query(Some(query("foo")), 0);
4550        assert_eq!((d.find_match_count(), d.active_find_match()), (3, None));
4551        d.set_selections(SelectionSet::new(5)); // between match 0 and 1
4552        assert_eq!(d.find_next(0), Some(8..11)); // first start ≥ 5
4553        assert_eq!(d.active_find_match(), Some(1));
4554        assert_eq!(d.find_next(0), Some(16..19)); // repeated press cycles
4555        assert_eq!(d.find_next(0), Some(0..3), "wraps to first");
4556        assert_eq!(d.find_prev(0), Some(16..19), "prev from first wraps to last");
4557        d.set_selections(SelectionSet::new(0));
4558        assert_eq!(d.find_prev(0), Some(16..19), "prev before all wraps to last");
4559    }
4560
4561    #[test]
4562    fn active_match_survives_a_rescan_at_the_same_start() {
4563        // The active match survives an edit that adds matches elsewhere: the
4564        // commit-path repair makes the new match visible IMMEDIATELY and the
4565        // untouched active survives by decoration-id stability —
4566        // `maybe_rescan_find` is a no-op on an uncapped set.
4567        let mut d = doc("foo foo foo");
4568        d.set_find_query(Some(query("foo")), 0);
4569        d.set_selections(SelectionSet::new(4)); // the middle match's start
4570        d.find_next(0);
4571        assert_eq!(d.active_find_match(), Some(1));
4572        d.set_selections(SelectionSet::new(11));
4573        d.edit(vec![EditOp::insert(11, " foo")]).unwrap(); // a 4th match, below
4574        assert_eq!(
4575            (d.find_match_count(), d.active_find_match()),
4576            (4, Some(1)),
4577            "current at the commit, active untouched"
4578        );
4579        assert!(!d.maybe_rescan_find(1000), "nothing stale to rescan (uncapped)");
4580        assert_eq!((d.find_match_count(), d.active_find_match()), (4, Some(1)));
4581    }
4582
4583    #[test]
4584    fn find_matches_ride_undo_and_redo_through_the_shared_mover() {
4585        // Find highlights ride the reverse patch on undo/redo exactly like any
4586        // forward edit — through the shared decoration mover, with no
4587        // find-specific undo code — so they stay at their correct positions.
4588        let mut d = doc("foo bar");
4589        d.set_find_query(Some(query("foo")), 0);
4590        assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 0..3);
4591        d.set_selections(SelectionSet::new(0));
4592        d.edit(vec![EditOp::insert(0, "xy")]).unwrap(); // "xyfoo bar" → match rides to 2..5
4593        assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 2..5);
4594        d.undo(); // back to "foo bar" → match must ride back to 0..3
4595        assert_eq!(d.buffer().text(), "foo bar");
4596        assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 0..3, "rode the undo");
4597        d.redo(); // → 2..5 again
4598        assert_eq!(d.find_matches_in(0..100).next().unwrap().0, 2..5, "rode the redo");
4599    }
4600
4601    #[test]
4602    fn find_count_reflects_live_matches_after_a_collapse_delete() {
4603        // A collapse-delete drops a FindMatch decoration in `apply_patch`; the
4604        // count must follow the live store set. There is no shadow handle list to
4605        // over-report — the store IS the set.
4606        let mut d = doc("foo foo"); // matches 0..3 and 4..7
4607        d.set_find_query(Some(query("foo")), 0);
4608        assert_eq!(d.find_match_count(), 2);
4609        d.edit(vec![EditOp::delete(0..4)]).unwrap(); // "foo": collapses match 0
4610        assert_eq!(d.buffer().text(), "foo");
4611        assert_eq!(d.find_match_count(), 1, "collapsed match not counted");
4612        assert_eq!(d.find_matches_in(0..100).count(), 1, "count agrees with render");
4613    }
4614
4615    #[test]
4616    fn find_navigation_requests_a_reveal() {
4617        let mut d = doc("foo bar foo");
4618        d.set_find_query(Some(query("foo")), 0);
4619        let before = d.reveal_seq();
4620        assert!(d.find_next(0).is_some());
4621        assert!(d.reveal_seq() > before, "navigation bumps the reveal request");
4622    }
4623
4624    #[test]
4625    fn clearing_the_query_drops_every_match() {
4626        let mut d = doc("foo foo");
4627        d.set_find_query(Some(query("foo")), 0);
4628        assert_eq!(d.find_match_count(), 2);
4629        d.set_find_query(None, 0);
4630        assert_eq!((d.find_match_count(), d.find_query().is_none()), (0, true));
4631        assert_eq!(d.find_matches_in(0..100).count(), 0);
4632    }
4633
4634    #[test]
4635    fn edits_repair_the_match_set_immediately() {
4636        // The eager per-commit repair makes the match set current AT the commit,
4637        // so `maybe_rescan_find` narrows to the capped-refill path — a no-op on
4638        // an uncapped set, before or after any debounce window.
4639        let mut d = doc("foo");
4640        d.set_find_query(Some(query("foo")), 0);
4641        d.edit(vec![EditOp::insert(0, "foo ")]).unwrap(); // "foo foo"
4642        assert_eq!(d.find_match_count(), 2, "current immediately — no debounce");
4643        assert!(!d.maybe_rescan_find(50), "nothing to rescan");
4644        assert!(!d.maybe_rescan_find(1_000), "uncapped: never rescans");
4645        assert_eq!(d.find_match_count(), 2);
4646    }
4647
4648    // --- Find-match repair battery. ---
4649
4650    /// The live `FindMatch` decoration ids, in document order.
4651    fn match_ids(d: &Document) -> Vec<crate::decorations::DecorationId> {
4652        d.decorations
4653            .decorations_in(0..u32::MAX)
4654            .filter(|r| matches!(r.kind, DecorationKind::FindMatch))
4655            .map(|r| r.id)
4656            .collect()
4657    }
4658
4659    /// The live match ranges, in document order.
4660    fn match_ranges(d: &Document) -> Vec<Range<u32>> {
4661        d.find_matches_in(0..u32::MAX).map(|(r, _)| r).collect()
4662    }
4663
4664    /// A seeded xorshift for the randomized repair oracles.
4665    fn xorshift(mut state: u64) -> impl FnMut() -> u64 {
4666        move || {
4667            state ^= state << 13;
4668            state ^= state >> 7;
4669            state ^= state << 17;
4670            state
4671        }
4672    }
4673
4674    #[test]
4675    fn repair_preserves_far_match_ids() {
4676        // An edit FAR from every match must not re-mint the match decorations —
4677        // the repair touches only matches near the edit, so untouched ones keep
4678        // their ids rather than being wholesale-replaced.
4679        let mut d = doc("foo bar foo bar baz padding padding foo");
4680        d.set_find_query(Some(query("foo")), 0);
4681        let before = match_ids(&d);
4682        assert_eq!(before.len(), 3);
4683        // Inside the first "padding" — farther than a needle length from all.
4684        d.edit(vec![EditOp::insert(22, "zz")]).unwrap();
4685        assert!(!d.maybe_rescan_find(1_000), "always current: nothing to rescan");
4686        assert_eq!(match_ids(&d), before, "untouched matches keep their ids");
4687        assert_eq!(match_ranges(&d), vec![0..3, 8..11, 38..41]);
4688    }
4689
4690    #[test]
4691    fn repair_insert_creates_match_in_window() {
4692        let mut d = doc("fo bar");
4693        d.set_find_query(Some(query("foo")), 0);
4694        assert_eq!(d.find_match_count(), 0);
4695        d.edit(vec![EditOp::insert(2, "o")]).unwrap(); // "foo bar"
4696        assert_eq!(match_ranges(&d), vec![0..3], "current at the commit");
4697    }
4698
4699    #[test]
4700    fn repair_delete_joins_halves_creates_match() {
4701        // Deleting the interloper joins "fo" + "o" into a fresh match.
4702        let mut d = doc("foXo bar");
4703        d.set_find_query(Some(query("foo")), 0);
4704        assert_eq!(d.find_match_count(), 0);
4705        d.edit(vec![EditOp::delete(2..3)]).unwrap(); // "foo bar"
4706        assert_eq!(match_ranges(&d), vec![0..3]);
4707    }
4708
4709    #[test]
4710    fn repair_destroys_straddling_match() {
4711        // A deletion straddling the match's tail leaves "fo" — no match.
4712        let mut d = doc("afoob");
4713        d.set_find_query(Some(query("foo")), 0);
4714        assert_eq!(match_ranges(&d), vec![1..4]);
4715        d.edit(vec![EditOp::delete(3..5)]).unwrap(); // "afo"
4716        assert_eq!(d.find_match_count(), 0);
4717    }
4718
4719    #[test]
4720    fn repair_interior_insert_invalidates_match() {
4721        // NeverGrows grows on a strictly-interior insert: the grown range no
4722        // longer equals the needle, so the repair must remove it.
4723        let mut d = doc("foo");
4724        d.set_find_query(Some(query("foo")), 0);
4725        assert_eq!(d.find_match_count(), 1);
4726        d.edit(vec![EditOp::insert(1, "x")]).unwrap(); // "fxoo"
4727        assert_eq!(d.find_match_count(), 0);
4728    }
4729
4730    #[test]
4731    fn repair_multi_edit_transaction() {
4732        // ONE apply() with two scattered edits — one patch, two repair windows.
4733        let mut d = doc("fo bar fo baz");
4734        d.set_find_query(Some(query("foo")), 0);
4735        d.edit(vec![EditOp::insert(2, "o"), EditOp::insert(9, "o")]).unwrap();
4736        assert_eq!(d.buffer().text(), "foo bar foo baz");
4737        assert_eq!(match_ranges(&d), vec![0..3, 8..11]);
4738    }
4739
4740    #[test]
4741    fn repair_equals_full_scan_property() {
4742        // The repair oracle: for a NON-self-overlapping needle, after EVERY
4743        // commit the live match set is byte-identical to a fresh `scan` of the
4744        // live text. Driven through Document::edit so the rebase_views wiring is
4745        // what's under test.
4746        let fq = query("ab");
4747        let mut next = xorshift(0x243F_6A88_85A3_08D3);
4748        let alphabet = ['a', 'b', ' '];
4749        let mut d = doc(&"ab ba a b ab ".repeat(8));
4750        d.set_find_query(Some(fq.clone()), 0);
4751        for step in 0..300 {
4752            let len = d.buffer().len();
4753            match next() % 3 {
4754                0 => {
4755                    let at = (next() % u64::from(len + 1)) as u32;
4756                    let n = 1 + next() % 3;
4757                    let s: String = (0..n).map(|_| alphabet[(next() % 3) as usize]).collect();
4758                    d.edit(vec![EditOp::insert(at, s)]).unwrap();
4759                }
4760                1 if len > 0 => {
4761                    let a = (next() % u64::from(len)) as u32;
4762                    let b = (a + 1 + (next() % 4) as u32).min(len);
4763                    d.edit(vec![EditOp::delete(a..b)]).unwrap();
4764                }
4765                _ => {
4766                    // Two scattered inserts as ONE transaction.
4767                    let a = (next() % u64::from(len + 1)) as u32;
4768                    let b = (next() % u64::from(len + 1)) as u32;
4769                    let (a, b) = (a.min(b), a.max(b));
4770                    if a == b {
4771                        d.edit(vec![EditOp::insert(a, "ab")]).unwrap();
4772                    } else {
4773                        d.edit(vec![EditOp::insert(a, "ba"), EditOp::insert(b, "ab")]).unwrap();
4774                    }
4775                }
4776            }
4777            let (oracle, _) = crate::find::scan(&d.buffer().text(), &fq);
4778            assert_eq!(match_ranges(&d), oracle, "step {step} diverged from the oracle");
4779        }
4780    }
4781
4782    #[test]
4783    fn repair_self_overlapping_needle_is_maximal_and_valid() {
4784        // Documented relaxation: a self-overlapping needle ("aa") repairs to a
4785        // MAXIMAL valid non-overlapping set — every match equals the needle, no
4786        // two overlap, and no further non-overlapping placement exists — though
4787        // the phase near an edit may differ from a from-scratch greedy scan until
4788        // the next full scan.
4789        let mut next = xorshift(0x9E37_79B9_7F4A_7C15);
4790        let alphabet = ['a', ' '];
4791        let mut d = doc(&"aaa a aaaa aa ".repeat(6));
4792        d.set_find_query(Some(query("aa")), 0);
4793        for step in 0..300 {
4794            let len = d.buffer().len();
4795            if next().is_multiple_of(2) || len == 0 {
4796                let at = (next() % u64::from(len + 1)) as u32;
4797                let n = 1 + next() % 3;
4798                let s: String = (0..n).map(|_| alphabet[(next() % 2) as usize]).collect();
4799                d.edit(vec![EditOp::insert(at, s)]).unwrap();
4800            } else {
4801                let a = (next() % u64::from(len)) as u32;
4802                let b = (a + 1 + (next() % 3) as u32).min(len);
4803                d.edit(vec![EditOp::delete(a..b)]).unwrap();
4804            }
4805            let live = match_ranges(&d);
4806            let text = d.buffer().text();
4807            let bytes = text.as_bytes();
4808            for (i, r) in live.iter().enumerate() {
4809                assert_eq!(
4810                    &bytes[r.start as usize..r.end as usize],
4811                    b"aa",
4812                    "step {step}: stale match {r:?}"
4813                );
4814                if i > 0 {
4815                    assert!(live[i - 1].end <= r.start, "step {step}: overlapping matches");
4816                }
4817            }
4818            for p in 0..bytes.len().saturating_sub(1) {
4819                if &bytes[p..p + 2] == b"aa" {
4820                    let p = p as u32;
4821                    assert!(
4822                        live.iter().any(|r| r.start < p + 2 && p < r.end),
4823                        "step {step}: placement at {p} misses every match (not maximal)"
4824                    );
4825                }
4826            }
4827        }
4828    }
4829
4830    #[test]
4831    fn active_survives_untouched() {
4832        // Active-match stability by id: an edit near OTHER matches leaves the
4833        // active one's decoration (and so its N-of-M slot) alone.
4834        let mut d = doc("foo bar foo");
4835        d.set_find_query(Some(query("foo")), 0);
4836        d.find_next(0); // activates 0..3
4837        let id = d.find.active_id().expect("a match is active");
4838        d.edit(vec![EditOp::insert(7, "x")]).unwrap(); // within k−1 of the 2nd match
4839        assert_eq!(d.find.active_id(), Some(id), "untouched active keeps its id");
4840        assert_eq!((d.find_match_count(), d.active_find_match()), (2, Some(0)));
4841    }
4842
4843    #[test]
4844    fn active_transfers_same_start() {
4845        // A repair window removes and re-creates the active match at the same
4846        // start — the active transfers to the new id.
4847        let mut d = doc("foo bar");
4848        d.set_find_query(Some(query("foo")), 0);
4849        d.find_next(0); // activates 0..3
4850        let old = d.find.active_id().expect("a match is active");
4851        d.edit(vec![EditOp::insert(4, "x")]).unwrap(); // "foo xbar": in-window, text intact
4852        let new = d.find.active_id().expect("active transferred");
4853        assert_ne!(new, old, "the decoration was re-minted by the repair");
4854        assert_eq!(d.active_find_match(), Some(0), "still the first match");
4855        assert_eq!(match_ranges(&d), vec![0..3]);
4856    }
4857
4858    #[test]
4859    fn active_cleared_when_destroyed() {
4860        let mut d = doc("foo bar foo");
4861        d.set_find_query(Some(query("foo")), 0);
4862        d.find_next(0); // activates 0..3
4863        d.edit(vec![EditOp::delete(0..2)]).unwrap(); // "o bar foo": destroys it
4864        assert_eq!(d.find.active_id(), None, "destroyed active clears");
4865        assert_eq!((d.find_match_count(), d.active_find_match()), (1, None));
4866    }
4867
4868    #[test]
4869    fn count_and_order_consistent_after_repair() {
4870        let mut d = doc("foo foo foo");
4871        d.set_find_query(Some(query("foo")), 0);
4872        d.set_selections(SelectionSet::new(4));
4873        d.find_next(0); // activates the match at 4..7
4874        d.edit(vec![EditOp::insert(0, "foo ")]).unwrap(); // a NEW first match
4875        let all: Vec<(Range<u32>, bool)> = d.find_matches_in(0..u32::MAX).collect();
4876        let ranges: Vec<Range<u32>> = all.iter().map(|(r, _)| r.clone()).collect();
4877        assert_eq!(ranges, vec![0..3, 4..7, 8..11, 12..15]);
4878        assert_eq!(d.find_match_count(), all.len());
4879        // The active rode from 4..7 to 8..11; N-of-M agrees with the iteration.
4880        assert_eq!(d.active_find_match(), Some(2));
4881        assert_eq!(all.iter().position(|(_, active)| *active), Some(2));
4882    }
4883
4884    #[test]
4885    fn active_start_tracks_the_decoration_under_random_edits() {
4886        // Anti-drift pin: with a match active, after EVERY commit the tracked
4887        // `active_start` must equal the active decoration's live start (or both
4888        // be None). It is the single rebased position kept in lockstep with
4889        // `active` across the ~5 mutation sites; if the per-commit rebase is
4890        // dropped (or the transfer forgets to re-set it), an edit *before* the
4891        // active match slides its decoration but leaves `active_start` stale, and
4892        // this trips. (An edit exactly *at* the match start always routes through
4893        // the window remove→transfer, which resets `active_start` — so this pins
4894        // the delta-rebase, and `count_and_order_consistent_after_repair` pins the
4895        // ordinal that rebase feeds.)
4896        let base = "foo foo bar foo baz foo qux foo end";
4897        let mut rng = 0xDEAD_BEEF_0000_1111u64;
4898        let mut next = move || {
4899            rng ^= rng << 13;
4900            rng ^= rng >> 7;
4901            rng ^= rng << 17;
4902            rng
4903        };
4904        for trial in 0..400u32 {
4905            let mut d = doc(base);
4906            d.set_find_query(Some(query("foo")), 0);
4907            // Activate a MID-document match (offset > 0), so "edit before it"
4908            // trials exist to exercise the delta-rebase.
4909            d.set_selections(SelectionSet::new(10));
4910            d.find_next(0);
4911            for _step in 0..14 {
4912                if d.find.active_id().is_none() {
4913                    // Re-activate so the invariant keeps getting exercised.
4914                    d.set_selections(SelectionSet::new(1));
4915                    d.find_next(0);
4916                }
4917                let len = d.buffer().len();
4918                // Sometimes aim the edit at the active match's start (the
4919                // Bias::Right boundary), else a random position.
4920                let at = match d.find.active_start() {
4921                    Some(s) if next() % 3 == 0 => s,
4922                    _ => (next() % u64::from(len + 1)) as u32,
4923                };
4924                match next() % 3 {
4925                    0 => {
4926                        let _ = d.edit(vec![EditOp::insert(at, "z")]);
4927                    }
4928                    1 => {
4929                        let end = (at + 1 + (next() % 3) as u32).min(len);
4930                        if end > at {
4931                            let _ = d.edit(vec![EditOp::delete(at..end)]);
4932                        }
4933                    }
4934                    _ => {
4935                        // Multi-caret insert at two disjoint carets.
4936                        let a = (next() % u64::from(len + 1)) as u32;
4937                        let b = (next() % u64::from(len + 1)) as u32;
4938                        let (lo, hi) = (a.min(b), a.max(b));
4939                        if lo != hi {
4940                            d.set_selections(SelectionSet::from_ranges(&[(lo, lo), (hi, hi)], 0));
4941                            d.insert_text("q");
4942                        }
4943                    }
4944                }
4945                let want = d
4946                    .find
4947                    .active_id()
4948                    .and_then(|id| d.decorations.decoration_range(id))
4949                    .map(|r| r.start);
4950                assert_eq!(
4951                    d.find.active_start(),
4952                    want,
4953                    "trial {trial}: active_start drifted from its decoration's live start",
4954                );
4955            }
4956        }
4957    }
4958
4959    #[test]
4960    fn capped_scan_keeps_a_document_prefix() {
4961        // > FIND_MATCH_CAP occurrences: the fresh scan keeps the first 10k
4962        // (a document prefix) and reports capped.
4963        let text = "x ".repeat(crate::FIND_MATCH_CAP + 50);
4964        let mut d = doc(&text);
4965        d.set_find_query(Some(query("x")), 0);
4966        assert!(d.find.capped());
4967        let live = match_ranges(&d);
4968        assert_eq!(live.len(), crate::FIND_MATCH_CAP);
4969        assert_eq!(live.last().cloned(), Some(19_998..19_999), "prefix, not a sample");
4970    }
4971
4972    #[test]
4973    fn typing_while_capped_does_not_rescan_the_match_set() {
4974        // While a >cap find is active, the per-keystroke cap bookkeeping must be
4975        // O(1): a `decorations_in(0..u32::MAX).filter(is_find).collect()` would
4976        // charge ~FIND_MATCH_CAP work units on EVERY keystroke while capped.
4977        // Reading the count from the root summary (`find_count`, O(1)) and
4978        // trimming via ordinal `nth_find` (O(log)) means a steady capped
4979        // keystroke touches no whole-store scan.
4980        let text = "x ".repeat(crate::FIND_MATCH_CAP + 50);
4981        let mut d = doc(&text);
4982        d.set_find_query(Some(query("x")), 0);
4983        assert!(d.find.capped());
4984        assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP);
4985        d.set_selections(crate::SelectionSet::new(0));
4986        crate::perf::reset();
4987        d.type_char('z'); // adds no `x` ⇒ the set stays exactly at the cap (no trim)
4988        let work = crate::perf::meter();
4989        assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP, "still capped");
4990        assert!(
4991            work < 2_000,
4992            "capped keystroke charged {work} work units (≈FIND_MATCH_CAP ⇒ the O(M) cap scan survived)"
4993        );
4994    }
4995
4996    #[test]
4997    fn capped_trims_tail_and_coverage() {
4998        // Exactly at the cap (uncapped) — one edit adds matches mid-document,
4999        // the repair pushes past the cap, and the TAIL is trimmed so the set
5000        // stays a prefix of the document.
5001        let text = "x ".repeat(crate::FIND_MATCH_CAP);
5002        let mut d = doc(&text);
5003        d.set_find_query(Some(query("x")), 0);
5004        assert!(!d.find.capped());
5005        d.edit(vec![EditOp::insert(1_000, "xxx")]).unwrap();
5006        assert!(d.find.capped(), "over the cap after the repair");
5007        let live = match_ranges(&d);
5008        assert_eq!(live.len(), crate::FIND_MATCH_CAP, "trimmed back to the cap");
5009        assert!(live.windows(2).all(|w| w[0].end <= w[1].start), "still non-overlapping");
5010        // The new matches near the edit are IN; the trim came off the tail.
5011        assert_eq!(live.iter().filter(|r| (1_000..1_004).contains(&r.start)).count(), 4);
5012        assert_eq!(live.last().map(|r| r.start), Some(19_995), "tail trimmed");
5013    }
5014
5015    #[test]
5016    fn capped_refill_is_debounced() {
5017        let text = "x ".repeat(crate::FIND_MATCH_CAP + 50);
5018        let mut d = doc(&text);
5019        d.set_find_query(Some(query("x")), 0); // capped; scan stamp at 0 ms
5020        // Kill 5 matches inside the covered prefix → room below the cap.
5021        d.edit(vec![EditOp::delete(0..10)]).unwrap();
5022        assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP - 5);
5023        assert!(!d.maybe_rescan_find(50), "inside the debounce window");
5024        assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP - 5);
5025        assert!(d.maybe_rescan_find(100), "past the window → refill");
5026        assert_eq!(d.find_match_count(), crate::FIND_MATCH_CAP, "coverage re-grew");
5027        assert!(!d.maybe_rescan_find(300), "idle after the refill: no re-scan");
5028    }
5029
5030    #[test]
5031    fn undo_redo_match_set_equals_fresh_scan() {
5032        // Undo/redo flow through the same rebase_views mover, so the repaired
5033        // set must equal the oracle after every reverted/replayed step too.
5034        let fq = query("ab");
5035        let mut d = doc("ab ab ba ab");
5036        d.set_find_query(Some(fq.clone()), 0);
5037        let check = |d: &Document, at: &str| {
5038            let (oracle, _) = crate::find::scan(&d.buffer().text(), &fq);
5039            assert_eq!(match_ranges(d), oracle, "diverged after {at}");
5040        };
5041        d.edit(vec![EditOp::insert(2, "ab")]).unwrap();
5042        check(&d, "edit 1");
5043        d.edit(vec![EditOp::delete(0..3)]).unwrap();
5044        check(&d, "edit 2");
5045        d.edit(vec![EditOp::insert(5, "b a")]).unwrap();
5046        check(&d, "edit 3");
5047        assert!(d.undo());
5048        check(&d, "undo 1");
5049        assert!(d.undo());
5050        check(&d, "undo 2");
5051        assert!(d.undo());
5052        check(&d, "undo 3");
5053        assert!(d.redo());
5054        check(&d, "redo 1");
5055        assert!(d.redo());
5056        check(&d, "redo 2");
5057    }
5058
5059    #[test]
5060    fn case_fold_repair() {
5061        // The windowed repair honors the query's fold toggle.
5062        let fq = crate::FindQuery { text: "foo".into(), case_sensitive: false };
5063        let mut d = doc("FO bar");
5064        d.set_find_query(Some(fq), 0);
5065        assert_eq!(d.find_match_count(), 0);
5066        d.edit(vec![EditOp::insert(2, "O")]).unwrap(); // "FOO bar"
5067        assert_eq!(match_ranges(&d), vec![0..3]);
5068        d.edit(vec![EditOp::insert(0, "f")]).unwrap(); // "fFOO bar"
5069        assert_eq!(match_ranges(&d), vec![1..4], "re-anchored, still folded");
5070    }
5071}