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