Skip to main content

arity_parser/parser/
reparse.rs

1//! Incremental reparse: turn a single text edit plus the previous parse tree
2//! into a new tree without re-lexing and re-parsing the whole file.
3//!
4//! Modelled on rust-analyzer's `reparsing.rs`: try the cheapest strategy first
5//! and fall back to progressively more work.
6//!
7//! 1. [`reparse_token`] — the edit lands inside one leaf token (identifier,
8//!    string, comment, whitespace) and re-lexing that token's text yields the
9//!    same single token kind. Splice a fresh leaf in place.
10//! 2. [`reparse_block`] — the edit lands strictly inside a `{ … }` block. Re-lex
11//!    and re-parse just that block (it is self-contained — braces delimit it, so
12//!    its subtree never depends on the surrounding context) and splice the new
13//!    block subtree in place.
14//! 3. [`reparse_toplevel`] — the edit lands inside a single top-level statement
15//!    (a direct child of the `ROOT` node) that isn't inside any `{ … }`. Unlike a
16//!    block, a bare statement has no delimiter tokens, so its boundary is pinned
17//!    by two guards: a *consume-all* check (the reparse must consume every token
18//!    of the statement text — this rules out the statement shrinking and releasing
19//!    trailing tokens) and a *forward-merge* guard (re-parsing the statement with
20//!    the next sibling appended must still end at the statement's original length
21//!    — this rules out the statement growing to absorb what follows, e.g. a
22//!    trailing binary operator). The backward direction is inherently safe: R only
23//!    continues a statement across a newline via a *trailing* operator, never a
24//!    *leading* one, and the previous sibling's tokens are untouched by the edit.
25//! 4. Otherwise return `None`; the caller does a full [`crate::parser::parse`].
26//!
27//! **Correctness invariant (Tenet 4):** a successful reparse must yield a green
28//! tree *and* diagnostics byte-identical to a full parse of the edited text.
29//! Splicing relies on rowan green nodes being position-independent. The invariant
30//! is enforced by `tests/incremental_reparse.rs` (an oracle property test over the
31//! corpus) and a `debug_assert!` here.
32
33use std::borrow::Cow;
34use std::ops::Range;
35
36use rowan::{GreenNode, GreenToken, TextRange, TextSize};
37
38use crate::parser::bracket_balancer::rebalance_brackets;
39use crate::parser::core::ParseOptions;
40use crate::parser::diagnostics::ParseDiagnostic;
41use crate::parser::expr::parse_expr;
42use crate::parser::lexer::lex_with_md;
43use crate::parser::tree_builder::{build_tree, syntax_kind_for};
44use crate::syntax::{SyntaxKind, SyntaxNode};
45
46/// A single contiguous text edit: replace `range` (a byte range in the *old*
47/// text) with `insert`.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Edit {
50    pub range: Range<usize>,
51    pub insert: String,
52}
53
54impl Edit {
55    /// The signed length change this edit applies to text after `range`.
56    fn delta(&self) -> isize {
57        self.insert.len() as isize - (self.range.end - self.range.start) as isize
58    }
59
60    /// Apply the edit to `old`, producing the new text.
61    ///
62    /// Panics when `range` is not a slice `old` can take. Callers holding an
63    /// edit they did not compute themselves want [`Edit::produces`] or
64    /// [`edits_produce`] instead, which decide the same question without
65    /// building the result and without panicking.
66    pub fn apply(&self, old: &str) -> String {
67        let mut out =
68            String::with_capacity(old.len().saturating_sub(self.range.len()) + self.insert.len());
69        out.push_str(&old[..self.range.start]);
70        out.push_str(&self.insert);
71        out.push_str(&old[self.range.end..]);
72        out
73    }
74
75    /// Whether `range` is a slice `old` can actually take.
76    fn fits(&self, old: &str) -> bool {
77        self.range.start <= self.range.end
78            && self.range.end <= old.len()
79            && old.is_char_boundary(self.range.start)
80            && old.is_char_boundary(self.range.end)
81    }
82
83    /// [`Edit::apply`] for an untrusted range: `None` wherever `apply` would
84    /// panic.
85    fn apply_checked(&self, old: &str) -> Option<String> {
86        self.fits(old).then(|| self.apply(old))
87    }
88
89    /// Whether applying this edit to `old` yields exactly `target` — decided by
90    /// comparing the three spans in place, so neither the result nor a copy of
91    /// `old` is ever built.
92    ///
93    /// This is the cheap form of `edit.apply(old) == target`: one allocation and
94    /// one whole-document copy less, on a path a keystroke runs. It is also
95    /// *total* where `apply` panics — an out-of-bounds or mid-character range
96    /// answers `false` — which is what lets an untrusted edit be checked before
97    /// it reaches a reparse strategy.
98    fn produces(&self, old: &str, target: &str) -> bool {
99        if !self.fits(old) {
100            return false;
101        }
102        let (start, end) = (self.range.start, self.range.end);
103        let insert = self.insert.as_bytes();
104        let (old, target) = (old.as_bytes(), target.as_bytes());
105
106        // The length is fixed by the edit, so it discriminates before any
107        // comparison touches the document.
108        if target.len() != old.len() - (end - start) + insert.len() {
109            return false;
110        }
111        // `target.len() - tail == old.len() - end`, so both tails are in bounds.
112        let tail = start + insert.len();
113        old[..start] == target[..start]
114            && target[start..tail] == *insert
115            && old[end..] == target[tail..]
116    }
117}
118
119/// Apply `edits` to `old` left-to-right, each expressed against the text its
120/// predecessors produced — the inverse operation to [`map_range_through_edits`].
121///
122/// Panics on an edit that does not fit the text its predecessors produced. To
123/// *verify* a slice rather than materialize it — the reason a span-mapping
124/// consumer reaches for this — use [`edits_produce`], which is total and skips
125/// the final rebuild.
126pub fn apply_edits(old: &str, edits: &[Edit]) -> String {
127    edits.iter().fold(old.to_string(), |t, e| e.apply(&t))
128}
129
130/// Whether applying `edits` to `old` left-to-right yields exactly `target`:
131/// `apply_edits(old, edits) == target` without the final rebuild, and without
132/// the panic on a slice that does not fit.
133///
134/// This is the apply-and-verify guard span-mapping consumers want. Proving an
135/// accumulated slice is the exact transform between an old snapshot and the
136/// current text is what licenses folding a range through it; a stale or
137/// misaligned slice is rejected here in favor of the whole-text [`diff_edit`]
138/// fallback. The last edit is answered by comparison alone, so the common
139/// one-element slice allocates nothing at all.
140pub fn edits_produce(old: &str, edits: &[Edit], target: &str) -> bool {
141    let Some((last, init)) = edits.split_last() else {
142        return old == target;
143    };
144    let mut text = Cow::Borrowed(old);
145    for edit in init {
146        let Some(next) = edit.apply_checked(&text) else {
147            return false;
148        };
149        text = Cow::Owned(next);
150    }
151    last.produces(&text, target)
152}
153
154/// Map a `TextRange` taken against the text *before* `edit` to its position in
155/// the text *after* `edit`.
156///
157/// Returns `None` when the edit's replaced range overlaps the node's interior:
158/// a `(kind, range)` handle cannot promise the node survived an edit inside it
159/// (its text, and possibly its kind, may have changed). Comparisons are
160/// half-open, so the boundary cases are deterministic: an edit ending exactly at
161/// the node's start shifts the node (it survives), an edit starting exactly at
162/// the node's end leaves it unchanged (it survives), and an insertion strictly
163/// inside the node returns `None`. A zero-length node sitting at an insertion
164/// point is treated as preceding the insertion (unchanged).
165///
166/// Note: the `arity` crate's `incremental::parsed_document` recovers a
167/// single spanning [`diff_edit`], so several disjoint keystrokes coalesce into
168/// one wide edit. That only widens the overlap and yields `None` more often
169/// (more position-based re-resolution by the caller) — it never produces a wrong
170/// mapping. Feeding precise per-change LSP edits would shrink the edit and
171/// reduce those fallbacks.
172pub fn map_range_through_edit(range: TextRange, edit: &Edit) -> Option<TextRange> {
173    let node_start = usize::from(range.start());
174    let node_end = usize::from(range.end());
175
176    if node_end <= edit.range.start {
177        // Node entirely before the edit: untouched.
178        Some(range)
179    } else if node_start >= edit.range.end {
180        // Node entirely after the edit: every offset shifts by the length delta.
181        let start = (node_start as isize + edit.delta()) as usize;
182        let end = (node_end as isize + edit.delta()) as usize;
183        Some(text_range(start, end))
184    } else {
185        // The edit's replaced range intersects the node's interior: invalidated.
186        None
187    }
188}
189
190/// Map a range through a sequence of edits applied left-to-right (the order
191/// [`Edit::apply`] would run them — each edit is expressed against the text
192/// produced by its predecessors). Any single invalidating edit collapses the
193/// whole result to `None`.
194pub fn map_range_through_edits(range: TextRange, edits: &[Edit]) -> Option<TextRange> {
195    edits.iter().try_fold(range, map_range_through_edit)
196}
197
198/// Which strategy produced a [`Reparsed`]. Surfaced for tests and benchmarks.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum ReparseKind {
201    Token,
202    Block,
203    TopLevel,
204    /// A chained multi-edit reparse ([`reparse_edits`]): several single-edit
205    /// strategies applied in sequence. The final tree is still byte-identical to
206    /// a full parse.
207    Multi,
208}
209
210/// The result of a successful incremental reparse: the new whole-file green tree
211/// and its parse diagnostics (absolute offsets in the new text).
212#[derive(Debug, Clone)]
213pub struct Reparsed {
214    pub green: GreenNode,
215    pub diagnostics: Vec<ParseDiagnostic>,
216    pub kind: ReparseKind,
217}
218
219/// Recover a single contiguous [`Edit`] (deleted range in `old` + inserted text)
220/// from a pair of full texts by stripping the common prefix and suffix. Multiple
221/// disjoint edits collapse into one spanning edit — still a correct transform,
222/// just coarser. Boundaries are clamped to char boundaries.
223pub fn diff_edit(old: &str, new: &str) -> Edit {
224    let ob = old.as_bytes();
225    let nb = new.as_bytes();
226
227    let mut prefix = 0;
228    let max_prefix = ob.len().min(nb.len());
229    while prefix < max_prefix && ob[prefix] == nb[prefix] {
230        prefix += 1;
231    }
232    while prefix > 0 && !old.is_char_boundary(prefix) {
233        prefix -= 1;
234    }
235
236    let mut suffix = 0;
237    let max_suffix = (ob.len() - prefix).min(nb.len() - prefix);
238    while suffix < max_suffix && ob[ob.len() - 1 - suffix] == nb[nb.len() - 1 - suffix] {
239        suffix += 1;
240    }
241    while suffix > 0
242        && (!old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix))
243    {
244        suffix -= 1;
245    }
246
247    Edit {
248        range: prefix..(old.len() - suffix),
249        insert: new[prefix..(new.len() - suffix)].to_string(),
250    }
251}
252
253/// Attempt an incremental reparse of `old_root` (parsed from `old_text`, with
254/// `old_diags`) under `edit`. Returns `None` when no incremental strategy applies
255/// — the caller must then do a full parse. An `edit` whose range `old_text`
256/// cannot take is one such case, not a panic.
257pub fn reparse(
258    old_root: &SyntaxNode,
259    old_text: &str,
260    old_diags: &[ParseDiagnostic],
261    edit: &Edit,
262) -> Option<Reparsed> {
263    reparse_with_options(
264        old_root,
265        old_text,
266        old_diags,
267        edit,
268        &ParseOptions::default(),
269    )
270}
271
272/// [`reparse`] with the [`ParseOptions`] the old tree was parsed under. A tree
273/// parsed via [`parse_with_options`](crate::parser::core::parse_with_options)
274/// must be reparsed with the **same** options, or the correctness invariant
275/// (reparse output ≡ full parse of the edited text) breaks for roxygen blocks
276/// resolving their markdown mode from the default.
277pub fn reparse_with_options(
278    old_root: &SyntaxNode,
279    old_text: &str,
280    old_diags: &[ParseDiagnostic],
281    edit: &Edit,
282    options: &ParseOptions,
283) -> Option<Reparsed> {
284    // The strategies below index `old_text` and the tree by the edit's range, so
285    // a range the text cannot take would panic several frames down. An `Edit` is
286    // caller data — a language server's staged `didChange` sequence, say — so
287    // answer "no strategy applies" instead and let the caller fall back.
288    if !edit.fits(old_text) {
289        return None;
290    }
291
292    let md = options.roxygen_markdown_default;
293    let result = reparse_token(old_root, old_text, old_diags, edit, md)
294        .or_else(|| reparse_block(old_root, old_text, old_diags, edit, md))
295        .or_else(|| reparse_toplevel(old_root, old_text, old_diags, edit, md));
296
297    if let Some(reparsed) = &result {
298        debug_assert_eq!(
299            SyntaxNode::new_root(reparsed.green.clone())
300                .text()
301                .to_string(),
302            edit.apply(old_text),
303            "incremental reparse ({:?}) is not lossless",
304            reparsed.kind,
305        );
306    }
307    result
308}
309
310/// Chain the single-edit [`reparse`] over a sequence of `edits` (each expressed
311/// against the text its predecessors produced, in the order [`Edit::apply`]
312/// would run them). Each step reparses off the previous step's whole-file tree,
313/// so N disjoint edits each reparse their own tight region instead of collapsing
314/// into one wide [`diff_edit`] span (the whole point of feeding precise LSP
315/// edits through — multi-cursor and find-replace).
316///
317/// Returns `None` unless *every* step is incrementally reparseable **and** the
318/// fully-applied text equals `target`. The `== target` check is the Tenet-4
319/// safety net: an edit sequence that does not actually transform `old_text` into
320/// the current buffer (a coalescing gap, an interleaved disk write, a
321/// full-document replacement, a range the text cannot even hold) is rejected
322/// here so the caller falls back to the always-correct [`diff_edit`] path. A
323/// successful result is therefore byte-identical to a full
324/// [`parse`](crate::parser::parse) of `target` — the composition of correct
325/// single-edit reparses over the true edit sequence.
326pub fn reparse_edits(
327    old_root: &SyntaxNode,
328    old_text: &str,
329    old_diags: &[ParseDiagnostic],
330    edits: &[Edit],
331    target: &str,
332) -> Option<Reparsed> {
333    reparse_edits_with_options(
334        old_root,
335        old_text,
336        old_diags,
337        edits,
338        target,
339        &ParseOptions::default(),
340    )
341}
342
343/// [`reparse_edits`] with the [`ParseOptions`] the old tree was parsed under
344/// (see [`reparse_with_options`]).
345pub fn reparse_edits_with_options(
346    old_root: &SyntaxNode,
347    old_text: &str,
348    old_diags: &[ParseDiagnostic],
349    edits: &[Edit],
350    target: &str,
351    options: &ParseOptions,
352) -> Option<Reparsed> {
353    // Split the *last* edit off rather than the first: it is the one whose
354    // result is `target`, so it can be verified by comparison instead of by
355    // rebuilding the document. A one-edit chain — a single keystroke, the common
356    // case — therefore allocates nothing here at all.
357    let (last, init) = edits.split_last()?;
358
359    // Each step reparses off the prior step's tree; the first off the caller's.
360    let mut root = old_root.clone();
361    let mut text = Cow::Borrowed(old_text);
362    let mut diags = Cow::Borrowed(old_diags);
363    for edit in init {
364        let reparsed = reparse_with_options(&root, &text, &diags, edit, options)?;
365        // Infallible once the reparse succeeded, which implies the fit.
366        text = Cow::Owned(edit.apply_checked(&text)?);
367        root = SyntaxNode::new_root(reparsed.green);
368        diags = Cow::Owned(reparsed.diagnostics);
369    }
370
371    // Tenet-4 guard, ahead of the work it gates: `init` has already been
372    // applied, so this settles the whole chain, and a stale sequence is rejected
373    // before it can spend a reparse. It also stands in for the bounds the
374    // strategies below assume — an edit that cannot produce `target` never
375    // reaches a slice that would panic on it.
376    if !last.produces(&text, target) {
377        return None;
378    }
379
380    let mut reparsed = reparse_with_options(&root, &text, &diags, last, options)?;
381    reparsed.kind = ReparseKind::Multi;
382    Some(reparsed)
383}
384
385const TOKEN_REPARSE_KINDS: &[SyntaxKind] = &[
386    SyntaxKind::IDENT,
387    SyntaxKind::STRING,
388    SyntaxKind::COMMENT,
389    SyntaxKind::WHITESPACE,
390    // Roxygen leaves are deliberately absent: they only arise from the `#'`-line
391    // lexer path, so relexing one in isolation (`lex("A number.")`) never yields
392    // a roxygen token and the single-token guard would always bail. Edits inside
393    // a roxygen line therefore fall back to block reparse (when the block sits in
394    // a `{}` body) or a full reparse — correct, just not token-incremental.
395];
396
397fn reparse_token(
398    old_root: &SyntaxNode,
399    old_text: &str,
400    old_diags: &[ParseDiagnostic],
401    edit: &Edit,
402    md_default: bool,
403) -> Option<Reparsed> {
404    let (s, e) = (edit.range.start, edit.range.end);
405    let elem = old_root.covering_element(text_range(s, e));
406    let token = elem.into_token()?;
407    if !TOKEN_REPARSE_KINDS.contains(&token.kind()) {
408        return None;
409    }
410
411    let tr = token.text_range();
412    let (t0, t1) = (usize::from(tr.start()), usize::from(tr.end()));
413    // A pure insertion must land strictly inside the token, so it is
414    // unambiguous which token it extends. Edits with a non-empty range that
415    // span a token boundary never reach here — `covering_element` returns the
416    // parent node, not a token.
417    if s == e && !(t0 < s && s < t1) {
418        return None;
419    }
420
421    // The token's new text, with the edit applied at its relative offset.
422    let mut new_text = token.text().to_string();
423    new_text.replace_range((s - t0)..(e - t0), &edit.insert);
424
425    // Re-lex in isolation: it must still be exactly one token of the same kind.
426    let relexed = lex_with_md(&new_text, md_default);
427    let [only] = relexed.as_slice() else {
428        return None;
429    };
430    if only.start != 0 || only.end != new_text.len() || syntax_kind_for(&only.kind) != token.kind()
431    {
432        return None;
433    }
434
435    // Merge guard: appending the following source character must not extend the
436    // token (which would mean the edit merged it with its neighbor, e.g.
437    // `for`→`fore`, or an unterminated string swallowing the next line).
438    if let Some(next_char) = old_text[t1..].chars().next() {
439        let mut probe = new_text.clone();
440        probe.push(next_char);
441        let probe_toks = lex_with_md(&probe, md_default);
442        let first = probe_toks.first()?;
443        if first.end != new_text.len() || syntax_kind_for(&first.kind) != token.kind() {
444            return None;
445        }
446    }
447
448    // Keep diagnostic remapping trivial: only proceed when no diagnostic touches
449    // the edited token. A clean single-token relex introduces none of its own.
450    if old_diags.iter().any(|d| d.start < t1 && d.end > t0) {
451        return None;
452    }
453    let delta = edit.delta();
454    let diagnostics = old_diags
455        .iter()
456        .map(|d| {
457            if d.start >= t1 {
458                shift(d, delta)
459            } else {
460                d.clone()
461            }
462        })
463        .collect();
464
465    let green_token = GreenToken::new(rowan::SyntaxKind::from(token.kind()), &new_text);
466    let green = token.replace_with(green_token);
467    Some(Reparsed {
468        green,
469        diagnostics,
470        kind: ReparseKind::Token,
471    })
472}
473
474fn reparse_block(
475    old_root: &SyntaxNode,
476    old_text: &str,
477    old_diags: &[ParseDiagnostic],
478    edit: &Edit,
479    md_default: bool,
480) -> Option<Reparsed> {
481    let (s, e) = (edit.range.start, edit.range.end);
482    let elem = old_root.covering_element(text_range(s, e));
483    let start_node = match elem {
484        rowan::NodeOrToken::Node(n) => n,
485        rowan::NodeOrToken::Token(t) => t.parent()?,
486    };
487
488    // Smallest enclosing `{ … }` whose *interior* (between the braces) fully
489    // contains the edit, so the brace delimiters themselves are untouched.
490    let block = start_node.ancestors().find(|node| {
491        if node.kind() != SyntaxKind::BLOCK_EXPR {
492            return false;
493        }
494        let r = node.text_range();
495        let (bstart, bend) = (usize::from(r.start()), usize::from(r.end()));
496        // First/last tokens are `{`/`}` (one byte each); require interior containment.
497        bend.saturating_sub(bstart) >= 2 && s > bstart && e < bend
498    })?;
499
500    let r = block.text_range();
501    let (bstart, bend) = (usize::from(r.start()), usize::from(r.end()));
502
503    // New block text: the old block slice with the edit applied at its relative
504    // offset. The edit is interior, so the slice still starts with `{`/ends `}`.
505    let mut block_text = old_text[bstart..bend].to_string();
506    block_text.replace_range((s - bstart)..(e - bstart), &edit.insert);
507
508    let (block_green, block_diags) = parse_block_in_isolation(&block_text, md_default)?;
509
510    let delta = edit.delta();
511    let mut diagnostics: Vec<ParseDiagnostic> =
512        Vec::with_capacity(old_diags.len() + block_diags.len());
513    for d in old_diags {
514        if d.end <= bstart {
515            diagnostics.push(d.clone());
516        } else if d.start >= bend {
517            diagnostics.push(shift(d, delta));
518        }
519        // diagnostics inside the old block are dropped; the reparse regenerates them
520    }
521    for d in &block_diags {
522        diagnostics.push(ParseDiagnostic {
523            message: d.message.clone(),
524            start: d.start + bstart,
525            end: d.end + bstart,
526        });
527    }
528    diagnostics.sort_by_key(|d| (d.start, d.end));
529
530    let green = block.replace_with(block_green);
531    Some(Reparsed {
532        green,
533        diagnostics,
534        kind: ReparseKind::Block,
535    })
536}
537
538/// Reparse a single top-level statement (a direct child of `ROOT`) that isn't
539/// inside any `{ … }` block. See the module doc for the boundary argument.
540fn reparse_toplevel(
541    old_root: &SyntaxNode,
542    old_text: &str,
543    old_diags: &[ParseDiagnostic],
544    edit: &Edit,
545    md_default: bool,
546) -> Option<Reparsed> {
547    let (s, e) = (edit.range.start, edit.range.end);
548    let elem = old_root.covering_element(text_range(s, e));
549    let start_node = match elem {
550        rowan::NodeOrToken::Node(n) => n,
551        rowan::NodeOrToken::Token(t) => t.parent()?,
552    };
553
554    // The top-level statement: the ancestor whose parent is `ROOT`. When the
555    // covering element is a trivia token directly under `ROOT` (an edit in
556    // inter-statement whitespace), `start_node` *is* the root and there is no such
557    // ancestor — fall back.
558    let stmt = start_node
559        .ancestors()
560        .find(|node| node.parent().map(|p| p.kind()) == Some(SyntaxKind::ROOT))?;
561
562    // Roxygen blocks come from the dedicated `emit_roxygen_block` path, not
563    // `parse_expr`, so reparsing one via `parse_expr` would be wrong. (The kind
564    // match in `parse_stmt_in_isolation` would also reject it, but bail early.)
565    if stmt.kind() == SyntaxKind::ROXYGEN_BLOCK {
566        return None;
567    }
568
569    let r = stmt.text_range();
570    let (ss, se) = (usize::from(r.start()), usize::from(r.end()));
571    // The covering-element walk guarantees the statement contains the edit.
572    debug_assert!(ss <= s && e <= se);
573
574    // New statement text: the old statement slice with the edit applied at its
575    // relative offset. The edit is interior, so the slice still begins at the
576    // statement's first significant token.
577    let mut stmt_text = old_text[ss..se].to_string();
578    stmt_text.replace_range((s - ss)..(e - ss), &edit.insert);
579
580    // Isolation parse: one expression consuming every token, of the same node
581    // kind. Consume-all rejects the *shrink* case (the edit split the statement,
582    // e.g. removing a trailing operator, so some tokens leak past the boundary).
583    let (stmt_green, stmt_diags) = parse_stmt_in_isolation(&stmt_text, stmt.kind(), md_default)?;
584
585    // Forward-merge guard: reparsing the statement with the next sibling's text
586    // appended must still end exactly at the statement's length. Rejects the
587    // *grow* case (the edit made the statement continue onto what follows, e.g. a
588    // trailing binary operator). One sibling of context is sufficient and bounded:
589    // `parse_expr` reads a single expression, so a merge past the boundary is
590    // detected immediately; a statement that doesn't merge into its immediate
591    // successor cannot reach anything further.
592    let forward_end = match stmt.next_sibling() {
593        Some(next) => usize::from(next.text_range().end()),
594        None => old_text.len(),
595    };
596    if !toplevel_boundary_stable(&stmt_text, &old_text[se..forward_end], md_default) {
597        return None;
598    }
599
600    let delta = edit.delta();
601    let mut diagnostics: Vec<ParseDiagnostic> =
602        Vec::with_capacity(old_diags.len() + stmt_diags.len());
603    for d in old_diags {
604        if d.end <= ss {
605            diagnostics.push(d.clone());
606        } else if d.start >= se {
607            diagnostics.push(shift(d, delta));
608        }
609        // diagnostics inside the old statement are dropped; the reparse regenerates them
610    }
611    for d in &stmt_diags {
612        diagnostics.push(ParseDiagnostic {
613            message: d.message.clone(),
614            start: d.start + ss,
615            end: d.end + ss,
616        });
617    }
618    diagnostics.sort_by_key(|d| (d.start, d.end));
619
620    let green = stmt.replace_with(stmt_green);
621    Some(Reparsed {
622        green,
623        diagnostics,
624        kind: ReparseKind::TopLevel,
625    })
626}
627
628/// Parse a bare top-level statement's text on its own and return the green node
629/// for the single top-level node it produces (which must be of `expected_kind`),
630/// with statement-relative diagnostics. Returns `None` unless the text parses to
631/// exactly one expression consuming every token — the consume-all guard.
632fn parse_stmt_in_isolation(
633    stmt_text: &str,
634    expected_kind: SyntaxKind,
635    md_default: bool,
636) -> Option<(GreenNode, Vec<ParseDiagnostic>)> {
637    let tokens = rebalance_brackets(lex_with_md(stmt_text, md_default));
638    let mut diagnostics = Vec::new();
639    let expr = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default)?;
640    if expr.start != 0 || expr.end != tokens.len() {
641        return None;
642    }
643
644    let root = build_tree(&tokens, &expr.events);
645    let mut children = root.children();
646    let node = children.next()?;
647    if children.next().is_some() || node.kind() != expected_kind {
648        return None;
649    }
650    Some((node.green().to_owned(), diagnostics))
651}
652
653/// Whether `stmt_text` still ends at the same boundary once `forward_ctx` (the
654/// text up to the end of the next top-level sibling) is appended. `parse_expr`
655/// over the concatenation must consume a first expression that ends exactly at
656/// `stmt_text.len()`; anything else means the edit made the statement merge
657/// forward. Empty context (statement at end of file) is trivially stable.
658fn toplevel_boundary_stable(stmt_text: &str, forward_ctx: &str, md_default: bool) -> bool {
659    if forward_ctx.is_empty() {
660        return true;
661    }
662    let mut combined = String::with_capacity(stmt_text.len() + forward_ctx.len());
663    combined.push_str(stmt_text);
664    combined.push_str(forward_ctx);
665
666    let tokens = rebalance_brackets(lex_with_md(&combined, md_default));
667    let mut diagnostics = Vec::new();
668    let Some(expr) = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default) else {
669        return false;
670    };
671    // Byte offset one past the last token the first expression consumed. A stable
672    // boundary lands it exactly at the (unchanged) end of the statement text.
673    expr.end
674        .checked_sub(1)
675        .and_then(|last| tokens.get(last))
676        .is_some_and(|last| last.end == stmt_text.len())
677}
678
679/// Parse `{ … }` text on its own and return the green node for the single
680/// `BLOCK_EXPR` it produces, with block-relative diagnostics. Returns `None` if
681/// the text does not parse to exactly one block consuming all tokens (e.g. the
682/// edit transiently unbalanced the braces) — the caller falls back.
683fn parse_block_in_isolation(
684    block_text: &str,
685    md_default: bool,
686) -> Option<(GreenNode, Vec<ParseDiagnostic>)> {
687    let tokens = rebalance_brackets(lex_with_md(block_text, md_default));
688    let mut diagnostics = Vec::new();
689    let expr = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default)?;
690    if expr.start != 0 || expr.end != tokens.len() {
691        return None;
692    }
693
694    let root = build_tree(&tokens, &expr.events);
695    let mut children = root.children();
696    let block = children.next()?;
697    if children.next().is_some() || block.kind() != SyntaxKind::BLOCK_EXPR {
698        return None;
699    }
700
701    // The block must still be properly brace-delimited by its own *direct*
702    // delimiters. If the edit opened an unterminated string/comment or an
703    // unclosed inner `(`/`[` that swallowed the closing `}` (re-parenting it
704    // under a child, or leaking past the old block boundary in a full parse),
705    // the block is no longer self-contained — fall back.
706    let first = block.first_child_or_token().map(|e| e.kind());
707    let last = block.last_child_or_token().map(|e| e.kind());
708    if first != Some(SyntaxKind::LBRACE) || last != Some(SyntaxKind::RBRACE) {
709        return None;
710    }
711
712    Some((block.green().to_owned(), diagnostics))
713}
714
715fn shift(d: &ParseDiagnostic, delta: isize) -> ParseDiagnostic {
716    ParseDiagnostic {
717        message: d.message.clone(),
718        start: (d.start as isize + delta) as usize,
719        end: (d.end as isize + delta) as usize,
720    }
721}
722
723fn text_range(start: usize, end: usize) -> TextRange {
724    TextRange::new(TextSize::new(start as u32), TextSize::new(end as u32))
725}
726
727#[cfg(test)]
728mod verify_tests {
729    use super::*;
730
731    fn edit(start: usize, end: usize, insert: &str) -> Edit {
732        Edit {
733            range: start..end,
734            insert: insert.to_string(),
735        }
736    }
737
738    /// `produces` must agree with `apply` wherever `apply` is defined, and must
739    /// answer `false` — not panic — wherever it is not.
740    fn agrees_with_apply(old: &str, e: &Edit) {
741        if e.fits(old) {
742            let applied = e.apply(old);
743            assert!(
744                e.produces(old, &applied),
745                "produces rejected its own apply: {e:?} on {old:?}",
746            );
747            // Any other target is a different string, so it must be rejected.
748            // Perturb at each end, so neither the length check nor one span
749            // compare can carry the verdict alone.
750            assert!(!e.produces(old, &format!("{applied}~")));
751            assert!(!e.produces(old, &format!("~{applied}")));
752        } else {
753            assert!(!e.produces(old, old), "unfit edit accepted: {e:?}");
754        }
755    }
756
757    #[test]
758    fn produces_agrees_with_apply_across_edit_shapes() {
759        let old = "alpha <- beta\n";
760        for e in [
761            edit(0, 0, "x"),                  // insert at the start
762            edit(old.len(), old.len(), "x"),  // insert at EOF
763            edit(6, 6, ""),                   // empty insert, empty range
764            edit(0, old.len(), "wholly new"), // replace everything
765            edit(0, old.len(), ""),           // delete everything
766            edit(9, 13, ""),                  // delete a token
767            edit(9, 13, "gamma"),             // replace a token, growing
768            edit(9, 13, "g"),                 // replace a token, shrinking
769        ] {
770            agrees_with_apply(old, &e);
771        }
772    }
773
774    #[test]
775    fn produces_is_total_on_ranges_the_text_cannot_take() {
776        let old = "café <- 1\n";
777        for e in [
778            edit(0, 900, "x"),   // end past the text
779            edit(900, 900, "x"), // start past the text
780            edit(5, 2, "x"),     // inverted
781            edit(3, 4, "x"),     // splits the 'é'
782            edit(0, 4, "x"),     // ends inside the 'é'
783        ] {
784            agrees_with_apply(old, &e);
785        }
786    }
787
788    #[test]
789    fn produces_rejects_a_same_length_but_different_target() {
790        // The length check alone would pass here; the span compares are what
791        // decide it.
792        let old = "x <- 1\n";
793        let e = edit(5, 6, "9");
794        assert!(e.produces(old, "x <- 9\n"));
795        assert!(!e.produces(old, "y <- 9\n"));
796        assert!(!e.produces(old, "x <- 8\n"));
797        assert!(!e.produces(old, "x <- 9!"));
798    }
799
800    #[test]
801    fn edits_produce_folds_each_against_prior_text() {
802        // The same sequence `apply_edits` folds, verified without rebuilding it.
803        let old = "cdef";
804        let edits = [edit(0, 0, "ab"), edit(4, 4, "Z")];
805        assert_eq!(apply_edits(old, &edits), "abcdZef");
806        assert!(edits_produce(old, &edits, "abcdZef"));
807        assert!(!edits_produce(old, &edits, "abcdef"));
808    }
809
810    #[test]
811    fn edits_produce_rejects_a_chain_that_stops_fitting() {
812        // The second edit is in bounds against `old` but not against what the
813        // first leaves behind. `apply_edits` would panic on it.
814        let old = "abcdef";
815        let edits = [edit(0, 5, ""), edit(4, 5, "z")];
816        assert!(!edits_produce(old, &edits, "fz"));
817    }
818
819    #[test]
820    fn edits_produce_on_an_empty_slice_is_an_identity_check() {
821        assert!(edits_produce("xyz", &[], "xyz"));
822        assert!(!edits_produce("xyz", &[], "xyw"));
823    }
824}
825
826#[cfg(test)]
827mod map_range_tests {
828    use super::*;
829
830    fn edit(start: usize, end: usize, insert: &str) -> Edit {
831        Edit {
832            range: start..end,
833            insert: insert.to_string(),
834        }
835    }
836
837    #[test]
838    fn node_before_edit_is_unchanged() {
839        let node = text_range(0, 5);
840        assert_eq!(
841            map_range_through_edit(node, &edit(10, 12, "xxxx")),
842            Some(node)
843        );
844    }
845
846    #[test]
847    fn node_after_insertion_shifts_by_delta() {
848        let node = text_range(10, 15);
849        // Insert 4 chars where 2 stood before the node: net +2.
850        assert_eq!(
851            map_range_through_edit(node, &edit(0, 2, "xxxx")),
852            Some(text_range(12, 17))
853        );
854    }
855
856    #[test]
857    fn node_after_deletion_shifts_back() {
858        let node = text_range(10, 15);
859        assert_eq!(
860            map_range_through_edit(node, &edit(0, 4, "")),
861            Some(text_range(6, 11))
862        );
863    }
864
865    #[test]
866    fn edit_ending_at_node_start_shifts_node() {
867        // ee == ns: the node survives and shifts by the delta.
868        let node = text_range(10, 15);
869        assert_eq!(
870            map_range_through_edit(node, &edit(8, 10, "abcd")),
871            Some(text_range(12, 17))
872        );
873    }
874
875    #[test]
876    fn insertion_at_node_start_shifts_node() {
877        let node = text_range(10, 15);
878        assert_eq!(
879            map_range_through_edit(node, &edit(10, 10, "ab")),
880            Some(text_range(12, 17))
881        );
882    }
883
884    #[test]
885    fn insertion_at_node_end_leaves_node_unchanged() {
886        let node = text_range(10, 15);
887        assert_eq!(
888            map_range_through_edit(node, &edit(15, 15, "ab")),
889            Some(node)
890        );
891    }
892
893    #[test]
894    fn insertion_strictly_inside_invalidates() {
895        let node = text_range(10, 15);
896        assert_eq!(map_range_through_edit(node, &edit(12, 12, "z")), None);
897    }
898
899    #[test]
900    fn overlapping_deletion_invalidates() {
901        let node = text_range(10, 15);
902        assert_eq!(map_range_through_edit(node, &edit(12, 18, "")), None);
903    }
904
905    #[test]
906    fn zero_length_node_at_insertion_point_is_unchanged() {
907        // Deterministic boundary rule: a zero-length node at the insertion point
908        // is treated as preceding the insertion.
909        let node = text_range(10, 10);
910        assert_eq!(
911            map_range_through_edit(node, &edit(10, 10, "ab")),
912            Some(node)
913        );
914    }
915
916    #[test]
917    fn append_at_eof_leaves_earlier_node_unchanged() {
918        let node = text_range(0, 5);
919        assert_eq!(
920            map_range_through_edit(node, &edit(5, 5, "\n# trailing")),
921            Some(node)
922        );
923    }
924
925    #[test]
926    fn map_through_edits_folds_in_order() {
927        let node = text_range(10, 15);
928        // Two insertions before the node, each expressed against the prior text.
929        let edits = [edit(0, 0, "ab"), edit(0, 0, "cd")];
930        assert_eq!(
931            map_range_through_edits(node, &edits),
932            Some(text_range(14, 19))
933        );
934    }
935
936    #[test]
937    fn map_through_edits_invalidates_on_any_overlap() {
938        let node = text_range(10, 15);
939        // After the first edit the node is at [12, 17); the second lands inside.
940        let edits = [edit(0, 0, "ab"), edit(13, 13, "z")];
941        assert_eq!(map_range_through_edits(node, &edits), None);
942    }
943
944    #[test]
945    fn apply_edits_folds_each_against_prior_text() {
946        // Two disjoint edits, each expressed against the text its predecessor
947        // produced: prepend "ab", then insert "Z" before "ef" in the now-longer
948        // text (index 4, not 2 — the prepend shifted it).
949        let edits = [edit(0, 0, "ab"), edit(4, 4, "Z")];
950        assert_eq!(apply_edits("cdef", &edits), "abcdZef");
951    }
952
953    #[test]
954    fn apply_edits_empty_is_identity() {
955        assert_eq!(apply_edits("xyz", &[]), "xyz");
956    }
957}