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    if !edit.fits(old_text) {
285        return None;
286    }
287
288    let md = options.roxygen_markdown_default;
289    let result = reparse_token(old_root, old_text, old_diags, edit, md)
290        .or_else(|| reparse_block(old_root, old_text, old_diags, edit, md))
291        .or_else(|| reparse_toplevel(old_root, old_text, old_diags, edit, md));
292
293    if let Some(reparsed) = &result {
294        debug_assert_eq!(
295            SyntaxNode::new_root(reparsed.green.clone())
296                .text()
297                .to_string(),
298            edit.apply(old_text),
299            "incremental reparse ({:?}) is not lossless",
300            reparsed.kind,
301        );
302    }
303    result
304}
305
306/// Chain the single-edit [`reparse`] over a sequence of `edits` (each expressed
307/// against the text its predecessors produced, in the order [`Edit::apply`]
308/// would run them). Each step reparses off the previous step's whole-file tree,
309/// so N disjoint edits each reparse their own tight region instead of collapsing
310/// into one wide [`diff_edit`] span (the whole point of feeding precise LSP
311/// edits through — multi-cursor and find-replace).
312///
313/// Returns `None` unless *every* step is incrementally reparseable **and** the
314/// fully-applied text equals `target`. The `== target` check is the Tenet-4
315/// safety net: an edit sequence that does not actually transform `old_text` into
316/// the current buffer (a coalescing gap, an interleaved disk write, a
317/// full-document replacement, a range the text cannot even hold) is rejected
318/// here so the caller falls back to the always-correct [`diff_edit`] path. A
319/// successful result is therefore byte-identical to a full
320/// [`parse`](crate::parser::parse) of `target` — the composition of correct
321/// single-edit reparses over the true edit sequence.
322pub fn reparse_edits(
323    old_root: &SyntaxNode,
324    old_text: &str,
325    old_diags: &[ParseDiagnostic],
326    edits: &[Edit],
327    target: &str,
328) -> Option<Reparsed> {
329    reparse_edits_with_options(
330        old_root,
331        old_text,
332        old_diags,
333        edits,
334        target,
335        &ParseOptions::default(),
336    )
337}
338
339/// [`reparse_edits`] with the [`ParseOptions`] the old tree was parsed under
340/// (see [`reparse_with_options`]).
341pub fn reparse_edits_with_options(
342    old_root: &SyntaxNode,
343    old_text: &str,
344    old_diags: &[ParseDiagnostic],
345    edits: &[Edit],
346    target: &str,
347    options: &ParseOptions,
348) -> Option<Reparsed> {
349    // Split the *last* edit off rather than the first: it is the one whose
350    // result is `target`, so it can be verified by comparison instead of by
351    // rebuilding the document. A one-edit chain — a single keystroke, the common
352    // case — therefore allocates nothing here at all.
353    let (last, init) = edits.split_last()?;
354
355    // Each step reparses off the prior step's tree; the first off the caller's.
356    let mut root = old_root.clone();
357    let mut text = Cow::Borrowed(old_text);
358    let mut diags = Cow::Borrowed(old_diags);
359    for edit in init {
360        let reparsed = reparse_with_options(&root, &text, &diags, edit, options)?;
361        // Infallible once the reparse succeeded, which implies the fit.
362        text = Cow::Owned(edit.apply_checked(&text)?);
363        root = SyntaxNode::new_root(reparsed.green);
364        diags = Cow::Owned(reparsed.diagnostics);
365    }
366
367    if !last.produces(&text, target) {
368        return None;
369    }
370
371    let mut reparsed = reparse_with_options(&root, &text, &diags, last, options)?;
372    reparsed.kind = ReparseKind::Multi;
373    Some(reparsed)
374}
375
376const TOKEN_REPARSE_KINDS: &[SyntaxKind] = &[
377    SyntaxKind::IDENT,
378    SyntaxKind::STRING,
379    SyntaxKind::COMMENT,
380    SyntaxKind::WHITESPACE,
381    // Roxygen leaves are deliberately absent: they only arise from the `#'`-line
382    // lexer path, so relexing one in isolation (`lex("A number.")`) never yields
383    // a roxygen token and the single-token guard would always bail. Edits inside
384    // a roxygen line therefore fall back to block reparse (when the block sits in
385    // a `{}` body) or a full reparse — correct, just not token-incremental.
386];
387
388fn reparse_token(
389    old_root: &SyntaxNode,
390    old_text: &str,
391    old_diags: &[ParseDiagnostic],
392    edit: &Edit,
393    md_default: bool,
394) -> Option<Reparsed> {
395    let (s, e) = (edit.range.start, edit.range.end);
396    let elem = old_root.covering_element(text_range(s, e));
397    let token = elem.into_token()?;
398    if !TOKEN_REPARSE_KINDS.contains(&token.kind()) {
399        return None;
400    }
401
402    let tr = token.text_range();
403    let (t0, t1) = (usize::from(tr.start()), usize::from(tr.end()));
404    // A pure insertion must land strictly inside the token, so it is
405    // unambiguous which token it extends. Edits with a non-empty range that
406    // span a token boundary never reach here — `covering_element` returns the
407    // parent node, not a token.
408    if s == e && !(t0 < s && s < t1) {
409        return None;
410    }
411
412    // The token's new text, with the edit applied at its relative offset.
413    let mut new_text = token.text().to_string();
414    new_text.replace_range((s - t0)..(e - t0), &edit.insert);
415
416    // Re-lex in isolation: it must still be exactly one token of the same kind.
417    let relexed = lex_with_md(&new_text, md_default);
418    let [only] = relexed.as_slice() else {
419        return None;
420    };
421    if only.start != 0 || only.end != new_text.len() || syntax_kind_for(&only.kind) != token.kind()
422    {
423        return None;
424    }
425
426    // Merge guard: appending the following source character must not extend the
427    // token (which would mean the edit merged it with its neighbor, e.g.
428    // `for`→`fore`, or an unterminated string swallowing the next line).
429    if let Some(next_char) = old_text[t1..].chars().next() {
430        let mut probe = new_text.clone();
431        probe.push(next_char);
432        let probe_toks = lex_with_md(&probe, md_default);
433        let first = probe_toks.first()?;
434        if first.end != new_text.len() || syntax_kind_for(&first.kind) != token.kind() {
435            return None;
436        }
437    }
438
439    // Keep diagnostic remapping trivial: only proceed when no diagnostic touches
440    // the edited token. A clean single-token relex introduces none of its own.
441    if old_diags.iter().any(|d| d.start < t1 && d.end > t0) {
442        return None;
443    }
444    let delta = edit.delta();
445    let diagnostics = old_diags
446        .iter()
447        .map(|d| {
448            if d.start >= t1 {
449                shift(d, delta)
450            } else {
451                d.clone()
452            }
453        })
454        .collect();
455
456    let green_token = GreenToken::new(rowan::SyntaxKind::from(token.kind()), &new_text);
457    let green = token.replace_with(green_token);
458    Some(Reparsed {
459        green,
460        diagnostics,
461        kind: ReparseKind::Token,
462    })
463}
464
465fn reparse_block(
466    old_root: &SyntaxNode,
467    old_text: &str,
468    old_diags: &[ParseDiagnostic],
469    edit: &Edit,
470    md_default: bool,
471) -> Option<Reparsed> {
472    let (s, e) = (edit.range.start, edit.range.end);
473    let elem = old_root.covering_element(text_range(s, e));
474    let start_node = match elem {
475        rowan::NodeOrToken::Node(n) => n,
476        rowan::NodeOrToken::Token(t) => t.parent()?,
477    };
478
479    // Smallest enclosing `{ … }` whose *interior* (between the braces) fully
480    // contains the edit, so the brace delimiters themselves are untouched.
481    let block = start_node.ancestors().find(|node| {
482        if node.kind() != SyntaxKind::BLOCK_EXPR {
483            return false;
484        }
485        let r = node.text_range();
486        let (bstart, bend) = (usize::from(r.start()), usize::from(r.end()));
487        // First/last tokens are `{`/`}` (one byte each); require interior containment.
488        bend.saturating_sub(bstart) >= 2 && s > bstart && e < bend
489    })?;
490
491    let r = block.text_range();
492    let (bstart, bend) = (usize::from(r.start()), usize::from(r.end()));
493
494    // New block text: the old block slice with the edit applied at its relative
495    // offset. The edit is interior, so the slice still starts with `{`/ends `}`.
496    let mut block_text = old_text[bstart..bend].to_string();
497    block_text.replace_range((s - bstart)..(e - bstart), &edit.insert);
498
499    let (block_green, block_diags) = parse_block_in_isolation(&block_text, md_default)?;
500
501    let delta = edit.delta();
502    let mut diagnostics: Vec<ParseDiagnostic> =
503        Vec::with_capacity(old_diags.len() + block_diags.len());
504    for d in old_diags {
505        if d.end <= bstart {
506            diagnostics.push(d.clone());
507        } else if d.start >= bend {
508            diagnostics.push(shift(d, delta));
509        }
510        // diagnostics inside the old block are dropped; the reparse regenerates them
511    }
512    for d in &block_diags {
513        diagnostics.push(ParseDiagnostic {
514            message: d.message.clone(),
515            start: d.start + bstart,
516            end: d.end + bstart,
517        });
518    }
519    diagnostics.sort_by_key(|d| (d.start, d.end));
520
521    let green = block.replace_with(block_green);
522    Some(Reparsed {
523        green,
524        diagnostics,
525        kind: ReparseKind::Block,
526    })
527}
528
529/// Reparse a single top-level statement (a direct child of `ROOT`) that isn't
530/// inside any `{ … }` block. See the module doc for the boundary argument.
531fn reparse_toplevel(
532    old_root: &SyntaxNode,
533    old_text: &str,
534    old_diags: &[ParseDiagnostic],
535    edit: &Edit,
536    md_default: bool,
537) -> Option<Reparsed> {
538    let (s, e) = (edit.range.start, edit.range.end);
539    let elem = old_root.covering_element(text_range(s, e));
540    let start_node = match elem {
541        rowan::NodeOrToken::Node(n) => n,
542        rowan::NodeOrToken::Token(t) => t.parent()?,
543    };
544
545    // The top-level statement: the ancestor whose parent is `ROOT`. When the
546    // covering element is a trivia token directly under `ROOT` (an edit in
547    // inter-statement whitespace), `start_node` *is* the root and there is no such
548    // ancestor — fall back.
549    let stmt = start_node
550        .ancestors()
551        .find(|node| node.parent().map(|p| p.kind()) == Some(SyntaxKind::ROOT))?;
552
553    // Roxygen blocks come from the dedicated `emit_roxygen_block` path, not
554    // `parse_expr`, so reparsing one via `parse_expr` would be wrong. (The kind
555    // match in `parse_stmt_in_isolation` would also reject it, but bail early.)
556    if stmt.kind() == SyntaxKind::ROXYGEN_BLOCK {
557        return None;
558    }
559
560    let r = stmt.text_range();
561    let (ss, se) = (usize::from(r.start()), usize::from(r.end()));
562    // The covering-element walk guarantees the statement contains the edit.
563    debug_assert!(ss <= s && e <= se);
564
565    // New statement text: the old statement slice with the edit applied at its
566    // relative offset. The edit is interior, so the slice still begins at the
567    // statement's first significant token.
568    let mut stmt_text = old_text[ss..se].to_string();
569    stmt_text.replace_range((s - ss)..(e - ss), &edit.insert);
570
571    // Isolation parse: one expression consuming every token, of the same node
572    // kind. Consume-all rejects the *shrink* case (the edit split the statement,
573    // e.g. removing a trailing operator, so some tokens leak past the boundary).
574    let (stmt_green, stmt_diags) = parse_stmt_in_isolation(&stmt_text, stmt.kind(), md_default)?;
575
576    // Forward-merge guard: reparsing the statement with the next sibling's text
577    // appended must still end exactly at the statement's length. Rejects the
578    // *grow* case (the edit made the statement continue onto what follows, e.g. a
579    // trailing binary operator). One sibling of context is sufficient and bounded:
580    // `parse_expr` reads a single expression, so a merge past the boundary is
581    // detected immediately; a statement that doesn't merge into its immediate
582    // successor cannot reach anything further.
583    let forward_end = match stmt.next_sibling() {
584        Some(next) => usize::from(next.text_range().end()),
585        None => old_text.len(),
586    };
587    if !toplevel_boundary_stable(&stmt_text, &old_text[se..forward_end], md_default) {
588        return None;
589    }
590
591    let delta = edit.delta();
592    let mut diagnostics: Vec<ParseDiagnostic> =
593        Vec::with_capacity(old_diags.len() + stmt_diags.len());
594    for d in old_diags {
595        if d.end <= ss {
596            diagnostics.push(d.clone());
597        } else if d.start >= se {
598            diagnostics.push(shift(d, delta));
599        }
600        // diagnostics inside the old statement are dropped; the reparse regenerates them
601    }
602    for d in &stmt_diags {
603        diagnostics.push(ParseDiagnostic {
604            message: d.message.clone(),
605            start: d.start + ss,
606            end: d.end + ss,
607        });
608    }
609    diagnostics.sort_by_key(|d| (d.start, d.end));
610
611    let green = stmt.replace_with(stmt_green);
612    Some(Reparsed {
613        green,
614        diagnostics,
615        kind: ReparseKind::TopLevel,
616    })
617}
618
619/// Parse a bare top-level statement's text on its own and return the green node
620/// for the single top-level node it produces (which must be of `expected_kind`),
621/// with statement-relative diagnostics. Returns `None` unless the text parses to
622/// exactly one expression consuming every token — the consume-all guard.
623fn parse_stmt_in_isolation(
624    stmt_text: &str,
625    expected_kind: SyntaxKind,
626    md_default: bool,
627) -> Option<(GreenNode, Vec<ParseDiagnostic>)> {
628    let tokens = rebalance_brackets(lex_with_md(stmt_text, md_default));
629    let mut diagnostics = Vec::new();
630    let expr = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default)?;
631    if expr.start != 0 || expr.end != tokens.len() {
632        return None;
633    }
634
635    let root = build_tree(&tokens, &expr.events);
636    let mut children = root.children();
637    let node = children.next()?;
638    if children.next().is_some() || node.kind() != expected_kind {
639        return None;
640    }
641    crate::parser::diagnostics::refine_unicode_diagnostics(&tokens, &mut diagnostics);
642    Some((node.green().to_owned(), diagnostics))
643}
644
645/// Whether `stmt_text` still ends at the same boundary once `forward_ctx` (the
646/// text up to the end of the next top-level sibling) is appended. `parse_expr`
647/// over the concatenation must consume a first expression that ends exactly at
648/// `stmt_text.len()`; anything else means the edit made the statement merge
649/// forward. Empty context (statement at end of file) is trivially stable.
650fn toplevel_boundary_stable(stmt_text: &str, forward_ctx: &str, md_default: bool) -> bool {
651    if forward_ctx.is_empty() {
652        return true;
653    }
654    let mut combined = String::with_capacity(stmt_text.len() + forward_ctx.len());
655    combined.push_str(stmt_text);
656    combined.push_str(forward_ctx);
657
658    let tokens = rebalance_brackets(lex_with_md(&combined, md_default));
659    let mut diagnostics = Vec::new();
660    let Some(expr) = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default) else {
661        return false;
662    };
663    // Byte offset one past the last token the first expression consumed. A stable
664    // boundary lands it exactly at the (unchanged) end of the statement text.
665    expr.end
666        .checked_sub(1)
667        .and_then(|last| tokens.get(last))
668        .is_some_and(|last| last.end == stmt_text.len())
669}
670
671/// Parse `{ … }` text on its own and return the green node for the single
672/// `BLOCK_EXPR` it produces, with block-relative diagnostics. Returns `None` if
673/// the text does not parse to exactly one block consuming all tokens (e.g. the
674/// edit transiently unbalanced the braces) — the caller falls back.
675fn parse_block_in_isolation(
676    block_text: &str,
677    md_default: bool,
678) -> Option<(GreenNode, Vec<ParseDiagnostic>)> {
679    let tokens = rebalance_brackets(lex_with_md(block_text, md_default));
680    let mut diagnostics = Vec::new();
681    let expr = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default)?;
682    if expr.start != 0 || expr.end != tokens.len() {
683        return None;
684    }
685
686    let root = build_tree(&tokens, &expr.events);
687    let mut children = root.children();
688    let block = children.next()?;
689    if children.next().is_some() || block.kind() != SyntaxKind::BLOCK_EXPR {
690        return None;
691    }
692
693    let first = block.first_child_or_token().map(|e| e.kind());
694    let last = block.last_child_or_token().map(|e| e.kind());
695    if first != Some(SyntaxKind::LBRACE) || last != Some(SyntaxKind::RBRACE) {
696        return None;
697    }
698
699    crate::parser::diagnostics::refine_unicode_diagnostics(&tokens, &mut diagnostics);
700    Some((block.green().to_owned(), diagnostics))
701}
702
703fn shift(d: &ParseDiagnostic, delta: isize) -> ParseDiagnostic {
704    ParseDiagnostic {
705        message: d.message.clone(),
706        start: (d.start as isize + delta) as usize,
707        end: (d.end as isize + delta) as usize,
708    }
709}
710
711fn text_range(start: usize, end: usize) -> TextRange {
712    TextRange::new(TextSize::new(start as u32), TextSize::new(end as u32))
713}
714
715#[cfg(test)]
716mod verify_tests {
717    use super::*;
718
719    fn edit(start: usize, end: usize, insert: &str) -> Edit {
720        Edit {
721            range: start..end,
722            insert: insert.to_string(),
723        }
724    }
725
726    /// `produces` must agree with `apply` wherever `apply` is defined, and must
727    /// answer `false` — not panic — wherever it is not.
728    fn agrees_with_apply(old: &str, e: &Edit) {
729        if e.fits(old) {
730            let applied = e.apply(old);
731            assert!(
732                e.produces(old, &applied),
733                "produces rejected its own apply: {e:?} on {old:?}",
734            );
735            // Any other target is a different string, so it must be rejected.
736            // Perturb at each end, so neither the length check nor one span
737            // compare can carry the verdict alone.
738            assert!(!e.produces(old, &format!("{applied}~")));
739            assert!(!e.produces(old, &format!("~{applied}")));
740        } else {
741            assert!(!e.produces(old, old), "unfit edit accepted: {e:?}");
742        }
743    }
744
745    #[test]
746    fn produces_agrees_with_apply_across_edit_shapes() {
747        let old = "alpha <- beta\n";
748        for e in [
749            edit(0, 0, "x"),                  // insert at the start
750            edit(old.len(), old.len(), "x"),  // insert at EOF
751            edit(6, 6, ""),                   // empty insert, empty range
752            edit(0, old.len(), "wholly new"), // replace everything
753            edit(0, old.len(), ""),           // delete everything
754            edit(9, 13, ""),                  // delete a token
755            edit(9, 13, "gamma"),             // replace a token, growing
756            edit(9, 13, "g"),                 // replace a token, shrinking
757        ] {
758            agrees_with_apply(old, &e);
759        }
760    }
761
762    #[test]
763    fn produces_is_total_on_ranges_the_text_cannot_take() {
764        let old = "café <- 1\n";
765        for e in [
766            edit(0, 900, "x"),   // end past the text
767            edit(900, 900, "x"), // start past the text
768            edit(5, 2, "x"),     // inverted
769            edit(3, 4, "x"),     // splits the 'é'
770            edit(0, 4, "x"),     // ends inside the 'é'
771        ] {
772            agrees_with_apply(old, &e);
773        }
774    }
775
776    #[test]
777    fn produces_rejects_a_same_length_but_different_target() {
778        // The length check alone would pass here; the span compares are what
779        // decide it.
780        let old = "x <- 1\n";
781        let e = edit(5, 6, "9");
782        assert!(e.produces(old, "x <- 9\n"));
783        assert!(!e.produces(old, "y <- 9\n"));
784        assert!(!e.produces(old, "x <- 8\n"));
785        assert!(!e.produces(old, "x <- 9!"));
786    }
787
788    #[test]
789    fn edits_produce_folds_each_against_prior_text() {
790        // The same sequence `apply_edits` folds, verified without rebuilding it.
791        let old = "cdef";
792        let edits = [edit(0, 0, "ab"), edit(4, 4, "Z")];
793        assert_eq!(apply_edits(old, &edits), "abcdZef");
794        assert!(edits_produce(old, &edits, "abcdZef"));
795        assert!(!edits_produce(old, &edits, "abcdef"));
796    }
797
798    #[test]
799    fn edits_produce_rejects_a_chain_that_stops_fitting() {
800        // The second edit is in bounds against `old` but not against what the
801        // first leaves behind. `apply_edits` would panic on it.
802        let old = "abcdef";
803        let edits = [edit(0, 5, ""), edit(4, 5, "z")];
804        assert!(!edits_produce(old, &edits, "fz"));
805    }
806
807    #[test]
808    fn edits_produce_on_an_empty_slice_is_an_identity_check() {
809        assert!(edits_produce("xyz", &[], "xyz"));
810        assert!(!edits_produce("xyz", &[], "xyw"));
811    }
812}
813
814#[cfg(test)]
815mod map_range_tests {
816    use super::*;
817
818    fn edit(start: usize, end: usize, insert: &str) -> Edit {
819        Edit {
820            range: start..end,
821            insert: insert.to_string(),
822        }
823    }
824
825    #[test]
826    fn node_before_edit_is_unchanged() {
827        let node = text_range(0, 5);
828        assert_eq!(
829            map_range_through_edit(node, &edit(10, 12, "xxxx")),
830            Some(node)
831        );
832    }
833
834    #[test]
835    fn node_after_insertion_shifts_by_delta() {
836        let node = text_range(10, 15);
837        // Insert 4 chars where 2 stood before the node: net +2.
838        assert_eq!(
839            map_range_through_edit(node, &edit(0, 2, "xxxx")),
840            Some(text_range(12, 17))
841        );
842    }
843
844    #[test]
845    fn node_after_deletion_shifts_back() {
846        let node = text_range(10, 15);
847        assert_eq!(
848            map_range_through_edit(node, &edit(0, 4, "")),
849            Some(text_range(6, 11))
850        );
851    }
852
853    #[test]
854    fn edit_ending_at_node_start_shifts_node() {
855        // ee == ns: the node survives and shifts by the delta.
856        let node = text_range(10, 15);
857        assert_eq!(
858            map_range_through_edit(node, &edit(8, 10, "abcd")),
859            Some(text_range(12, 17))
860        );
861    }
862
863    #[test]
864    fn insertion_at_node_start_shifts_node() {
865        let node = text_range(10, 15);
866        assert_eq!(
867            map_range_through_edit(node, &edit(10, 10, "ab")),
868            Some(text_range(12, 17))
869        );
870    }
871
872    #[test]
873    fn insertion_at_node_end_leaves_node_unchanged() {
874        let node = text_range(10, 15);
875        assert_eq!(
876            map_range_through_edit(node, &edit(15, 15, "ab")),
877            Some(node)
878        );
879    }
880
881    #[test]
882    fn insertion_strictly_inside_invalidates() {
883        let node = text_range(10, 15);
884        assert_eq!(map_range_through_edit(node, &edit(12, 12, "z")), None);
885    }
886
887    #[test]
888    fn overlapping_deletion_invalidates() {
889        let node = text_range(10, 15);
890        assert_eq!(map_range_through_edit(node, &edit(12, 18, "")), None);
891    }
892
893    #[test]
894    fn zero_length_node_at_insertion_point_is_unchanged() {
895        // Deterministic boundary rule: a zero-length node at the insertion point
896        // is treated as preceding the insertion.
897        let node = text_range(10, 10);
898        assert_eq!(
899            map_range_through_edit(node, &edit(10, 10, "ab")),
900            Some(node)
901        );
902    }
903
904    #[test]
905    fn append_at_eof_leaves_earlier_node_unchanged() {
906        let node = text_range(0, 5);
907        assert_eq!(
908            map_range_through_edit(node, &edit(5, 5, "\n# trailing")),
909            Some(node)
910        );
911    }
912
913    #[test]
914    fn map_through_edits_folds_in_order() {
915        let node = text_range(10, 15);
916        // Two insertions before the node, each expressed against the prior text.
917        let edits = [edit(0, 0, "ab"), edit(0, 0, "cd")];
918        assert_eq!(
919            map_range_through_edits(node, &edits),
920            Some(text_range(14, 19))
921        );
922    }
923
924    #[test]
925    fn map_through_edits_invalidates_on_any_overlap() {
926        let node = text_range(10, 15);
927        // After the first edit the node is at [12, 17); the second lands inside.
928        let edits = [edit(0, 0, "ab"), edit(13, 13, "z")];
929        assert_eq!(map_range_through_edits(node, &edits), None);
930    }
931
932    #[test]
933    fn apply_edits_folds_each_against_prior_text() {
934        // Two disjoint edits, each expressed against the text its predecessor
935        // produced: prepend "ab", then insert "Z" before "ef" in the now-longer
936        // text (index 4, not 2 — the prepend shifted it).
937        let edits = [edit(0, 0, "ab"), edit(4, 4, "Z")];
938        assert_eq!(apply_edits("cdef", &edits), "abcdZef");
939    }
940
941    #[test]
942    fn apply_edits_empty_is_identity() {
943        assert_eq!(apply_edits("xyz", &[]), "xyz");
944    }
945}