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