Skip to main content

scrive_core/
bracket.rs

1//! Bracket matching — the shared structural pass behind bracket-pair
2//! colorization, matching-bracket highlight, indent guides, and folding.
3//! `()`/`[]`/`{}` pair by a stack automaton (quotes are not nestable), yielding
4//! each bracket's nesting depth and matched-partner offset and flagging the
5//! unmatched.
6//!
7//! By default a purely structural scan — every `()[]{}` counts, even inside a
8//! string or comment. An optional [`BracketConfig`] makes it comment/string
9//! *aware*: a lightweight, line-local `SkipContext` lexer (NOT the highlight
10//! layer — that carries only resolved colors, no lexical scope, and runs lazily
11//! while brackets are eager and whole-document) excludes brackets inside line
12//! comments, strings, and char literals, so they are not colored, matched,
13//! folded, or indent-guided. It is opt-in and zero-cost when off. Block comments
14//! and multi-line strings are deliberately out of scope: their lexer state is
15//! non-local, which would make an edit's cost scale with the document — the whole
16//! reason the scan stays line-local and restartable at every line boundary.
17//!
18//! [`Brackets`] is a thin façade over one `SumTree<BracketItem>` (see
19//! `bracket_tree`) that stores only PRIMARY facts — each bracket's char and
20//! delta-gap position. Every derived fact a caller reads — `open`, `depth`,
21//! `partner`, "what pair encloses this" — is recomputed by an O(log) cursor
22//! descent, so no cached copy can drift, and an edit is a pure
23//! `SumTree::replace` of the edited byte range with
24//! the rescanned brackets. Because no partner offset is stored, there is nothing
25//! to repair across the edit boundary — the tree is always internally
26//! consistent by construction. [`Brackets::match_text`] is the O(n) load-time
27//! constructor (and the tests' oracle); [`Brackets::apply_edit`] rides one
28//! committed patch through the same tree, on the forward path and on every
29//! undo/redo step alike.
30
31use std::ops::Range;
32
33use crate::bracket_tree::{self, BracketItem};
34use crate::buffer::Buffer;
35use crate::patch::Patch;
36use crate::sum_tree::SumTree;
37
38// Op-count canary: how many `enclosing_or_touching` walks ran on this thread. A
39// document-scale multi-caret edit must trigger O(1) of them inside
40// `expand_folds_touched` (one, for the first edit point), NOT one per caret, so a
41// select-all → fold → type sequence stays cheap regardless of caret count.
42// Debug/test only; zero-cost in release.
43#[cfg(any(test, debug_assertions))]
44thread_local! {
45    pub(crate) static ENCLOSING_WALKS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
46}
47
48/// Whether `c` is any bracket char (opener or closer), as a byte — the document's
49/// reconcile gate ("did this edit add or remove a bracket?").
50pub(crate) fn is_bracket_byte(c: u8) -> bool {
51    matches!(c, b'(' | b')' | b'[' | b']' | b'{' | b'}')
52}
53
54/// Language config for comment/string-aware bracket matching: which delimiters
55/// open a line comment / string / char literal, so brackets inside them are not
56/// counted. **All constructs are line-local** — the state resets at every `\n`,
57/// so block comments and multi-line strings are deliberately *not* handled (that
58/// would make an edit's cost scale with the document; see the module doc). The
59/// `Default` (empty) config is *no awareness* — every bracket counts, the
60/// original purely-structural behavior, at zero added cost.
61#[derive(Clone, Debug, Default, PartialEq, Eq)]
62pub struct BracketConfig {
63    /// Line-comment markers (e.g. `"//"`, `"#"`). From a marker to end of line is
64    /// a comment.
65    pub line_comments: Vec<String>,
66    /// String delimiters (e.g. `b'"'`). A delimiter opens a string until the same
67    /// delimiter or end of line; `\` escapes the next byte.
68    pub string_delims: Vec<u8>,
69    /// Char-literal delimiter (e.g. `b'\''`), if any. Off by default: in some
70    /// languages `'` is ambiguous (Rust lifetimes), so it is opt-in per language.
71    pub char_delim: Option<u8>,
72}
73
74impl BracketConfig {
75    /// Whether any awareness is configured. When `false`, the bracket scan takes
76    /// the original zero-overhead structural path.
77    #[must_use]
78    pub fn is_active(&self) -> bool {
79        !self.line_comments.is_empty() || !self.string_delims.is_empty() || self.char_delim.is_some()
80    }
81}
82
83/// The lexer state of the [`SkipContext`] adapter — a small unified machine, not a
84/// chain of per-construct passes, because comment/string precedence is positional
85/// (whichever delimiter opens first at a byte wins).
86#[derive(Clone, Copy, PartialEq, Eq)]
87enum Skip {
88    Code,
89    LineComment,
90    /// Inside a string opened by this delimiter byte.
91    Str(u8),
92    /// Inside a char literal opened by this delimiter byte.
93    Char(u8),
94}
95
96/// Annotates a byte stream with "is this byte inside a comment / string / char
97/// literal" — the one owner of skip context, so the bracket matcher shrinks to
98/// `if !skip && is_bracket_byte(b)`. Yields `(offset, byte, skip)`.
99///
100/// **Line-local:** the state resets to `Code` at every `\n`, so the adapter is
101/// restartable at any line boundary with a fresh state — the property that keeps
102/// incremental re-matching O(edit) rather than O(document).
103pub(crate) struct SkipContext<'a> {
104    bytes: &'a [u8],
105    i: usize,
106    cfg: &'a BracketConfig,
107    state: Skip,
108    /// The previous string/char byte was an unescaped `\`.
109    escaped: bool,
110}
111
112impl<'a> SkipContext<'a> {
113    pub(crate) fn new(bytes: &'a [u8], cfg: &'a BracketConfig) -> Self {
114        Self { bytes, i: 0, cfg, state: Skip::Code, escaped: false }
115    }
116
117    /// Advance one byte, updating the state, and return whether *this* byte is in
118    /// a skip context (a bracket here must be ignored).
119    fn step(&mut self, b: u8) -> bool {
120        // A newline ends every line-local construct.
121        if b == b'\n' {
122            let was_skip = self.state != Skip::Code;
123            self.state = Skip::Code;
124            self.escaped = false;
125            return was_skip; // `\n` is never a bracket; the value is moot
126        }
127        match self.state {
128            Skip::LineComment => true,
129            Skip::Str(delim) | Skip::Char(delim) => {
130                if self.escaped {
131                    self.escaped = false;
132                } else if b == b'\\' {
133                    self.escaped = true;
134                } else if b == delim {
135                    self.state = Skip::Code;
136                }
137                true
138            }
139            Skip::Code => {
140                // Guard the (multi-byte) `starts_with` on a first-byte match, so a
141                // non-comment byte pays only one comparison per marker, not a slice
142                // scan — most bytes are not a comment opener.
143                let opens_comment = self.cfg.line_comments.iter().any(|m| {
144                    let mb = m.as_bytes();
145                    mb.first() == Some(&b) && self.bytes[self.i..].starts_with(mb)
146                });
147                if opens_comment {
148                    self.state = Skip::LineComment;
149                    return true;
150                }
151                if self.cfg.string_delims.contains(&b) {
152                    self.state = Skip::Str(b);
153                    return true;
154                }
155                if Some(b) == self.cfg.char_delim {
156                    self.state = Skip::Char(b);
157                    return true;
158                }
159                false
160            }
161        }
162    }
163}
164
165impl Iterator for SkipContext<'_> {
166    /// `(byte offset, byte, is-in-skip-context)`.
167    type Item = (u32, u8, bool);
168
169    fn next(&mut self) -> Option<Self::Item> {
170        let b = *self.bytes.get(self.i)?;
171        let off = self.i as u32;
172        let skip = self.step(b);
173        self.i += 1;
174        Some((off, b, skip))
175    }
176}
177
178/// One bracket occurrence in the document — fully derived on demand (nothing here
179/// is stored; the tree holds only the char and position).
180#[derive(Copy, Clone, PartialEq, Eq, Debug)]
181pub struct Bracket {
182    /// Byte offset of the bracket char.
183    pub offset: u32,
184    /// Whether it is an opener (`([{`) vs a closer (`)]}`).
185    pub open: bool,
186    /// Nesting depth (0 = outermost).
187    pub depth: u32,
188    /// The matched partner's offset, or `None` if unmatched.
189    pub partner: Option<u32>,
190}
191
192/// Every bracket in a document, riding one augmented `SumTree` that is the sole
193/// owner of position. Queries are O(log) descents; an edit is a
194/// `SumTree::replace` of the edited byte range.
195///
196/// `Default` is an EMPTY tree — valid both for the read-only query surface
197/// (movement's fold-free helper pairs it with an empty `FoldSet`) and as a base
198/// for [`Brackets::apply_edit`], since the tree needs no scaffold to seed: the
199/// first edit simply replaces an empty range.
200#[derive(Clone, Debug, Default)]
201pub struct Brackets {
202    tree: SumTree<BracketItem>,
203}
204
205impl Brackets {
206    /// Match every `()`/`[]`/`{}` bracket in `text` (LF-only — the buffer
207    /// contract) — the O(n) from-scratch constructor (document load; the tests'
208    /// oracle). Per-edit maintenance goes through [`Brackets::apply_edit`].
209    #[must_use]
210    pub fn match_text(text: &str) -> Self {
211        Self { tree: bracket_tree::tree_from_text(text) }
212    }
213
214    /// [`Self::match_text`], but skipping brackets inside comments / strings / char
215    /// literals per `cfg`. An inactive (`Default`) config takes the exact
216    /// zero-overhead structural path — the two are byte-identical then.
217    #[must_use]
218    pub fn match_text_with(text: &str, cfg: &BracketConfig) -> Self {
219        if !cfg.is_active() {
220            return Self::match_text(text);
221        }
222        Self { tree: bracket_tree::tree_from_text_with(text, cfg) }
223    }
224
225    /// All brackets, in document order — the O(n) cold path (fold-source scans, the
226    /// capture-folds harness, tests). Hot paths use [`Self::in_range`].
227    #[must_use]
228    pub fn all(&self) -> Vec<Bracket> {
229        bracket_tree::derive_all(&self.tree)
230    }
231
232    /// The bracket at exactly `offset`, if any. O(log).
233    #[must_use]
234    pub fn at(&self, offset: u32) -> Option<Bracket> {
235        bracket_tree::at(&self.tree, offset)
236    }
237
238    /// The matched partner of the OPEN bracket at `offset`, or `None` if it is not a
239    /// live opener or is unmatched — the fold hot-path lookup (fold reconcile,
240    /// expand-on-hidden-edit, block/inline resolution). Unlike [`Self::at`] it never
241    /// resolves `depth`, so it costs O(log + partner fold) with no `ShapeSummary`
242    /// allocation — the difference between a fold-heavy keystroke being O(folds·log)
243    /// and an allocation storm. Callers that only need "is this opener's pair still
244    /// there, and where does it close?" must use this, never `at().partner`.
245    #[must_use]
246    pub fn foldable_partner(&self, offset: u32) -> Option<u32> {
247        bracket_tree::foldable_partner(&self.tree, offset)
248    }
249
250    /// The brackets whose offset lies in `range` (half-open: start inclusive, end
251    /// exclusive), in order, collected — the cold/slice callers (fold-pair
252    /// derivation, tests). An inverted range is empty, not a panic. Per-frame hot
253    /// paths iterate [`Self::in_range_iter`] instead, which allocates nothing.
254    #[must_use]
255    pub fn in_range(&self, range: Range<u32>) -> Vec<Bracket> {
256        self.in_range_iter(range).collect()
257    }
258
259    /// [`Self::in_range`] as a lazy iterator — the per-visible-row colorization
260    /// loop iterates this directly, so a frame's bracket colouring allocates no
261    /// `Vec` (one per visible row × every frame otherwise). An inverted range
262    /// yields nothing (the count bounds cross), matching [`Self::in_range`].
263    pub fn in_range_iter(&self, range: Range<u32>) -> impl Iterator<Item = Bracket> + '_ {
264        bracket_tree::in_range(&self.tree, range.start, range.end)
265    }
266
267    /// The matched bracket pair to highlight for a caret at `caret`, as
268    /// `(bracket_offset, partner_offset)`. The bracket directly *left* of the
269    /// caret is preferred (the one you just typed past); the one directly right
270    /// is the fallback. `None` if the caret isn't adjacent to a *matched* bracket.
271    #[must_use]
272    pub fn active_pair(&self, caret: u32) -> Option<(u32, u32)> {
273        bracket_tree::active_pair(&self.tree, caret)
274    }
275
276    /// The innermost matched pair whose interior contains `caret` (opener offset
277    /// `< caret <=` closer offset), as `(open_offset, close_offset)` — the *active*
278    /// indent guide. `None` if the caret is inside no matched pair.
279    #[must_use]
280    pub fn enclosing_pair(&self, caret: u32) -> Option<(u32, u32)> {
281        bracket_tree::enclosing_pairs(&self.tree, caret).into_iter().next()
282    }
283
284    /// The opener of the innermost pair strictly enclosing `caret`
285    /// (`open < caret <= close`) that satisfies `pred`. `None` if none matches.
286    #[must_use]
287    pub fn innermost_enclosing_where(&self, caret: u32, pred: impl Fn(u32, u32) -> bool) -> Option<u32> {
288        bracket_tree::enclosing_pairs(&self.tree, caret)
289            .into_iter()
290            .find(|&(o, c)| pred(o, c))
291            .map(|(o, _)| o)
292    }
293
294    /// The innermost matched pair whose CONTENTS contain the whole range —
295    /// `open + 1 <= start && end <= close` — as `(open, close)`. The
296    /// expand-selection ladder's next rung. `None` outside every pair.
297    #[must_use]
298    pub fn enclosing_pair_of_range(&self, start: u32, end: u32) -> Option<(u32, u32)> {
299        // The enclosers of `start` are innermost-first (closes widen outward), so
300        // the first whose closer also reaches `end` is the tightest fit.
301        bracket_tree::enclosing_pairs(&self.tree, start).into_iter().find(|&(_, close)| end <= close)
302    }
303
304    /// Every matched pair `(open, close)` with `open <= caret <= close + 1` —
305    /// the pairs enclosing the caret PLUS the ones it touches at either bracket
306    /// (caret AT an opener, or just past a closer) — the fold-at-caret candidate
307    /// set. Unordered; the caller picks the innermost.
308    #[must_use]
309    pub fn enclosing_or_touching(&self, caret: u32) -> Vec<(u32, u32)> {
310        #[cfg(any(test, debug_assertions))]
311        ENCLOSING_WALKS.with(|c| c.set(c.get() + 1));
312        bracket_tree::enclosing_or_touching_pairs(&self.tree, caret)
313    }
314
315    /// Splice this structure through one committed `patch` — the incremental twin
316    /// of [`Brackets::match_text`]. `buffer` is the POST-edit buffer and `patch`
317    /// the committed transaction's old→new byte ranges (`new` in final
318    /// coordinates) — exactly what `rebase_views` holds, on the forward path and on
319    /// every undo/redo step alike.
320    ///
321    /// Returns the reconcile window `[seed_lo, re)` in post-edit coordinates: the
322    /// re-matched span extended left to the outermost opener enclosing the first
323    /// edit (so a fold whose partner the edit deletes is covered), or `0..0` when
324    /// no bracket structure changed. `Document::reconcile_folds` re-checks only the
325    /// folds in that window.
326    ///
327    /// # Why the result deep-equals a from-scratch rebuild
328    ///
329    /// The tree stores only each bracket's char and delta-gap position; `open`,
330    /// `depth`, and `partner` are DERIVED per query from the always-current tree.
331    /// So an edit need only make the stored primary facts correct: rescan the
332    /// edited byte range into brackets, `SumTree::replace` them in, and — because
333    /// the encoding is delta-gap — re-anchor just the FIRST suffix bracket's gap
334    /// (the rest are relative and do not move). No cross-boundary partner pointer
335    /// is stored, so none can be left stale by the splice; the
336    /// incremental≡`match_text` oracle enforces the equivalence.
337    pub fn apply_edit(&mut self, patch: &Patch, buffer: &Buffer, cfg: &BracketConfig) -> Range<u32> {
338        let (tree, region) = bracket_tree::apply_edit(&self.tree, patch, buffer, cfg);
339        self.tree = tree;
340        region
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use crate::coords::Point;
348    use crate::transaction::{apply, EditOp};
349
350    fn bat(b: &Brackets, o: u32) -> Bracket {
351        b.at(o).expect("a bracket at that offset")
352    }
353
354    /// Offsets of the surviving (counted) brackets, in order.
355    fn offs(b: &Brackets) -> Vec<u32> {
356        b.all().iter().map(|x| x.offset).collect()
357    }
358
359    fn cfg() -> BracketConfig {
360        BracketConfig {
361            line_comments: vec!["//".into()],
362            string_delims: vec![b'"'],
363            char_delim: None,
364        }
365    }
366
367    #[test]
368    fn inactive_config_is_identical_to_plain_match() {
369        let text = "(a)//(b)\n\"(\"[]";
370        assert_eq!(
371            Brackets::match_text_with(text, &BracketConfig::default()).all(),
372            Brackets::match_text(text).all(),
373        );
374    }
375
376    #[test]
377    fn skips_brackets_in_line_comments_and_resets_at_newline() {
378        // ( 0  ) 2   // at 4,5   ( 7  ) 9   \n 10   ( 11  ) 13
379        let b = Brackets::match_text_with("(a) // (b)\n(c)", &cfg());
380        assert_eq!(offs(&b), vec![0, 2, 11, 13], "the comment's ( ) at 7,9 are skipped");
381    }
382
383    #[test]
384    fn skips_brackets_in_strings() {
385        // " 0  ( 1  " 2  ( 3   → the ( at 1 is inside the string; the ( at 3 is code
386        let b = Brackets::match_text_with("\"(\"(", &cfg());
387        assert_eq!(offs(&b), vec![3]);
388    }
389
390    #[test]
391    fn precedence_is_positional_not_a_fixed_layer_order() {
392        // `"//"()` — the // is INSIDE the string, so it is not a comment; the ()
393        // after the string is code and counts. A chain of comment-then-string
394        // adapters would wrongly flag the whole line as a comment here.
395        let b = Brackets::match_text_with("\"//\"()", &cfg());
396        assert_eq!(offs(&b), vec![4, 5]);
397    }
398
399    #[test]
400    fn a_backslash_escapes_the_closing_quote() {
401        // "\")"(  — the escaped quote does not close, so ) stays in the string;
402        // the trailing ( is code.  " 0  \ 1  " 2  ) 3  " 4  ( 5
403        let b = Brackets::match_text_with("\"\\\")\"(", &cfg());
404        assert_eq!(offs(&b), vec![5]);
405    }
406
407    #[test]
408    fn char_delim_is_opt_in() {
409        let c = BracketConfig { char_delim: Some(b'\''), ..Default::default() };
410        // '(') — the ( is in a char literal (skipped); the trailing ) is code
411        assert_eq!(offs(&Brackets::match_text_with("'(')", &c)), vec![3]);
412        // Without char_delim, the ( counts (default: ' is not special).
413        assert_eq!(offs(&Brackets::match_text_with("'(')", &BracketConfig::default())), vec![1, 3]);
414    }
415
416    #[test]
417    fn matches_nested_pairs_with_depth() {
418        let b = Brackets::match_text("(a[b]{c})"); // ( [ ] { } ) at 0 2 4 5 7 8
419        assert_eq!((bat(&b, 0).partner, bat(&b, 0).depth), (Some(8), 0));
420        assert_eq!((bat(&b, 2).partner, bat(&b, 2).depth), (Some(4), 1));
421        assert_eq!((bat(&b, 5).partner, bat(&b, 5).depth), (Some(7), 1));
422        assert_eq!((bat(&b, 8).partner, bat(&b, 8).depth), (Some(0), 0));
423    }
424
425    #[test]
426    fn flags_unmatched_brackets() {
427        let b = Brackets::match_text("(]"); // opener never closes, closer has no opener
428        assert_eq!(bat(&b, 0).partner, None);
429        assert_eq!(bat(&b, 1).partner, None);
430    }
431
432    #[test]
433    fn active_pair_prefers_the_bracket_left_of_the_caret() {
434        let b = Brackets::match_text("(){}"); // '(' 0↔1  ')' 1↔0  '{' 2↔3  '}' 3↔2
435        assert_eq!(b.active_pair(0), Some((0, 1))); // no left → right '(' at 0
436        assert_eq!(b.active_pair(1), Some((0, 1))); // left '(' at 0 (preferred over ')')
437        assert_eq!(b.active_pair(2), Some((1, 0))); // left ')' at 1 (preferred over '{')
438        assert_eq!(b.active_pair(3), Some((2, 3))); // left '{' at 2
439        assert_eq!(b.active_pair(4), Some((3, 2))); // left '}' at 3
440        assert_eq!(Brackets::match_text("x").active_pair(1), None); // none adjacent
441    }
442
443    #[test]
444    fn enclosing_pair_is_the_innermost_containing_the_caret() {
445        let b = Brackets::match_text("(a[b]{c})"); // ( [ ] { } ) at 0 2 4 5 7 8
446        assert_eq!(b.enclosing_pair(1), Some((0, 8))); // inside only the outer ()
447        assert_eq!(b.enclosing_pair(3), Some((2, 4))); // inside [] (depth 1) beats ()
448        assert_eq!(b.enclosing_pair(6), Some((5, 7))); // inside {} (depth 1)
449        assert_eq!(b.enclosing_pair(0), None); // at the opener → not yet inside
450        assert_eq!(b.enclosing_pair(9), None); // past the closer
451    }
452
453    /// The enclosing queries must find the pair even when its opener is FAR to the
454    /// left with many sibling pairs in between (the O(brackets)-scan case the tree
455    /// descent replaces), and the touching cases (`caret == open`, `caret ==
456    /// close + 1`). An `enclosing_*` result independent of the sibling count is the
457    /// whole point.
458    #[test]
459    fn enclosing_walk_matches_a_brute_scan_with_siblings() {
460        //           0         1         2
461        //           0123456789012345678901234567
462        let b = Brackets::match_text("([1][2]{a(b)c}[3])xy");
463        // A brute-force oracle: innermost matched pair strictly containing c.
464        let brute_enclosing = |caret: u32| {
465            b.all()
466                .iter()
467                .filter_map(|br| br.partner.map(|p| (br.offset, p)).filter(|_| br.open))
468                .filter(|&(o, c)| o < caret && caret <= c)
469                .min_by_key(|&(o, c)| c - o)
470        };
471        for caret in 0..=20 {
472            assert_eq!(b.enclosing_pair(caret), brute_enclosing(caret), "enclosing_pair({caret})");
473        }
474        // Deep inside the nested `(b)` at offset 10 — enclosed by (9,11), {7,13),
475        // (0,17), reached past the [1] [2] siblings.
476        assert_eq!(b.enclosing_pair(10), Some((9, 11)));
477        // Just after the `}` at 13: caret 14 is back at the outer ( level.
478        assert_eq!(b.enclosing_pair(14), Some((0, 17)));
479        // enclosing_or_touching includes the touched pairs at either bracket.
480        let mut touch = b.enclosing_or_touching(9); // caret ON the `(` at 9
481        touch.sort_unstable();
482        assert!(touch.contains(&(9, 11)), "the pair opening at the caret is touched");
483        assert!(touch.contains(&(7, 13)) && touch.contains(&(0, 17)), "and its enclosers");
484        let after_close = b.enclosing_or_touching(12); // caret just past `)` at 11
485        assert!(after_close.contains(&(9, 11)), "the pair closing at caret-1 is touched");
486        // enclosing_pair_of_range: the tightest pair whose CONTENTS hold [start,end].
487        assert_eq!(b.enclosing_pair_of_range(10, 11), Some((9, 11))); // fits (b)
488        assert_eq!(b.enclosing_pair_of_range(9, 12), Some((7, 13))); // spans (b) → next out
489    }
490
491    #[test]
492    fn active_pair_skips_unmatched_brackets() {
493        let b = Brackets::match_text("(]"); // both unmatched
494        assert_eq!(b.active_pair(1), None); // left '(' unmatched, right ']' unmatched
495        assert_eq!(b.active_pair(2), None);
496    }
497
498    #[test]
499    fn quotes_are_not_brackets() {
500        let b = Brackets::match_text("\"()\""); // quotes ignored; ( ) matched
501        assert_eq!(b.all().len(), 2);
502        assert_eq!(bat(&b, 1).partner, Some(2));
503    }
504
505    #[test]
506    fn in_range_boundaries() {
507        let b = Brackets::match_text("(a[b]{c})"); // ( [ ] { } ) at 0 2 4 5 7 8
508        let offsets = |s: &[Bracket]| s.iter().map(|x| x.offset).collect::<Vec<_>>();
509        assert!(b.in_range(0..0).is_empty()); // empty range
510        assert!(b.in_range(3..3).is_empty());
511        assert_eq!(b.in_range(0..9), b.all()); // full range == all
512        assert_eq!(b.in_range(0..u32::MAX), b.all());
513        assert_eq!(offsets(&b.in_range(2..5)), vec![2, 4]); // start inclusive…
514        assert_eq!(offsets(&b.in_range(2..6)), vec![2, 4, 5]); // …end exclusive
515        assert_eq!(offsets(&b.in_range(8..9)), vec![8]); // exactly the last one
516        assert_eq!(offsets(&b.in_range(1..2)), Vec::<u32>::new()); // gap between brackets
517        #[allow(clippy::reversed_empty_ranges)]
518        let inverted = b.in_range(5..2);
519        assert!(inverted.is_empty()); // inverted range is empty, not a panic
520    }
521
522    // ───────────────────────── incremental engine ─────────────────────────
523
524    /// The oracle assertion: the incrementally-maintained structure must
525    /// deep-equal a from-scratch `match_text` of the same text — every derived
526    /// Bracket field. With no stored line shapes, `all()` (offset, open, depth,
527    /// partner) IS the whole observable state.
528    fn assert_oracle(b: &Brackets, buf: &Buffer, ctx: &str) {
529        let oracle = Brackets::match_text(&buf.text());
530        assert_eq!(b.all(), oracle.all(), "brackets diverged from a scratch rebuild: {ctx}");
531    }
532
533    /// Apply `ops` to the buffer and ride the committed patch through
534    /// `apply_edit` — the exact call shape `rebase_views` will use.
535    fn edit(b: &mut Brackets, buf: &mut Buffer, ops: Vec<EditOp>) -> Range<u32> {
536        let committed = apply(buf, ops).expect("test edits are disjoint and in-bounds");
537        b.apply_edit(committed.patch(), buf, &BracketConfig::default())
538    }
539
540    /// Oracle test: ~500 seeded-random edits (biased toward bracket chars,
541    /// newlines, mid-pair splits, multi-char inserts/deletes and the occasional
542    /// scattered two-edit transaction) over a bracket-heavy document; after every
543    /// commit the incremental structure must deep-equal a from-scratch rebuild.
544    /// This generator exercises every way an edit can re-pair brackets across the
545    /// splice boundary, so any divergence between incremental and scratch matching
546    /// surfaces here.
547    #[test]
548    fn incremental_matches_scratch_after_random_edits() {
549        let seed: u64 = 0x5EED_0BAD_F00D_2026;
550        let mut s = seed;
551        let mut rng = move || {
552            // xorshift64 — deterministic, no dev-dependency.
553            s ^= s << 13;
554            s ^= s >> 7;
555            s ^= s << 17;
556            s
557        };
558        let mut buf = Buffer::new(
559            "fn main() {\n    let v = vec![1, (2), [3]];\n    if (a[i]) { b(c[d]); }\n}\n",
560        )
561        .expect("fixture loads");
562        let mut b = Brackets::match_text(&buf.text());
563        const POOL: &[u8] = b"(){}[](){}[]\n\nab ";
564        for i in 0..500 {
565            let len = buf.len();
566            let kind = rng() % 12;
567            let ops = if kind < 5 {
568                let at = (rng() % (u64::from(len) + 1)) as u32;
569                let n = 1 + (rng() % 4) as usize;
570                let text: String =
571                    (0..n).map(|_| POOL[(rng() % POOL.len() as u64) as usize] as char).collect();
572                vec![EditOp::insert(at, text)]
573            } else if kind < 7 {
574                let a = (rng() % (u64::from(len) + 1)) as u32;
575                let e = (a + 1 + (rng() % 12) as u32).min(len);
576                vec![EditOp::delete(a..e)]
577            } else if kind < 9 {
578                let a = (rng() % (u64::from(len) + 1)) as u32;
579                let e = (a + (rng() % 6) as u32).min(len);
580                let n = 1 + (rng() % 5) as usize;
581                let text: String =
582                    (0..n).map(|_| POOL[(rng() % POOL.len() as u64) as usize] as char).collect();
583                vec![EditOp::new(a..e, text)]
584            } else if kind == 9 {
585                let a = (rng() % (u64::from(len) / 2 + 1)) as u32;
586                let c = (len / 2 + (rng() % (u64::from(len) / 2 + 1)) as u32).min(len);
587                vec![EditOp::insert(a, "("), EditOp::insert(c, ")")]
588            } else {
589                // Scattered structure-neutral inserts across the whole document —
590                // the O(N log) per-edit splice path over a MULTI-edit patch; must
591                // still deep-equal a scratch rebuild.
592                let n = 2 + (rng() % 3) as usize; // 2..=4 carets
593                let mut offs: Vec<u32> =
594                    (0..n).map(|_| (rng() % (u64::from(len) + 1)) as u32).collect();
595                offs.sort_unstable();
596                offs.dedup();
597                offs.into_iter().map(|o| EditOp::insert(o, "z")).collect()
598            };
599            let committed = apply(&mut buf, ops.clone()).expect("generated ops are disjoint");
600            b.apply_edit(committed.patch(), &buf, &BracketConfig::default());
601            let ctx = format!("edit {i} (seed {seed:#x}): {ops:?}");
602            assert_oracle(&b, &buf, &ctx);
603        }
604    }
605
606    /// Oracle test for the comment/string-AWARE incremental path: the same random
607    /// edit stress, but with an active config and a byte pool that toggles
608    /// comment/string state (`/`, `"`, `\`, newlines). After every commit — single
609    /// edits, replacements, and scattered/same-line MULTI-edit patches (the
610    /// line-aligned region merge) — the incremental `apply_edit` must deep-equal a
611    /// from-scratch `match_text_with`. This is the correctness proof for the
612    /// line-aligned splice.
613    #[test]
614    fn incremental_matches_scratch_with_comment_awareness() {
615        let cfg = BracketConfig {
616            line_comments: vec!["//".into()],
617            string_delims: vec![b'"'],
618            char_delim: None,
619        };
620        let seed: u64 = 0xC0FF_EE00_2026_1234;
621        let mut s = seed;
622        let mut rng = move || {
623            s ^= s << 13;
624            s ^= s >> 7;
625            s ^= s << 17;
626            s
627        };
628        let mut buf = Buffer::new("fn f() { // (a)\n    let s = \"([{\";\n    g([1]);\n}\n")
629            .expect("fixture loads");
630        let mut b = Brackets::match_text_with(&buf.text(), &cfg);
631        // Biased toward bracket chars, comment/string delimiters, escapes, newlines.
632        const POOL: &[u8] = b"(){}[]//\"\"\\\n ab";
633        for i in 0..600 {
634            let len = buf.len();
635            let kind = rng() % 12;
636            let ops = if kind < 5 {
637                let at = (rng() % (u64::from(len) + 1)) as u32;
638                let n = 1 + (rng() % 4) as usize;
639                let text: String =
640                    (0..n).map(|_| POOL[(rng() % POOL.len() as u64) as usize] as char).collect();
641                vec![EditOp::insert(at, text)]
642            } else if kind < 7 {
643                let a = (rng() % (u64::from(len) + 1)) as u32;
644                let e = (a + 1 + (rng() % 10) as u32).min(len);
645                vec![EditOp::delete(a..e)]
646            } else if kind < 9 {
647                let a = (rng() % (u64::from(len) + 1)) as u32;
648                let e = (a + (rng() % 6) as u32).min(len);
649                let n = 1 + (rng() % 5) as usize;
650                let text: String =
651                    (0..n).map(|_| POOL[(rng() % POOL.len() as u64) as usize] as char).collect();
652                vec![EditOp::new(a..e, text)]
653            } else {
654                // Scattered/same-line multi-caret inserts — exercises the
655                // line-aligned region merge across a multi-edit patch.
656                let n = 2 + (rng() % 3) as usize;
657                let mut offs: Vec<u32> =
658                    (0..n).map(|_| (rng() % (u64::from(len) + 1)) as u32).collect();
659                offs.sort_unstable();
660                offs.dedup();
661                offs.into_iter()
662                    .map(|o| {
663                        let byte = POOL[(rng() % POOL.len() as u64) as usize];
664                        EditOp::insert(o, (byte as char).to_string())
665                    })
666                    .collect()
667            };
668            let committed = apply(&mut buf, ops.clone()).expect("generated ops are disjoint");
669            b.apply_edit(committed.patch(), &buf, &cfg);
670            let oracle = Brackets::match_text_with(&buf.text(), &cfg);
671            assert_eq!(
672                b.all(),
673                oracle.all(),
674                "aware brackets diverged from a scratch rebuild at edit {i} (seed {seed:#x}): {ops:?}",
675            );
676        }
677    }
678
679    // ── re-pairing cases: an edit changes which brackets pair with which. With
680    //    partners DERIVED from the always-current tree, each is just an ordinary
681    //    edit — the oracle confirms the derived partners follow the text ──
682
683    #[test]
684    fn prefix_opener_repoints_into_region() {
685        // "{\nx\n}", line 1 → "}": the prefix `{` re-points to the NEW line-1 closer,
686        // and the old line-2 `}` goes unmatched.
687        let mut buf = Buffer::new("{\nx\n}").expect("fixture loads");
688        let mut b = Brackets::match_text(&buf.text());
689        edit(&mut b, &mut buf, vec![EditOp::new(2..3, "}")]); // "{\n}\n}"
690        assert_oracle(&b, &buf, "line 1 x → }");
691        assert_eq!(bat(&b, 0).partner, Some(2));
692        assert_eq!(bat(&b, 2).partner, Some(0));
693        assert_eq!((bat(&b, 4).partner, bat(&b, 4).depth), (None, 0));
694    }
695
696    #[test]
697    fn converged_shape_different_identity() {
698        // "{\nx\n}", line 1 → "}{": the shape after line 1 is again `{`, but the
699        // identity behind it changed — the final `}` pairs the NEW region `{` at 3,
700        // and the prefix `{` re-points to the new line-1 `}`.
701        let mut buf = Buffer::new("{\nx\n}").expect("fixture loads");
702        let mut b = Brackets::match_text(&buf.text());
703        edit(&mut b, &mut buf, vec![EditOp::new(2..3, "}{")]); // "{\n}{\n}"
704        assert_oracle(&b, &buf, "line 1 x → }{");
705        assert_eq!(bat(&b, 0).partner, Some(2));
706        assert_eq!(bat(&b, 3).partner, Some(5));
707        assert_eq!(bat(&b, 5).partner, Some(3));
708    }
709
710    #[test]
711    fn converged_shape_crosses_into_the_suffix() {
712        // Same identity swap, but with a clean line between the edit and the final
713        // `}` — the `}` is a true suffix entry that must re-pair the region opener.
714        let mut buf = Buffer::new("{\nx\nq\n}").expect("fixture loads");
715        let mut b = Brackets::match_text(&buf.text());
716        edit(&mut b, &mut buf, vec![EditOp::new(2..3, "}{")]); // "{\n}{\nq\n}"
717        assert_oracle(&b, &buf, "line 1 x → }{ with a clean row before the closer");
718        assert_eq!(bat(&b, 0).partner, Some(2));
719        assert_eq!(bat(&b, 3).partner, Some(7));
720        assert_eq!(bat(&b, 7).partner, Some(3));
721    }
722
723    #[test]
724    fn crossing_repairs_mixed_seed_and_region_entries() {
725        // "{\n(\nb\n)}", line 1 → ")(": `)` pairs the region opener, `}` pairs the
726        // seed `{`, and the new line-1 `)` stays unmatched.
727        let mut buf = Buffer::new("{\n(\nb\n)}").expect("fixture loads");
728        let mut b = Brackets::match_text(&buf.text());
729        edit(&mut b, &mut buf, vec![EditOp::new(2..3, ")(")]); // "{\n)(\nb\n)}"
730        assert_oracle(&b, &buf, "mixed carried stack");
731        assert_eq!((bat(&b, 2).partner, bat(&b, 2).depth), (None, 0)); // unmatched )
732        assert_eq!(bat(&b, 3).partner, Some(7)); // region ( ↔ suffix )
733        assert_eq!(bat(&b, 7).partner, Some(3));
734        assert_eq!(bat(&b, 0).partner, Some(8)); // seed { ↔ suffix }
735        assert_eq!(bat(&b, 8).partner, Some(0));
736    }
737
738    #[test]
739    fn pair_spanning_edit_shifts_the_closer() {
740        // An edit strictly inside a multi-line pair's interior (no bracket chars
741        // touched): the closer's offset shifts by delta and the opener's DERIVED
742        // partner follows automatically (nothing stored to leave stale).
743        let mut buf = Buffer::new("{\naaa\nzzz\n}").expect("fixture loads");
744        let mut b = Brackets::match_text(&buf.text());
745        edit(&mut b, &mut buf, vec![EditOp::new(2..5, "aaaaa")]); // grow "aaa"→"aaaaa"
746        assert_oracle(&b, &buf, "interior growth inside a multi-line pair");
747        assert_eq!(bat(&b, 0).partner, Some(12)); // was Some(10)
748        assert_eq!(bat(&b, 12).partner, Some(0)); // the closer, shifted
749    }
750
751    #[test]
752    fn seed_leftover_cleared_to_none() {
753        // Delete a prefix opener's closer: the opener is now unmatched — with a
754        // derived partner there is no stale pointer to clear.
755        let mut buf = Buffer::new("{\n}").expect("fixture loads");
756        let mut b = Brackets::match_text(&buf.text());
757        edit(&mut b, &mut buf, vec![EditOp::delete(2..3)]); // "{\n"
758        assert_oracle(&b, &buf, "closer deleted");
759        assert_eq!(b.all().len(), 1);
760        assert_eq!((bat(&b, 0).partner, bat(&b, 0).open), (None, true));
761    }
762
763    // ── edits at scale: correctness deep in a large document (the incremental
764    //    splice is local by construction; the oracle proves it stays correct) ──
765
766    #[test]
767    fn keystroke_deep_in_2000_lines() {
768        let doc = vec!["fn f() { g(a[i], (b)); }"; 2000].join("\n");
769        let mut buf = Buffer::new(&doc).expect("fixture loads");
770        let mut b = Brackets::match_text(&buf.text());
771        let target = buf.point_to_offset(Point::new(1000, 3));
772        edit(&mut b, &mut buf, vec![EditOp::insert(target, "x")]);
773        assert_oracle(&b, &buf, "keystroke at line 1000");
774    }
775
776    #[test]
777    fn balanced_pair_insert_deep_in_a_document() {
778        let doc = vec!["fn f() { g(a[i], (b)); }"; 2000].join("\n");
779        let mut buf = Buffer::new(&doc).expect("fixture loads");
780        let mut b = Brackets::match_text(&buf.text());
781        let target = buf.point_to_offset(Point::new(1000, 8));
782        edit(&mut b, &mut buf, vec![EditOp::insert(target, "()")]);
783        assert_oracle(&b, &buf, "balanced pair at line 1000");
784    }
785
786    #[test]
787    fn structural_edit_after_many_sibling_blocks() {
788        // A newline at depth 0 after N closed sibling blocks — the seed reconcile
789        // walks nothing (enclosing is an O(log) descent), but the result must still
790        // be oracle-correct.
791        const BLOCKS: usize = 20;
792        let mut text = String::new();
793        for _ in 0..BLOCKS {
794            text.push_str("{[[[[[[[[[[]]]]]]]]]]}\n");
795        }
796        let tail = text.len() as u32;
797        text.push_str("tail");
798        let mut buf = Buffer::new(&text).expect("fixture loads");
799        let mut b = Brackets::match_text(&buf.text());
800        edit(&mut b, &mut buf, vec![EditOp::insert(tail, "\n")]);
801        assert_oracle(&b, &buf, "newline after N closed sibling blocks");
802    }
803
804    #[test]
805    fn structure_neutral_insert_at_document_start() {
806        let mut lines = vec!["alpha", "beta"]; // two bracket-free head rows
807        lines.extend(std::iter::repeat_n("x(y[z]) {w}", 1998));
808        let doc = lines.join("\n");
809        let mut buf = Buffer::new(&doc).expect("fixture loads");
810        let mut b = Brackets::match_text(&buf.text());
811        let region = edit(&mut b, &mut buf, vec![EditOp::insert(0, "q")]);
812        assert_eq!(region, 0..0, "a structure-neutral insert reconciles nothing");
813        assert_oracle(&b, &buf, "letter at offset 0");
814    }
815
816    #[test]
817    fn scattered_multicursor_typing_is_structure_neutral() {
818        // THE document-scale multi-cursor case: a plain-letter keystroke at N
819        // cursors spread top-to-bottom is per-edit O(log) (the delta-gap suffix does
820        // not move) and reconciles nothing.
821        let doc = vec!["fn f() { g(a[i], (b)); }"; 500].join("\n");
822        let mut buf = Buffer::new(&doc).expect("fixture loads");
823        let mut b = Brackets::match_text(&buf.text());
824        let inserts: Vec<EditOp> = (0..500)
825            .step_by(50)
826            .map(|row| EditOp::insert(buf.point_to_offset(Point::new(row, 0)), "x"))
827            .collect();
828        let region = edit(&mut b, &mut buf, inserts);
829        assert_eq!(region, 0..0, "scattered structure-neutral edits reconcile nothing");
830        assert_oracle(&b, &buf, "scattered multi-cursor letter inserts");
831    }
832
833    #[test]
834    fn unbalanced_opener_at_the_top() {
835        // An unbalanced opener changes every downstream depth — but depth is derived,
836        // so the tree just gains one bracket. Oracle-equal, and the opener is
837        // unmatched.
838        let doc = vec!["(x)"; 100].join("\n");
839        let mut buf = Buffer::new(&doc).expect("fixture loads");
840        let mut b = Brackets::match_text(&buf.text());
841        edit(&mut b, &mut buf, vec![EditOp::insert(0, "{")]);
842        assert_oracle(&b, &buf, "unbalanced opener at the top");
843        assert_eq!((bat(&b, 0).partner, bat(&b, 0).open), (None, true));
844    }
845
846    // ─────────────────────────────── edges ───────────────────────────────
847
848    #[test]
849    fn empty_document_first_insert_and_full_delete() {
850        let mut buf = Buffer::new("").expect("empty loads");
851        let mut b = Brackets::match_text(&buf.text());
852        // An empty patch is a no-op with an empty reconcile window.
853        assert_eq!(b.apply_edit(&Patch::new(), &buf, &BracketConfig::default()), 0..0);
854        edit(&mut b, &mut buf, vec![EditOp::insert(0, "({\n[")]);
855        assert_oracle(&b, &buf, "first insert into an empty document");
856        let len = buf.len();
857        edit(&mut b, &mut buf, vec![EditOp::delete(0..len)]);
858        assert_oracle(&b, &buf, "delete everything");
859        assert!(b.all().is_empty());
860    }
861
862    #[test]
863    fn edit_at_eof_appends() {
864        let mut buf = Buffer::new("(a\n[b").expect("fixture loads");
865        let mut b = Brackets::match_text(&buf.text());
866        let len = buf.len();
867        edit(&mut b, &mut buf, vec![EditOp::insert(len, "])")]); // "(a\n[b])"
868        assert_oracle(&b, &buf, "append at EOF");
869        assert_eq!(bat(&b, 0).partner, Some(6)); // ( … )
870        assert_eq!(bat(&b, 3).partner, Some(5)); // [ … ]
871    }
872
873    #[test]
874    fn whole_document_replace() {
875        let mut buf = Buffer::new("(a)\n[b]\n{c}").expect("fixture loads");
876        let mut b = Brackets::match_text(&buf.text());
877        let len = buf.len();
878        edit(&mut b, &mut buf, vec![EditOp::new(0..len, "{new\n(doc)\n}")]);
879        assert_oracle(&b, &buf, "whole-document replace");
880        let len = buf.len();
881        edit(&mut b, &mut buf, vec![EditOp::new(0..len, "[]")]);
882        assert_oracle(&b, &buf, "whole-document replace, fewer lines");
883    }
884
885    #[test]
886    fn edit_ending_exactly_on_a_line_boundary() {
887        let mut buf = Buffer::new("(a)\n[b]\n{c}").expect("fixture loads");
888        let mut b = Brackets::match_text(&buf.text());
889        let target = buf.point_to_offset(Point::new(1, 0));
890        edit(&mut b, &mut buf, vec![EditOp::insert(target, "(q)\n")]);
891        assert_oracle(&b, &buf, "insert ending with a newline");
892        let a = buf.point_to_offset(Point::new(1, 0));
893        let e = buf.point_to_offset(Point::new(2, 0));
894        edit(&mut b, &mut buf, vec![EditOp::delete(a..e)]);
895        assert_oracle(&b, &buf, "delete ending at a line start");
896    }
897
898    #[test]
899    fn multi_edit_transaction_with_scattered_inserts() {
900        // Two scattered inserts in ONE apply(): the pair they form must resolve.
901        let mut buf = Buffer::new("aaa\nbbb\nccc\nddd\neee").expect("fixture loads");
902        let mut b = Brackets::match_text(&buf.text());
903        let p1 = buf.point_to_offset(Point::new(0, 1));
904        let p2 = buf.point_to_offset(Point::new(4, 1));
905        edit(&mut b, &mut buf, vec![EditOp::insert(p1, "("), EditOp::insert(p2, ")")]);
906        assert_oracle(&b, &buf, "scattered two-insert transaction");
907        assert_eq!(bat(&b, 1).partner, Some(18)); // "a(aa\n…\ne)ee"
908    }
909}