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::ops::Range;
34
35use rowan::{GreenNode, GreenToken, TextRange, TextSize};
36
37use crate::parser::bracket_balancer::rebalance_brackets;
38use crate::parser::core::ParseOptions;
39use crate::parser::diagnostics::ParseDiagnostic;
40use crate::parser::expr::parse_expr;
41use crate::parser::lexer::lex_with_md;
42use crate::parser::tree_builder::{build_tree, syntax_kind_for};
43use crate::syntax::{SyntaxKind, SyntaxNode};
44
45/// A single contiguous text edit: replace `range` (a byte range in the *old*
46/// text) with `insert`.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Edit {
49    pub range: Range<usize>,
50    pub insert: String,
51}
52
53impl Edit {
54    /// The signed length change this edit applies to text after `range`.
55    fn delta(&self) -> isize {
56        self.insert.len() as isize - (self.range.end - self.range.start) as isize
57    }
58
59    /// Apply the edit to `old`, producing the new text.
60    pub fn apply(&self, old: &str) -> String {
61        let mut out =
62            String::with_capacity(old.len().saturating_sub(self.range.len()) + self.insert.len());
63        out.push_str(&old[..self.range.start]);
64        out.push_str(&self.insert);
65        out.push_str(&old[self.range.end..]);
66        out
67    }
68}
69
70/// Apply `edits` to `old` left-to-right, each expressed against the text its
71/// predecessors produced — the inverse operation to [`map_range_through_edits`].
72///
73/// Used as an apply-and-verify guard by span-mapping consumers: reconstructing
74/// the current text from an old snapshot plus an accumulated edit slice proves
75/// the slice is the exact transform between them before a range is folded
76/// through it, so a stale or misaligned slice can be rejected in favor of the
77/// whole-text [`diff_edit`] fallback.
78pub fn apply_edits(old: &str, edits: &[Edit]) -> String {
79    edits.iter().fold(old.to_string(), |t, e| e.apply(&t))
80}
81
82/// Map a `TextRange` taken against the text *before* `edit` to its position in
83/// the text *after* `edit`.
84///
85/// Returns `None` when the edit's replaced range overlaps the node's interior:
86/// a `(kind, range)` handle cannot promise the node survived an edit inside it
87/// (its text, and possibly its kind, may have changed). Comparisons are
88/// half-open, so the boundary cases are deterministic: an edit ending exactly at
89/// the node's start shifts the node (it survives), an edit starting exactly at
90/// the node's end leaves it unchanged (it survives), and an insertion strictly
91/// inside the node returns `None`. A zero-length node sitting at an insertion
92/// point is treated as preceding the insertion (unchanged).
93///
94/// Note: the `arity` crate's `incremental::parsed_document` recovers a
95/// single spanning [`diff_edit`], so several disjoint keystrokes coalesce into
96/// one wide edit. That only widens the overlap and yields `None` more often
97/// (more position-based re-resolution by the caller) — it never produces a wrong
98/// mapping. Feeding precise per-change LSP edits would shrink the edit and
99/// reduce those fallbacks.
100pub fn map_range_through_edit(range: TextRange, edit: &Edit) -> Option<TextRange> {
101    let node_start = usize::from(range.start());
102    let node_end = usize::from(range.end());
103
104    if node_end <= edit.range.start {
105        // Node entirely before the edit: untouched.
106        Some(range)
107    } else if node_start >= edit.range.end {
108        // Node entirely after the edit: every offset shifts by the length delta.
109        let start = (node_start as isize + edit.delta()) as usize;
110        let end = (node_end as isize + edit.delta()) as usize;
111        Some(text_range(start, end))
112    } else {
113        // The edit's replaced range intersects the node's interior: invalidated.
114        None
115    }
116}
117
118/// Map a range through a sequence of edits applied left-to-right (the order
119/// [`Edit::apply`] would run them — each edit is expressed against the text
120/// produced by its predecessors). Any single invalidating edit collapses the
121/// whole result to `None`.
122pub fn map_range_through_edits(range: TextRange, edits: &[Edit]) -> Option<TextRange> {
123    edits.iter().try_fold(range, map_range_through_edit)
124}
125
126/// Which strategy produced a [`Reparsed`]. Surfaced for tests and benchmarks.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum ReparseKind {
129    Token,
130    Block,
131    TopLevel,
132    /// A chained multi-edit reparse ([`reparse_edits`]): several single-edit
133    /// strategies applied in sequence. The final tree is still byte-identical to
134    /// a full parse.
135    Multi,
136}
137
138/// The result of a successful incremental reparse: the new whole-file green tree
139/// and its parse diagnostics (absolute offsets in the new text).
140#[derive(Debug, Clone)]
141pub struct Reparsed {
142    pub green: GreenNode,
143    pub diagnostics: Vec<ParseDiagnostic>,
144    pub kind: ReparseKind,
145}
146
147/// Recover a single contiguous [`Edit`] (deleted range in `old` + inserted text)
148/// from a pair of full texts by stripping the common prefix and suffix. Multiple
149/// disjoint edits collapse into one spanning edit — still a correct transform,
150/// just coarser. Boundaries are clamped to char boundaries.
151pub fn diff_edit(old: &str, new: &str) -> Edit {
152    let ob = old.as_bytes();
153    let nb = new.as_bytes();
154
155    let mut prefix = 0;
156    let max_prefix = ob.len().min(nb.len());
157    while prefix < max_prefix && ob[prefix] == nb[prefix] {
158        prefix += 1;
159    }
160    while prefix > 0 && !old.is_char_boundary(prefix) {
161        prefix -= 1;
162    }
163
164    let mut suffix = 0;
165    let max_suffix = (ob.len() - prefix).min(nb.len() - prefix);
166    while suffix < max_suffix && ob[ob.len() - 1 - suffix] == nb[nb.len() - 1 - suffix] {
167        suffix += 1;
168    }
169    while suffix > 0
170        && (!old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix))
171    {
172        suffix -= 1;
173    }
174
175    Edit {
176        range: prefix..(old.len() - suffix),
177        insert: new[prefix..(new.len() - suffix)].to_string(),
178    }
179}
180
181/// Attempt an incremental reparse of `old_root` (parsed from `old_text`, with
182/// `old_diags`) under `edit`. Returns `None` when no incremental strategy applies
183/// — the caller must then do a full parse.
184pub fn reparse(
185    old_root: &SyntaxNode,
186    old_text: &str,
187    old_diags: &[ParseDiagnostic],
188    edit: &Edit,
189) -> Option<Reparsed> {
190    reparse_with_options(
191        old_root,
192        old_text,
193        old_diags,
194        edit,
195        &ParseOptions::default(),
196    )
197}
198
199/// [`reparse`] with the [`ParseOptions`] the old tree was parsed under. A tree
200/// parsed via [`parse_with_options`](crate::parser::core::parse_with_options)
201/// must be reparsed with the **same** options, or the correctness invariant
202/// (reparse output ≡ full parse of the edited text) breaks for roxygen blocks
203/// resolving their markdown mode from the default.
204pub fn reparse_with_options(
205    old_root: &SyntaxNode,
206    old_text: &str,
207    old_diags: &[ParseDiagnostic],
208    edit: &Edit,
209    options: &ParseOptions,
210) -> Option<Reparsed> {
211    let md = options.roxygen_markdown_default;
212    let result = reparse_token(old_root, old_text, old_diags, edit, md)
213        .or_else(|| reparse_block(old_root, old_text, old_diags, edit, md))
214        .or_else(|| reparse_toplevel(old_root, old_text, old_diags, edit, md));
215
216    if let Some(reparsed) = &result {
217        debug_assert_eq!(
218            SyntaxNode::new_root(reparsed.green.clone())
219                .text()
220                .to_string(),
221            edit.apply(old_text),
222            "incremental reparse ({:?}) is not lossless",
223            reparsed.kind,
224        );
225    }
226    result
227}
228
229/// Chain the single-edit [`reparse`] over a sequence of `edits` (each expressed
230/// against the text its predecessors produced, in the order [`Edit::apply`]
231/// would run them). Each step reparses off the previous step's whole-file tree,
232/// so N disjoint edits each reparse their own tight region instead of collapsing
233/// into one wide [`diff_edit`] span (the whole point of feeding precise LSP
234/// edits through — multi-cursor and find-replace).
235///
236/// Returns `None` unless *every* step is incrementally reparseable **and** the
237/// fully-applied text equals `target`. The `== target` check is the Tenet-4
238/// safety net: an edit sequence that does not actually transform `old_text` into
239/// the current buffer (a coalescing gap, an interleaved disk write, a
240/// full-document replacement) is rejected here so the caller falls back to the
241/// always-correct [`diff_edit`] path. A successful result is therefore
242/// byte-identical to a full [`parse`](crate::parser::parse) of `target` — the
243/// composition of correct single-edit reparses over the true edit sequence.
244pub fn reparse_edits(
245    old_root: &SyntaxNode,
246    old_text: &str,
247    old_diags: &[ParseDiagnostic],
248    edits: &[Edit],
249    target: &str,
250) -> Option<Reparsed> {
251    reparse_edits_with_options(
252        old_root,
253        old_text,
254        old_diags,
255        edits,
256        target,
257        &ParseOptions::default(),
258    )
259}
260
261/// [`reparse_edits`] with the [`ParseOptions`] the old tree was parsed under
262/// (see [`reparse_with_options`]).
263pub fn reparse_edits_with_options(
264    old_root: &SyntaxNode,
265    old_text: &str,
266    old_diags: &[ParseDiagnostic],
267    edits: &[Edit],
268    target: &str,
269    options: &ParseOptions,
270) -> Option<Reparsed> {
271    let (first, rest) = edits.split_first()?;
272
273    // Step 0 off the caller's tree; each later step off the prior step's tree.
274    let mut reparsed = reparse_with_options(old_root, old_text, old_diags, first, options)?;
275    let mut text = first.apply(old_text);
276    for edit in rest {
277        let root = SyntaxNode::new_root(reparsed.green.clone());
278        reparsed = reparse_with_options(&root, &text, &reparsed.diagnostics, edit, options)?;
279        text = edit.apply(&text);
280    }
281
282    // Tenet-4 guard: the edits must reconstruct the current buffer exactly.
283    if text != target {
284        return None;
285    }
286    reparsed.kind = ReparseKind::Multi;
287    Some(reparsed)
288}
289
290const TOKEN_REPARSE_KINDS: &[SyntaxKind] = &[
291    SyntaxKind::IDENT,
292    SyntaxKind::STRING,
293    SyntaxKind::COMMENT,
294    SyntaxKind::WHITESPACE,
295    // Roxygen leaves are deliberately absent: they only arise from the `#'`-line
296    // lexer path, so relexing one in isolation (`lex("A number.")`) never yields
297    // a roxygen token and the single-token guard would always bail. Edits inside
298    // a roxygen line therefore fall back to block reparse (when the block sits in
299    // a `{}` body) or a full reparse — correct, just not token-incremental.
300];
301
302fn reparse_token(
303    old_root: &SyntaxNode,
304    old_text: &str,
305    old_diags: &[ParseDiagnostic],
306    edit: &Edit,
307    md_default: bool,
308) -> Option<Reparsed> {
309    let (s, e) = (edit.range.start, edit.range.end);
310    let elem = old_root.covering_element(text_range(s, e));
311    let token = elem.into_token()?;
312    if !TOKEN_REPARSE_KINDS.contains(&token.kind()) {
313        return None;
314    }
315
316    let tr = token.text_range();
317    let (t0, t1) = (usize::from(tr.start()), usize::from(tr.end()));
318    // A pure insertion must land strictly inside the token, so it is
319    // unambiguous which token it extends. Edits with a non-empty range that
320    // span a token boundary never reach here — `covering_element` returns the
321    // parent node, not a token.
322    if s == e && !(t0 < s && s < t1) {
323        return None;
324    }
325
326    // The token's new text, with the edit applied at its relative offset.
327    let mut new_text = token.text().to_string();
328    new_text.replace_range((s - t0)..(e - t0), &edit.insert);
329
330    // Re-lex in isolation: it must still be exactly one token of the same kind.
331    let relexed = lex_with_md(&new_text, md_default);
332    let [only] = relexed.as_slice() else {
333        return None;
334    };
335    if only.start != 0 || only.end != new_text.len() || syntax_kind_for(&only.kind) != token.kind()
336    {
337        return None;
338    }
339
340    // Merge guard: appending the following source character must not extend the
341    // token (which would mean the edit merged it with its neighbor, e.g.
342    // `for`→`fore`, or an unterminated string swallowing the next line).
343    if let Some(next_char) = old_text[t1..].chars().next() {
344        let mut probe = new_text.clone();
345        probe.push(next_char);
346        let probe_toks = lex_with_md(&probe, md_default);
347        let first = probe_toks.first()?;
348        if first.end != new_text.len() || syntax_kind_for(&first.kind) != token.kind() {
349            return None;
350        }
351    }
352
353    // Keep diagnostic remapping trivial: only proceed when no diagnostic touches
354    // the edited token. A clean single-token relex introduces none of its own.
355    if old_diags.iter().any(|d| d.start < t1 && d.end > t0) {
356        return None;
357    }
358    let delta = edit.delta();
359    let diagnostics = old_diags
360        .iter()
361        .map(|d| {
362            if d.start >= t1 {
363                shift(d, delta)
364            } else {
365                d.clone()
366            }
367        })
368        .collect();
369
370    let green_token = GreenToken::new(rowan::SyntaxKind::from(token.kind()), &new_text);
371    let green = token.replace_with(green_token);
372    Some(Reparsed {
373        green,
374        diagnostics,
375        kind: ReparseKind::Token,
376    })
377}
378
379fn reparse_block(
380    old_root: &SyntaxNode,
381    old_text: &str,
382    old_diags: &[ParseDiagnostic],
383    edit: &Edit,
384    md_default: bool,
385) -> Option<Reparsed> {
386    let (s, e) = (edit.range.start, edit.range.end);
387    let elem = old_root.covering_element(text_range(s, e));
388    let start_node = match elem {
389        rowan::NodeOrToken::Node(n) => n,
390        rowan::NodeOrToken::Token(t) => t.parent()?,
391    };
392
393    // Smallest enclosing `{ … }` whose *interior* (between the braces) fully
394    // contains the edit, so the brace delimiters themselves are untouched.
395    let block = start_node.ancestors().find(|node| {
396        if node.kind() != SyntaxKind::BLOCK_EXPR {
397            return false;
398        }
399        let r = node.text_range();
400        let (bstart, bend) = (usize::from(r.start()), usize::from(r.end()));
401        // First/last tokens are `{`/`}` (one byte each); require interior containment.
402        bend.saturating_sub(bstart) >= 2 && s > bstart && e < bend
403    })?;
404
405    let r = block.text_range();
406    let (bstart, bend) = (usize::from(r.start()), usize::from(r.end()));
407
408    // New block text: the old block slice with the edit applied at its relative
409    // offset. The edit is interior, so the slice still starts with `{`/ends `}`.
410    let mut block_text = old_text[bstart..bend].to_string();
411    block_text.replace_range((s - bstart)..(e - bstart), &edit.insert);
412
413    let (block_green, block_diags) = parse_block_in_isolation(&block_text, md_default)?;
414
415    let delta = edit.delta();
416    let mut diagnostics: Vec<ParseDiagnostic> =
417        Vec::with_capacity(old_diags.len() + block_diags.len());
418    for d in old_diags {
419        if d.end <= bstart {
420            diagnostics.push(d.clone());
421        } else if d.start >= bend {
422            diagnostics.push(shift(d, delta));
423        }
424        // diagnostics inside the old block are dropped; the reparse regenerates them
425    }
426    for d in &block_diags {
427        diagnostics.push(ParseDiagnostic {
428            message: d.message.clone(),
429            start: d.start + bstart,
430            end: d.end + bstart,
431        });
432    }
433    diagnostics.sort_by_key(|d| (d.start, d.end));
434
435    let green = block.replace_with(block_green);
436    Some(Reparsed {
437        green,
438        diagnostics,
439        kind: ReparseKind::Block,
440    })
441}
442
443/// Reparse a single top-level statement (a direct child of `ROOT`) that isn't
444/// inside any `{ … }` block. See the module doc for the boundary argument.
445fn reparse_toplevel(
446    old_root: &SyntaxNode,
447    old_text: &str,
448    old_diags: &[ParseDiagnostic],
449    edit: &Edit,
450    md_default: bool,
451) -> Option<Reparsed> {
452    let (s, e) = (edit.range.start, edit.range.end);
453    let elem = old_root.covering_element(text_range(s, e));
454    let start_node = match elem {
455        rowan::NodeOrToken::Node(n) => n,
456        rowan::NodeOrToken::Token(t) => t.parent()?,
457    };
458
459    // The top-level statement: the ancestor whose parent is `ROOT`. When the
460    // covering element is a trivia token directly under `ROOT` (an edit in
461    // inter-statement whitespace), `start_node` *is* the root and there is no such
462    // ancestor — fall back.
463    let stmt = start_node
464        .ancestors()
465        .find(|node| node.parent().map(|p| p.kind()) == Some(SyntaxKind::ROOT))?;
466
467    // Roxygen blocks come from the dedicated `emit_roxygen_block` path, not
468    // `parse_expr`, so reparsing one via `parse_expr` would be wrong. (The kind
469    // match in `parse_stmt_in_isolation` would also reject it, but bail early.)
470    if stmt.kind() == SyntaxKind::ROXYGEN_BLOCK {
471        return None;
472    }
473
474    let r = stmt.text_range();
475    let (ss, se) = (usize::from(r.start()), usize::from(r.end()));
476    // The covering-element walk guarantees the statement contains the edit.
477    debug_assert!(ss <= s && e <= se);
478
479    // New statement text: the old statement slice with the edit applied at its
480    // relative offset. The edit is interior, so the slice still begins at the
481    // statement's first significant token.
482    let mut stmt_text = old_text[ss..se].to_string();
483    stmt_text.replace_range((s - ss)..(e - ss), &edit.insert);
484
485    // Isolation parse: one expression consuming every token, of the same node
486    // kind. Consume-all rejects the *shrink* case (the edit split the statement,
487    // e.g. removing a trailing operator, so some tokens leak past the boundary).
488    let (stmt_green, stmt_diags) = parse_stmt_in_isolation(&stmt_text, stmt.kind(), md_default)?;
489
490    // Forward-merge guard: reparsing the statement with the next sibling's text
491    // appended must still end exactly at the statement's length. Rejects the
492    // *grow* case (the edit made the statement continue onto what follows, e.g. a
493    // trailing binary operator). One sibling of context is sufficient and bounded:
494    // `parse_expr` reads a single expression, so a merge past the boundary is
495    // detected immediately; a statement that doesn't merge into its immediate
496    // successor cannot reach anything further.
497    let forward_end = match stmt.next_sibling() {
498        Some(next) => usize::from(next.text_range().end()),
499        None => old_text.len(),
500    };
501    if !toplevel_boundary_stable(&stmt_text, &old_text[se..forward_end], md_default) {
502        return None;
503    }
504
505    let delta = edit.delta();
506    let mut diagnostics: Vec<ParseDiagnostic> =
507        Vec::with_capacity(old_diags.len() + stmt_diags.len());
508    for d in old_diags {
509        if d.end <= ss {
510            diagnostics.push(d.clone());
511        } else if d.start >= se {
512            diagnostics.push(shift(d, delta));
513        }
514        // diagnostics inside the old statement are dropped; the reparse regenerates them
515    }
516    for d in &stmt_diags {
517        diagnostics.push(ParseDiagnostic {
518            message: d.message.clone(),
519            start: d.start + ss,
520            end: d.end + ss,
521        });
522    }
523    diagnostics.sort_by_key(|d| (d.start, d.end));
524
525    let green = stmt.replace_with(stmt_green);
526    Some(Reparsed {
527        green,
528        diagnostics,
529        kind: ReparseKind::TopLevel,
530    })
531}
532
533/// Parse a bare top-level statement's text on its own and return the green node
534/// for the single top-level node it produces (which must be of `expected_kind`),
535/// with statement-relative diagnostics. Returns `None` unless the text parses to
536/// exactly one expression consuming every token — the consume-all guard.
537fn parse_stmt_in_isolation(
538    stmt_text: &str,
539    expected_kind: SyntaxKind,
540    md_default: bool,
541) -> Option<(GreenNode, Vec<ParseDiagnostic>)> {
542    let tokens = rebalance_brackets(lex_with_md(stmt_text, md_default));
543    let mut diagnostics = Vec::new();
544    let expr = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default)?;
545    if expr.start != 0 || expr.end != tokens.len() {
546        return None;
547    }
548
549    let root = build_tree(&tokens, &expr.events);
550    let mut children = root.children();
551    let node = children.next()?;
552    if children.next().is_some() || node.kind() != expected_kind {
553        return None;
554    }
555    Some((node.green().into_owned(), diagnostics))
556}
557
558/// Whether `stmt_text` still ends at the same boundary once `forward_ctx` (the
559/// text up to the end of the next top-level sibling) is appended. `parse_expr`
560/// over the concatenation must consume a first expression that ends exactly at
561/// `stmt_text.len()`; anything else means the edit made the statement merge
562/// forward. Empty context (statement at end of file) is trivially stable.
563fn toplevel_boundary_stable(stmt_text: &str, forward_ctx: &str, md_default: bool) -> bool {
564    if forward_ctx.is_empty() {
565        return true;
566    }
567    let mut combined = String::with_capacity(stmt_text.len() + forward_ctx.len());
568    combined.push_str(stmt_text);
569    combined.push_str(forward_ctx);
570
571    let tokens = rebalance_brackets(lex_with_md(&combined, md_default));
572    let mut diagnostics = Vec::new();
573    let Some(expr) = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default) else {
574        return false;
575    };
576    // Byte offset one past the last token the first expression consumed. A stable
577    // boundary lands it exactly at the (unchanged) end of the statement text.
578    expr.end
579        .checked_sub(1)
580        .and_then(|last| tokens.get(last))
581        .is_some_and(|last| last.end == stmt_text.len())
582}
583
584/// Parse `{ … }` text on its own and return the green node for the single
585/// `BLOCK_EXPR` it produces, with block-relative diagnostics. Returns `None` if
586/// the text does not parse to exactly one block consuming all tokens (e.g. the
587/// edit transiently unbalanced the braces) — the caller falls back.
588fn parse_block_in_isolation(
589    block_text: &str,
590    md_default: bool,
591) -> Option<(GreenNode, Vec<ParseDiagnostic>)> {
592    let tokens = rebalance_brackets(lex_with_md(block_text, md_default));
593    let mut diagnostics = Vec::new();
594    let expr = parse_expr(&tokens, 0, 0, &mut diagnostics, md_default)?;
595    if expr.start != 0 || expr.end != tokens.len() {
596        return None;
597    }
598
599    let root = build_tree(&tokens, &expr.events);
600    let mut children = root.children();
601    let block = children.next()?;
602    if children.next().is_some() || block.kind() != SyntaxKind::BLOCK_EXPR {
603        return None;
604    }
605
606    // The block must still be properly brace-delimited by its own *direct*
607    // delimiters. If the edit opened an unterminated string/comment or an
608    // unclosed inner `(`/`[` that swallowed the closing `}` (re-parenting it
609    // under a child, or leaking past the old block boundary in a full parse),
610    // the block is no longer self-contained — fall back.
611    let first = block.first_child_or_token().map(|e| e.kind());
612    let last = block.last_child_or_token().map(|e| e.kind());
613    if first != Some(SyntaxKind::LBRACE) || last != Some(SyntaxKind::RBRACE) {
614        return None;
615    }
616
617    Some((block.green().into_owned(), diagnostics))
618}
619
620fn shift(d: &ParseDiagnostic, delta: isize) -> ParseDiagnostic {
621    ParseDiagnostic {
622        message: d.message.clone(),
623        start: (d.start as isize + delta) as usize,
624        end: (d.end as isize + delta) as usize,
625    }
626}
627
628fn text_range(start: usize, end: usize) -> TextRange {
629    TextRange::new(TextSize::new(start as u32), TextSize::new(end as u32))
630}
631
632#[cfg(test)]
633mod map_range_tests {
634    use super::*;
635
636    fn edit(start: usize, end: usize, insert: &str) -> Edit {
637        Edit {
638            range: start..end,
639            insert: insert.to_string(),
640        }
641    }
642
643    #[test]
644    fn node_before_edit_is_unchanged() {
645        let node = text_range(0, 5);
646        assert_eq!(
647            map_range_through_edit(node, &edit(10, 12, "xxxx")),
648            Some(node)
649        );
650    }
651
652    #[test]
653    fn node_after_insertion_shifts_by_delta() {
654        let node = text_range(10, 15);
655        // Insert 4 chars where 2 stood before the node: net +2.
656        assert_eq!(
657            map_range_through_edit(node, &edit(0, 2, "xxxx")),
658            Some(text_range(12, 17))
659        );
660    }
661
662    #[test]
663    fn node_after_deletion_shifts_back() {
664        let node = text_range(10, 15);
665        assert_eq!(
666            map_range_through_edit(node, &edit(0, 4, "")),
667            Some(text_range(6, 11))
668        );
669    }
670
671    #[test]
672    fn edit_ending_at_node_start_shifts_node() {
673        // ee == ns: the node survives and shifts by the delta.
674        let node = text_range(10, 15);
675        assert_eq!(
676            map_range_through_edit(node, &edit(8, 10, "abcd")),
677            Some(text_range(12, 17))
678        );
679    }
680
681    #[test]
682    fn insertion_at_node_start_shifts_node() {
683        let node = text_range(10, 15);
684        assert_eq!(
685            map_range_through_edit(node, &edit(10, 10, "ab")),
686            Some(text_range(12, 17))
687        );
688    }
689
690    #[test]
691    fn insertion_at_node_end_leaves_node_unchanged() {
692        let node = text_range(10, 15);
693        assert_eq!(
694            map_range_through_edit(node, &edit(15, 15, "ab")),
695            Some(node)
696        );
697    }
698
699    #[test]
700    fn insertion_strictly_inside_invalidates() {
701        let node = text_range(10, 15);
702        assert_eq!(map_range_through_edit(node, &edit(12, 12, "z")), None);
703    }
704
705    #[test]
706    fn overlapping_deletion_invalidates() {
707        let node = text_range(10, 15);
708        assert_eq!(map_range_through_edit(node, &edit(12, 18, "")), None);
709    }
710
711    #[test]
712    fn zero_length_node_at_insertion_point_is_unchanged() {
713        // Deterministic boundary rule: a zero-length node at the insertion point
714        // is treated as preceding the insertion.
715        let node = text_range(10, 10);
716        assert_eq!(
717            map_range_through_edit(node, &edit(10, 10, "ab")),
718            Some(node)
719        );
720    }
721
722    #[test]
723    fn append_at_eof_leaves_earlier_node_unchanged() {
724        let node = text_range(0, 5);
725        assert_eq!(
726            map_range_through_edit(node, &edit(5, 5, "\n# trailing")),
727            Some(node)
728        );
729    }
730
731    #[test]
732    fn map_through_edits_folds_in_order() {
733        let node = text_range(10, 15);
734        // Two insertions before the node, each expressed against the prior text.
735        let edits = [edit(0, 0, "ab"), edit(0, 0, "cd")];
736        assert_eq!(
737            map_range_through_edits(node, &edits),
738            Some(text_range(14, 19))
739        );
740    }
741
742    #[test]
743    fn map_through_edits_invalidates_on_any_overlap() {
744        let node = text_range(10, 15);
745        // After the first edit the node is at [12, 17); the second lands inside.
746        let edits = [edit(0, 0, "ab"), edit(13, 13, "z")];
747        assert_eq!(map_range_through_edits(node, &edits), None);
748    }
749
750    #[test]
751    fn apply_edits_folds_each_against_prior_text() {
752        // Two disjoint edits, each expressed against the text its predecessor
753        // produced: prepend "ab", then insert "Z" before "ef" in the now-longer
754        // text (index 4, not 2 — the prepend shifted it).
755        let edits = [edit(0, 0, "ab"), edit(4, 4, "Z")];
756        assert_eq!(apply_edits("cdef", &edits), "abcdZef");
757    }
758
759    #[test]
760    fn apply_edits_empty_is_identity() {
761        assert_eq!(apply_edits("xyz", &[]), "xyz");
762    }
763}