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