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