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}