Skip to main content

ocomment_core/
incremental.rs

1use crate::{
2    ByteSpan, Language, Layout, PreparedScanner, ScanOptions, ScanReport, Severity,
3    TransformOptions, TransformResult,
4    scanner::{
5        RestartRules, preamble_is_settled, scan_until_checkpoint_prepared,
6        scan_with_checkpoints_prepared,
7    },
8    transform::transform_report,
9};
10use thiserror::Error;
11
12/// The units a client counts a position's `character` in.
13///
14/// This is the LSP `positionEncoding` capability. The engine's own
15/// coordinates are always byte offsets; an encoding only says how to read
16/// the numbers a client sends.
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub enum PositionEncoding {
19    /// UTF-8 code units, which are the bytes themselves.
20    Utf8,
21    /// UTF-16 code units, the LSP default and this one too.
22    #[default]
23    Utf16,
24    /// Unicode scalar values, one per character.
25    Utf32,
26}
27
28/// One edit a client made to a document.
29///
30/// The spans of a batch address the document as it stands *before* the
31/// batch, so a client never has to compensate for its own earlier changes.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct DocumentChange {
34    /// The bytes to replace, empty to insert at that offset.
35    pub span: ByteSpan,
36    /// The bytes to put there, empty to delete.
37    pub replacement: Vec<u8>,
38}
39
40/// A document that rescans only what an edit disturbed.
41///
42/// This is the path an editor takes, where a full scan on every keystroke
43/// would be wasted work. The document keeps the previous revision's report
44/// and its restart points, and an edit is answered by scanning from the last
45/// safe point before it up to the first point where the new scan converges
46/// with the old one; everything outside that window is reused, with the spans
47/// past the edit shifted by its length delta.
48///
49/// The result is byte-for-byte the report [`scan`](crate::scan) would have
50/// produced for the same bytes — a restart point is only used while the
51/// bytes around it still permit one, and a scan that fails to converge
52/// simply runs to the end. [`Self::last_rescan_span`] says how much of the
53/// document the last edit actually cost.
54///
55/// # Examples
56///
57/// ```
58/// use ocomment_core::{
59///     ByteSpan, DocumentChange, IncrementalDocument, IncrementalError, Language, ScanOptions,
60/// };
61///
62/// let mut document = IncrementalDocument::new(
63///     b"let x = 1; // note\nlet y = 2;\n".to_vec(),
64///     Language::Rust,
65///     ScanOptions::default(),
66///     1,
67/// );
68/// assert_eq!(document.report().comments.len(), 1);
69///
70/// // Type a second comment onto the end of the second line.
71/// let end = document.source().len() - 1;
72/// let report = document
73///     .apply_changes(
74///         &[DocumentChange {
75///             span: ByteSpan::new(end, end),
76///             replacement: b" // more".to_vec(),
77///         }],
78///         2,
79///     )
80///     .unwrap();
81/// assert_eq!(report.comments.len(), 2);
82///
83/// // A batch that fails validation changes nothing, the version included.
84/// assert_eq!(
85///     document.apply_changes(&[], 2),
86///     Err(IncrementalError::StaleVersion {
87///         received: 2,
88///         current: 2,
89///     }),
90/// );
91/// assert_eq!(document.version(), 2);
92/// assert_eq!(document.report().comments.len(), 2);
93/// ```
94#[derive(Clone, Debug)]
95pub struct IncrementalDocument {
96    source: Vec<u8>,
97    language: Language,
98    options: ScanOptions,
99    prepared: PreparedScanner,
100    report: ScanReport,
101    checkpoints: Vec<usize>,
102    safe_checkpoints: Vec<usize>,
103    version: i64,
104    last_rescan: ByteSpan,
105}
106
107/// Why an edit or a position was refused.
108///
109/// Every one of these is raised before the document is touched, so a refused
110/// call leaves the previous revision intact.
111#[derive(Clone, Debug, Error, Eq, PartialEq)]
112pub enum IncrementalError {
113    /// The batch's version does not advance on the current one, so it
114    /// describes a revision that has already been overtaken.
115    #[error("stale document version {received}; current version is {current}")]
116    StaleVersion {
117        /// The version the batch claimed.
118        received: i64,
119        /// The version the document is already at.
120        current: i64,
121    },
122    /// A change is inverted, starts before its predecessor ends, or reaches
123    /// past the end of the document.
124    #[error("change span lies outside the document")]
125    InvalidSpan,
126    /// The line does not exist, or `character` does not land on a boundary of
127    /// the encoding it was counted in.
128    #[error("position does not lie on a valid encoding boundary")]
129    InvalidPosition,
130}
131
132impl IncrementalDocument {
133    /// Scan `source` once and hold on to what it takes to rescan cheaply.
134    ///
135    /// `version` is the client's revision number for these bytes; every later
136    /// [`Self::apply_changes`] has to advance on it.
137    pub fn new(source: Vec<u8>, language: Language, options: ScanOptions, version: i64) -> Self {
138        let prepared = PreparedScanner::lossy(options.clone());
139        let (report, safe_checkpoints) =
140            scan_with_checkpoints_prepared(&source, language, &prepared, 0);
141        let checkpoints = line_checkpoints(&source);
142        let last_rescan = ByteSpan::new(0, source.len());
143        Self {
144            source,
145            language,
146            options,
147            prepared,
148            report,
149            checkpoints,
150            safe_checkpoints,
151            version,
152            last_rescan,
153        }
154    }
155
156    /// The current bytes of the document.
157    pub fn source(&self) -> &[u8] {
158        &self.source
159    }
160    /// The scan of the current bytes.
161    pub fn report(&self) -> &ScanReport {
162        &self.report
163    }
164    /// The language the document is scanned as, fixed when it was created.
165    pub const fn language(&self) -> Language {
166        self.language
167    }
168    /// The options the document is scanned under, fixed when it was created.
169    pub fn scan_options(&self) -> &ScanOptions {
170        &self.options
171    }
172    /// The bytes a removal would write, from the report already in hand.
173    ///
174    /// No comment is scanned again: this is the current report run through the
175    /// same layout and source-map engine [`transform`](crate::transform) uses.
176    /// A YAML document does get one extra lexical pass in there, because where
177    /// a block scalar body ends decides which comment lines a removal has to
178    /// take whole and no report carries that; it is linear, like the edit walk
179    /// beside it, and every other language skips it on the language check.
180    pub fn transform(&self, layout: Layout) -> TransformResult {
181        transform_report(
182            &self.source,
183            self.report.clone(),
184            TransformOptions {
185                scan: self.options.clone(),
186                layout,
187            },
188        )
189    }
190    /// The revision number of the current bytes.
191    pub const fn version(&self) -> i64 {
192        self.version
193    }
194    /// The stretch of the current bytes the last edit had to rescan.
195    ///
196    /// A fresh document reports the whole source. An edit that reused both
197    /// ends reports only the window between them, which is what makes the
198    /// saving measurable rather than assumed.
199    pub const fn last_rescan_span(&self) -> ByteSpan {
200        self.last_rescan
201    }
202    /// The byte offset each line starts at, `0` first.
203    ///
204    /// A CRLF pair counts as one terminator, so `checkpoints()[n]` is where
205    /// line `n` begins for [`Self::byte_offset`].
206    pub fn checkpoints(&self) -> &[usize] {
207        &self.checkpoints
208    }
209    /// The offsets a rescan may restart from and still reproduce a full scan.
210    ///
211    /// Far fewer than [`Self::checkpoints`]: a line start only qualifies while
212    /// the scanner is in a clean top-level state there and the bytes around it
213    /// keep it that way.
214    pub fn safe_checkpoints(&self) -> &[usize] {
215        &self.safe_checkpoints
216    }
217
218    /// Apply a sorted, non-overlapping batch whose spans refer to the current
219    /// document snapshot. Validation is transactional: an invalid batch does
220    /// not alter the source, report, checkpoints, or version.
221    ///
222    /// An empty batch is accepted and only advances the version, which is what
223    /// a client that saved without typing sends.
224    ///
225    /// # Errors
226    ///
227    /// [`IncrementalError::StaleVersion`] when `version` does not advance on
228    /// the current one, and [`IncrementalError::InvalidSpan`] when a change is
229    /// inverted, starts before its predecessor ends, or reaches past the end
230    /// of the document. Both are raised before anything is written.
231    ///
232    /// # Examples
233    ///
234    /// See [`IncrementalDocument`] for a worked edit.
235    pub fn apply_changes(
236        &mut self,
237        changes: &[DocumentChange],
238        version: i64,
239    ) -> Result<&ScanReport, IncrementalError> {
240        if version <= self.version {
241            return Err(IncrementalError::StaleVersion {
242                received: version,
243                current: self.version,
244            });
245        }
246        let earliest = changes
247            .first()
248            .map_or(self.source.len(), |change| change.span.start);
249        let mut cursor = 0usize;
250        for change in changes {
251            if change.span.start > change.span.end
252                || change.span.start < cursor
253                || change.span.end > self.source.len()
254            {
255                return Err(IncrementalError::InvalidSpan);
256            }
257            cursor = change.span.end;
258        }
259        if changes.is_empty() {
260            self.version = version;
261            self.last_rescan = ByteSpan::new(self.source.len(), self.source.len());
262            return Ok(&self.report);
263        }
264        let output_len = changes.iter().fold(self.source.len(), |length, change| {
265            length
266                .saturating_sub(change.span.len())
267                .saturating_add(change.replacement.len())
268        });
269        let mut next = Vec::with_capacity(output_len);
270        cursor = 0;
271        for change in changes {
272            next.extend_from_slice(&self.source[cursor..change.span.start]);
273            next.extend_from_slice(&change.replacement);
274            cursor = change.span.end;
275        }
276        let old_tail_start = cursor;
277        let new_tail_start = next.len();
278        next.extend_from_slice(&self.source[cursor..]);
279        let can_reuse = self.report.valid;
280        let safe_start = if !can_reuse {
281            0
282        } else {
283            /* INVARIANT: The checkpoints belong to the *previous* revision, and a
284             * checkpoint is only a restart point while the bytes around it
285             * still allow one: an edit that turns line 2 into a Python encoding
286             * declaration, or that splices two C lines together, withdraws that
287             * permission. Every candidate is therefore re-asked against the
288             * edited document, falling back to an earlier checkpoint and
289             * ultimately to a full scan. */
290            let rules = RestartRules::of(&next, self.language);
291            let usable = self
292                .safe_checkpoints
293                .partition_point(|point| *point <= earliest);
294            self.safe_checkpoints[..usable]
295                .iter()
296                .copied()
297                .rev()
298                .find(|point| rules.permit_restart_at(&next, *point))
299                .unwrap_or(0)
300        };
301        let old_convergence = if can_reuse {
302            /* INVARIANT: Converging keeps the previous revision's report for every byte
303             * past the convergence point, shifted by the edit's length delta —
304             * including each comment's kind. Only the preamble rules care where
305             * a comment sits, so the tail may be reused exactly while it lies
306             * past the preamble both where it was and where the edit moves it;
307             * otherwise the scan runs on to the first checkpoint that does. */
308            self.safe_checkpoints.iter().copied().find(|point| {
309                *point >= old_tail_start.max(safe_start)
310                    && preamble_is_settled(&self.source, *point)
311                    && preamble_is_settled(&next, new_tail_start + point - old_tail_start)
312            })
313        } else {
314            None
315        };
316        let mut reused_tail = None;
317        let mut partial = None;
318        if let Some(old_convergence) = old_convergence {
319            let new_convergence = new_tail_start + old_convergence - old_tail_start;
320            /* INVARIANT: The scanner is handed the whole suffix, never a slice cut at the
321             * convergence point: lexical lookahead that reaches past the cut
322             * would otherwise decide differently than it does in the real
323             * document and the rescan would lose comments or diagnostics. */
324            let (report, checkpoints, converged) = scan_until_checkpoint_prepared(
325                &next[safe_start..],
326                self.language,
327                &self.prepared,
328                safe_start,
329                new_convergence,
330            );
331            if converged {
332                reused_tail = Some((old_convergence, new_convergence));
333                partial = Some((report, checkpoints, new_convergence));
334            } else {
335                /* NOTE: Lexical state diverged, so the scan already ran to the end of
336                 * the suffix; that report is exactly the fallback. */
337                partial = Some((report, checkpoints, next.len()));
338            }
339        }
340        let (mut suffix, suffix_checkpoints, rescan_end) = partial.unwrap_or_else(|| {
341            let (report, checkpoints) = scan_with_checkpoints_prepared(
342                &next[safe_start..],
343                self.language,
344                &self.prepared,
345                safe_start,
346            );
347            (report, checkpoints, next.len())
348        });
349        let mut comments: Vec<_> = self
350            .report
351            .comments
352            .iter()
353            .take_while(|comment| comment.span.start < safe_start && comment.span.end <= safe_start)
354            .cloned()
355            .collect();
356        comments.append(&mut suffix.comments);
357        if let Some((old_convergence, new_convergence)) = reused_tail {
358            comments.extend(
359                self.report
360                    .comments
361                    .iter()
362                    .filter(|comment| comment.span.start >= old_convergence)
363                    .cloned()
364                    .map(|mut comment| {
365                        comment.span =
366                            shift_tail_span(comment.span, old_convergence, new_convergence);
367                        comment
368                    }),
369            );
370        }
371        let mut diagnostics: Vec<_> = self
372            .report
373            .diagnostics
374            .iter()
375            .take_while(|diagnostic| {
376                diagnostic.span.start < safe_start && diagnostic.span.end <= safe_start
377            })
378            .cloned()
379            .collect();
380        diagnostics.append(&mut suffix.diagnostics);
381        if let Some((old_convergence, new_convergence)) = reused_tail {
382            diagnostics.extend(
383                self.report
384                    .diagnostics
385                    .iter()
386                    .filter(|diagnostic| diagnostic.span.start >= old_convergence)
387                    .cloned()
388                    .map(|mut diagnostic| {
389                        diagnostic.span =
390                            shift_tail_span(diagnostic.span, old_convergence, new_convergence);
391                        diagnostic
392                    }),
393            );
394        }
395        let report = ScanReport {
396            language: self.language,
397            valid: !diagnostics
398                .iter()
399                .any(|diagnostic| diagnostic.severity == Severity::Error),
400            comments,
401            diagnostics,
402        };
403        let mut safe_checkpoints: Vec<_> = self
404            .safe_checkpoints
405            .iter()
406            .copied()
407            .take_while(|point| *point < safe_start)
408            .collect();
409        safe_checkpoints.extend(suffix_checkpoints);
410        if let Some((old_convergence, new_convergence)) = reused_tail {
411            /* INVARIANT: the converged tail's checkpoints come from the previous
412             * revision, and an edit can grow a lexical construct — a `<` tag,
413             * a quote pair, an XML literal — across one, so each is re-asked
414             * against the edited document exactly as a candidate restart is. */
415            let rules = RestartRules::of(&next, self.language);
416            safe_checkpoints.extend(
417                self.safe_checkpoints
418                    .iter()
419                    .copied()
420                    .filter(|point| *point > old_convergence)
421                    .map(|point| new_convergence + point - old_convergence)
422                    .filter(|point| rules.permit_restart_at(&next, *point)),
423            );
424        }
425        safe_checkpoints.dedup();
426        let checkpoints = line_checkpoints(&next);
427        self.last_rescan = ByteSpan::new(safe_start, rescan_end);
428        self.source = next;
429        self.report = report;
430        self.checkpoints = checkpoints;
431        self.safe_checkpoints = safe_checkpoints;
432        self.version = version;
433        Ok(&self.report)
434    }
435
436    /// The byte offset of a line-and-character position.
437    ///
438    /// `line` is zero-based, and `character` is a zero-based offset into that
439    /// line counted in the units `encoding` names. The end of a line is a
440    /// valid position; the terminator itself is not part of the line.
441    ///
442    /// # Errors
443    ///
444    /// [`IncrementalError::InvalidPosition`] when the line does not exist,
445    /// when `character` reaches past the end of the line, or when it lands
446    /// inside a character instead of on a boundary. A line whose bytes are
447    /// not valid UTF-8 has no UTF-16 or UTF-32 positions at all.
448    pub fn byte_offset(
449        &self,
450        line: u32,
451        character: u32,
452        encoding: PositionEncoding,
453    ) -> Result<usize, IncrementalError> {
454        let start = *self
455            .checkpoints
456            .get(line as usize)
457            .ok_or(IncrementalError::InvalidPosition)?;
458        let raw_end = self
459            .checkpoints
460            .get(line as usize + 1)
461            .copied()
462            .unwrap_or(self.source.len());
463        let end = if raw_end > start && self.source.get(raw_end - 1) == Some(&b'\n') {
464            if raw_end > start + 1 && self.source.get(raw_end - 2) == Some(&b'\r') {
465                raw_end - 2
466            } else {
467                raw_end - 1
468            }
469        } else if raw_end > start && self.source.get(raw_end - 1) == Some(&b'\r') {
470            raw_end - 1
471        } else {
472            raw_end
473        };
474        let line_bytes = &self.source[start..end];
475        match encoding {
476            PositionEncoding::Utf8 => {
477                let offset = start + character as usize;
478                if offset <= end && std::str::from_utf8(&self.source[start..offset]).is_ok() {
479                    Ok(offset)
480                } else {
481                    Err(IncrementalError::InvalidPosition)
482                }
483            }
484            PositionEncoding::Utf16 | PositionEncoding::Utf32 => {
485                let text = std::str::from_utf8(line_bytes)
486                    .map_err(|_| IncrementalError::InvalidPosition)?;
487                let mut units = 0u32;
488                for (relative, ch) in text.char_indices() {
489                    if units == character {
490                        return Ok(start + relative);
491                    }
492                    units += if encoding == PositionEncoding::Utf16 {
493                        ch.len_utf16() as u32
494                    } else {
495                        1
496                    };
497                    if units > character {
498                        return Err(IncrementalError::InvalidPosition);
499                    }
500                }
501                if units == character {
502                    Ok(end)
503                } else {
504                    Err(IncrementalError::InvalidPosition)
505                }
506            }
507        }
508    }
509}
510
511fn shift_tail_span(span: ByteSpan, old_base: usize, new_base: usize) -> ByteSpan {
512    ByteSpan::new(
513        new_base + span.start - old_base,
514        new_base + span.end - old_base,
515    )
516}
517
518fn line_checkpoints(source: &[u8]) -> Vec<usize> {
519    let mut lines = vec![0];
520    let mut index = 0;
521    while index < source.len() {
522        if source[index] == b'\r' && source.get(index + 1) == Some(&b'\n') {
523            index += 2;
524            lines.push(index);
525        } else if matches!(source[index], b'\r' | b'\n') {
526            index += 1;
527            lines.push(index);
528        } else {
529            index += 1;
530        }
531    }
532    lines
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use crate::{
539        Disposition,
540        scanner::{scan_checkpoint_watermarks, scan_with_checkpoints},
541    };
542    use proptest::{prelude::*, sample::select};
543
544    /// A pool length as a `prop_oneof!` weight, so that drawing uniformly from
545    /// a pool of `n` gives each of its members the weight one arm would have.
546    fn weight(length: usize) -> u32 {
547        u32::try_from(length).expect("the pool is far smaller than a weight")
548    }
549
550    /// One byte of the shared pool, or a uniformly random one.
551    ///
552    /// The pool is [`crate::lexical_pool::BYTES`], and `tests/properties.rs`
553    /// draws from the same one: a fragment worth generating against the
554    /// whole-file scanner is worth generating against the incremental one. The
555    /// extra `\n` arm doubles that byte's weight, because a line boundary is
556    /// where a checkpoint may be offered and every one of them is a restart
557    /// this suite gets to try.
558    fn lexical_byte() -> impl Strategy<Value = u8> {
559        prop_oneof![
560            4 => any::<u8>(),
561            1 => Just(b'\n'),
562            weight(crate::lexical_pool::BYTES.len()) => select(crate::lexical_pool::BYTES),
563        ]
564    }
565
566    /// A fragment: one byte of the pool, or one whole token from it.
567    ///
568    /// The tokens are [`crate::lexical_pool::TOKENS`] — multi-byte openers a
569    /// single-byte alphabet can never synthesise — and each is drawn as often
570    /// as one byte is, which is what the eight-to-one weight in front of the
571    /// byte arm keeps in proportion.
572    fn lexical_fragment() -> impl Strategy<Value = Vec<u8>> {
573        prop_oneof![
574            8 => lexical_byte().prop_map(|byte| vec![byte]),
575            weight(crate::lexical_pool::TOKENS.len()) => select(crate::lexical_pool::TOKENS)
576                .prop_map(<[u8]>::to_vec),
577        ]
578    }
579
580    /// A source built from at most `fragments` raw bytes and literal tokens.
581    fn lexical_source(fragments: std::ops::Range<usize>) -> impl Strategy<Value = Vec<u8>> {
582        prop::collection::vec(lexical_fragment(), fragments)
583            .prop_map(|fragments| fragments.concat())
584    }
585
586    /// One end of an edit span, drawn with a heavy bias towards the two
587    /// document boundaries. The degenerate spans live there — an empty edit at
588    /// offset 0, an append at the end, a replacement that swallows the whole
589    /// document — and a uniform draw finds them only as often as it finds any
590    /// other single offset.
591    fn edit_endpoint() -> impl Strategy<Value = usize> {
592        prop_oneof![
593            1 => Just(0usize),
594            1 => Just(usize::MAX),
595            2 => any::<usize>(),
596        ]
597    }
598
599    /// Place a drawn endpoint in `source`. The document length is unknown when
600    /// the endpoint is drawn, so `usize::MAX` is the name of its end; every
601    /// other draw wraps into the document and keeps the interior offsets
602    /// spread evenly.
603    fn endpoint(source: &[u8], drawn: usize) -> usize {
604        if drawn == usize::MAX {
605            source.len()
606        } else {
607            drawn % (source.len() + 1)
608        }
609    }
610
611    /// The endpoint mapping has to reach both boundaries exactly, or the
612    /// biased draws below would still miss the degenerate spans they exist to
613    /// produce.
614    #[test]
615    fn an_edit_endpoint_reaches_both_document_boundaries() {
616        let source = b"// comment\n";
617        assert_eq!(endpoint(source, 0), 0);
618        assert_eq!(endpoint(source, usize::MAX), source.len());
619        assert_eq!(endpoint(source, source.len()), source.len());
620        assert_eq!(endpoint(b"", usize::MAX), 0);
621        assert!(endpoint(source, 12345) <= source.len());
622    }
623
624    #[test]
625    fn an_unmatched_markdown_code_span_withdraws_later_line_checkpoints() {
626        let mut document = IncrementalDocument::new(
627            b"text ```open\nnext".to_vec(),
628            Language::Markdown,
629            ScanOptions::default(),
630            1,
631        );
632        assert_eq!(document.safe_checkpoints(), [0]);
633        let end = document.source().len();
634        document
635            .apply_changes(
636                &[DocumentChange {
637                    span: ByteSpan::new(end, end),
638                    replacement: b"```".to_vec(),
639                }],
640                2,
641            )
642            .unwrap();
643        let (expected, expected_checkpoints) = scan_with_checkpoints(
644            document.source(),
645            Language::Markdown,
646            ScanOptions::default(),
647            0,
648        );
649        assert_eq!(document.report(), &expected);
650        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
651    }
652
653    proptest! {
654        /* NOTE: Unit-test proptests cannot persist regressions next to `src`, so the
655         * shrunk counterexample is reported inline instead. */
656        #![proptest_config(ProptestConfig { failure_persistence: None, ..ProptestConfig::default() })]
657
658        /// Every safe checkpoint must be a restart point: scanning the suffix
659        /// that begins there, at that offset, has to reproduce exactly the part
660        /// of the full scan that begins there. The incremental engine reuses the
661        /// prefix of the previous report on the strength of this invariant, so a
662        /// checkpoint that is not a clean lexical state silently corrupts a
663        /// rescan.
664        ///
665        /// The rescan is the observable half. The other half is the mechanism
666        /// under it: no checkpoint may stand at or before the furthest byte any
667        /// decision made before it consulted. A rescan that agrees only because
668        /// the lookahead which read across the checkpoint happens to re-lex the
669        /// same bytes and reach the same answer is agreeing by luck, and the
670        /// luck runs out at the next lookahead — so the watermark is asserted
671        /// directly, per language, before the restarts are tried.
672        #[test]
673        fn safe_checkpoints_restart_every_builtin_scan_exactly(
674            source in lexical_source(0..48),
675        ) {
676            for language in Language::ALL {
677                for (point, consulted) in
678                    scan_checkpoint_watermarks(&source, language, ScanOptions::default())
679                {
680                    prop_assert!(
681                        consulted <= point,
682                        "{} offers a checkpoint at {} that a decision before it read through {}",
683                        language, point, consulted,
684                    );
685                }
686                let (full, checkpoints) =
687                    scan_with_checkpoints(&source, language, ScanOptions::default(), 0);
688                for point in checkpoints.iter().copied() {
689                    let (suffix, suffix_checkpoints) = scan_with_checkpoints(
690                        &source[point..],
691                        language,
692                        ScanOptions::default(),
693                        point,
694                    );
695                    let comments: Vec<_> = full
696                        .comments
697                        .iter()
698                        .filter(|comment| comment.span.start >= point)
699                        .cloned()
700                        .collect();
701                    let diagnostics: Vec<_> = full
702                        .diagnostics
703                        .iter()
704                        .filter(|diagnostic| diagnostic.span.start >= point)
705                        .cloned()
706                        .collect();
707                    let tail: Vec<_> = checkpoints
708                        .iter()
709                        .copied()
710                        .filter(|candidate| *candidate >= point)
711                        .collect();
712                    prop_assert_eq!(
713                        &suffix.comments, &comments,
714                        "{} comments diverge restarting at {}", language, point,
715                    );
716                    prop_assert_eq!(
717                        &suffix.diagnostics, &diagnostics,
718                        "{} diagnostics diverge restarting at {}", language, point,
719                    );
720                    prop_assert_eq!(
721                        &suffix_checkpoints, &tail,
722                        "{} checkpoints diverge restarting at {}", language, point,
723                    );
724                }
725            }
726        }
727
728        /// The cross-edit form of the same invariant. A checkpoint is chosen
729        /// from the *previous* document's list, so it also has to survive the
730        /// edit: after `apply_changes` the document must be indistinguishable
731        /// from a full scan of the edited bytes — comments, diagnostics,
732        /// validity and the checkpoint list alike.
733        #[test]
734        fn arbitrary_edits_leave_every_builtin_document_equal_to_a_full_scan(
735            source in lexical_source(0..48),
736            replacement in lexical_source(0..8),
737            first in edit_endpoint(),
738            second in edit_endpoint(),
739        ) {
740            let left = endpoint(&source, first);
741            let right = endpoint(&source, second);
742            let span = ByteSpan::new(left.min(right), left.max(right));
743            for language in Language::ALL {
744                let mut document = IncrementalDocument::new(
745                    source.clone(),
746                    language,
747                    ScanOptions::default(),
748                    1,
749                );
750                document.apply_changes(&[DocumentChange {
751                    span,
752                    replacement: replacement.clone(),
753                }], 2).unwrap();
754                let (full, checkpoints) = scan_with_checkpoints(
755                    document.source(),
756                    language,
757                    ScanOptions::default(),
758                    0,
759                );
760                prop_assert_eq!(
761                    &document.report().comments, &full.comments,
762                    "{} comments diverge after editing {:?}", language, span,
763                );
764                prop_assert_eq!(
765                    &document.report().diagnostics, &full.diagnostics,
766                    "{} diagnostics diverge after editing {:?}", language, span,
767                );
768                prop_assert_eq!(
769                    document.report().valid, full.valid,
770                    "{} validity diverges after editing {:?}", language, span,
771                );
772                prop_assert_eq!(
773                    document.report(), &full,
774                    "{} report diverges after editing {:?}", language, span,
775                );
776                prop_assert_eq!(
777                    document.safe_checkpoints(), &checkpoints[..],
778                    "{} checkpoints diverge after editing {:?}", language, span,
779                );
780            }
781        }
782    }
783
784    /// Regression: Python emitted a safe checkpoint at the start of line 2, but
785    /// an encoding declaration is only recognised while scanning from offset 0,
786    /// so a rescan that restarted there demoted `# coding:` from `Encoding`
787    /// (kept) to a plain line comment (removed).
788    #[test]
789    fn a_rescan_never_demotes_a_second_line_python_encoding_declaration() {
790        let source = b"value = 1\n# coding: latin-1\ntail = 2\n".to_vec();
791        let mut document =
792            IncrementalDocument::new(source, Language::Python, ScanOptions::default(), 1);
793        document
794            .apply_changes(
795                &[DocumentChange {
796                    span: ByteSpan::new(26, 27),
797                    replacement: b"2".to_vec(),
798                }],
799                2,
800            )
801            .unwrap();
802        let (expected, expected_checkpoints) = scan_with_checkpoints(
803            document.source(),
804            Language::Python,
805            ScanOptions::default(),
806            0,
807        );
808        assert_eq!(document.report().comments, expected.comments);
809        assert_eq!(document.report(), &expected);
810        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
811    }
812
813    /// Ruby reads a source-encoding declaration out of the same two lines
814    /// Python does, and only while scanning from offset 0, so the start of line
815    /// 2 is a restart point for it under exactly the same condition. A rescan
816    /// that restarted there anyway would demote `# coding:` from `Encoding`
817    /// (kept) to a plain line comment (removed).
818    #[test]
819    fn a_rescan_never_demotes_a_second_line_ruby_encoding_declaration() {
820        let source = b"value = 1\n# coding: latin-1\ntail = 2\n".to_vec();
821        let mut document =
822            IncrementalDocument::new(source, Language::Ruby, ScanOptions::default(), 1);
823        /* NOTE: The edit falls inside the declaration itself, so the last
824         * checkpoint before it is the start of line 2 — the one offset this
825         * rule exists to refuse. */
826        document
827            .apply_changes(
828                &[DocumentChange {
829                    span: ByteSpan::new(26, 27),
830                    replacement: b"2".to_vec(),
831                }],
832                2,
833            )
834            .unwrap();
835        let (expected, expected_checkpoints) =
836            scan_with_checkpoints(document.source(), Language::Ruby, ScanOptions::default(), 0);
837        assert_eq!(
838            document.report().comments[0].kind,
839            crate::CommentKind::Encoding,
840            "{:?}",
841            document.report().comments,
842        );
843        assert_eq!(document.report(), &expected);
844        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
845    }
846
847    /// Regression: an edit that closes a tag the earlier scan had read as text
848    /// must withdraw every checkpoint the old reading offered inside it. A
849    /// tag's attributes can hold `<` of their own — `<div a="<b>"` reads the
850    /// second `<` as text inside the first tag's quotes — so a walk back to
851    /// the nearest `<` alone stands inside the outer tag once the edit
852    /// supplies its `>`; every `<` before the offset has to close.
853    #[test]
854    fn an_edit_that_closes_a_tag_withdraws_the_checkpoints_inside_it() {
855        let source = b"<div a=\"<b>\"\nline2\n".to_vec();
856        assert_eq!(source.len(), 19);
857        let mut document =
858            IncrementalDocument::new(source, Language::Vue, ScanOptions::default(), 1);
859        /* NOTE: the span is the document's end, and the replacement is the
860         * `>` the outer tag had been missing, which closes the tag across
861         * both checkpoints the unclosed tag had let stand. */
862        document
863            .apply_changes(
864                &[DocumentChange {
865                    span: ByteSpan::new(19, 19),
866                    replacement: b">".to_vec(),
867                }],
868                2,
869            )
870            .unwrap();
871        let (expected, expected_checkpoints) =
872            scan_with_checkpoints(document.source(), Language::Vue, ScanOptions::default(), 0);
873        assert_eq!(document.report().comments, expected.comments);
874        assert_eq!(document.report(), &expected);
875        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
876    }
877
878    /// A here document body and an embedded document are two Ruby states whose
879    /// lines say nothing about themselves: the `#` at the head of one is a byte
880    /// of the value, and the line that decides so sits above it. Neither offers
881    /// a restart point, so the checkpoints of such a file are the line starts
882    /// outside them and nothing else.
883    #[test]
884    fn a_ruby_line_start_inside_an_opaque_construct_is_no_restart_point() {
885        let heredoc = IncrementalDocument::new(
886            b"a = <<~EOS\n  # opaque\n  EOS\nb = 2\n".to_vec(),
887            Language::Ruby,
888            ScanOptions::default(),
889            1,
890        );
891        assert_eq!(heredoc.safe_checkpoints(), [0, 28, 34]);
892
893        let document = IncrementalDocument::new(
894            b"=begin\n# opaque\n=end\nb = 2\n".to_vec(),
895            Language::Ruby,
896            ScanOptions::default(),
897            1,
898        );
899        assert_eq!(document.safe_checkpoints(), [0, 21, 27]);
900
901        /* NOTE: The DATA section behind the marker is not source at all, so the
902         * marker's own line break is the last restart point there is. */
903        let data = IncrementalDocument::new(
904            b"a = 1\n__END__\nnot source\n".to_vec(),
905            Language::Ruby,
906            ScanOptions::default(),
907            1,
908        );
909        assert_eq!(data.safe_checkpoints(), [0, 6]);
910    }
911
912    /// Regression: `safe_start` was chosen from the *previous* document's
913    /// checkpoint list and never re-validated against the edited bytes, so an
914    /// edit that turned line 2 into a Python encoding declaration restarted the
915    /// scan at a checkpoint the edited document no longer admits and demoted
916    /// the declaration from `Encoding` (kept) to a line comment (removed).
917    #[test]
918    fn an_edit_that_creates_an_encoding_declaration_invalidates_the_reused_checkpoint() {
919        let source = b"value = 1\n# note\ntail\n".to_vec();
920        let mut document =
921            IncrementalDocument::new(source, Language::Python, ScanOptions::default(), 1);
922        document
923            .apply_changes(
924                &[DocumentChange {
925                    span: ByteSpan::new(10, 16),
926                    replacement: b"# coding: latin-1".to_vec(),
927                }],
928                2,
929            )
930            .unwrap();
931        let (expected, expected_checkpoints) = scan_with_checkpoints(
932            document.source(),
933            Language::Python,
934            ScanOptions::default(),
935            0,
936        );
937        assert_eq!(
938            document.report().comments[0].kind,
939            crate::CommentKind::Encoding
940        );
941        assert!(!document.report().comments[0].disposition.is_remove());
942        assert_eq!(document.report().comments, expected.comments);
943        assert_eq!(document.report(), &expected);
944        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
945    }
946
947    /// The same shape, stated without naming a language: whenever an edit
948    /// rewrites a line whose bytes decide whether an earlier checkpoint is a
949    /// restart point, the engine must agree with a full scan of the edited
950    /// document. Only Python's encoding rule has that property today, so the
951    /// loop also guards every other built-in against acquiring one silently.
952    #[test]
953    fn edits_to_a_preamble_line_never_reuse_a_checkpoint_the_edit_invalidates() {
954        for language in Language::ALL {
955            for replacement in [
956                &b"# coding: latin-1"[..],
957                b"# -*- coding: utf-8 -*-",
958                b"#!/bin/sh",
959                b"//go:build linux",
960                b"/*#__PURE__*/",
961            ] {
962                let mut document = IncrementalDocument::new(
963                    b"value = 1\n# note\ntail\n".to_vec(),
964                    language,
965                    ScanOptions::default(),
966                    1,
967                );
968                document
969                    .apply_changes(
970                        &[DocumentChange {
971                            span: ByteSpan::new(10, 16),
972                            replacement: replacement.to_vec(),
973                        }],
974                        2,
975                    )
976                    .unwrap();
977                let (expected, expected_checkpoints) =
978                    scan_with_checkpoints(document.source(), language, ScanOptions::default(), 0);
979                let token = String::from_utf8_lossy(replacement).into_owned();
980                assert_eq!(
981                    document.report(),
982                    &expected,
983                    "{language} report diverges after inserting {token}",
984                );
985                assert_eq!(
986                    document.safe_checkpoints(),
987                    expected_checkpoints,
988                    "{language} checkpoints diverge after inserting {token}",
989                );
990            }
991        }
992    }
993
994    /// Regression: the reused *tail* carries the previous revision's
995    /// classification, and a shebang is a shebang only at absolute offset 0.
996    /// Inserting a line in front of one used to shift the old `Shebang` comment
997    /// down and keep it, where a full scan of the edited bytes sees an ordinary
998    /// line comment.
999    #[test]
1000    fn an_edit_that_pushes_a_shebang_off_offset_zero_stops_reusing_its_kind() {
1001        let mut document = IncrementalDocument::new(
1002            b"#!/bin/sh\nvalue\n".to_vec(),
1003            Language::Shell,
1004            ScanOptions::default(),
1005            1,
1006        );
1007        document
1008            .apply_changes(
1009                &[DocumentChange {
1010                    span: ByteSpan::new(0, 0),
1011                    replacement: b"\n".to_vec(),
1012                }],
1013                2,
1014            )
1015            .unwrap();
1016        let (expected, expected_checkpoints) = scan_with_checkpoints(
1017            document.source(),
1018            Language::Shell,
1019            ScanOptions::default(),
1020            0,
1021        );
1022        assert_eq!(document.report().comments, expected.comments);
1023        assert_eq!(document.report(), &expected);
1024        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1025    }
1026
1027    /// The mirror image: deleting the lines in front of a `#!` line pulls it to
1028    /// offset 0, where a full scan reads a shebang, so the previous revision's
1029    /// ordinary line comment must not be reused either.
1030    #[test]
1031    fn an_edit_that_pulls_a_hashbang_line_to_offset_zero_stops_reusing_its_kind() {
1032        let mut document = IncrementalDocument::new(
1033            b"x\n#!/bin/sh\ntail\n".to_vec(),
1034            Language::Shell,
1035            ScanOptions::default(),
1036            1,
1037        );
1038        document
1039            .apply_changes(
1040                &[DocumentChange {
1041                    span: ByteSpan::new(0, 2),
1042                    replacement: Vec::new(),
1043                }],
1044                2,
1045            )
1046            .unwrap();
1047        let (expected, expected_checkpoints) = scan_with_checkpoints(
1048            document.source(),
1049            Language::Shell,
1050            ScanOptions::default(),
1051            0,
1052        );
1053        assert_eq!(document.report().comments, expected.comments);
1054        assert_eq!(document.report(), &expected);
1055        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1056    }
1057
1058    /// Regression: C and C++ splice `\\<newline>` out of the input before
1059    /// lexing, and a spliced document is scanned through a remapped copy that
1060    /// tracks no checkpoints at all — a full scan of it offers offset 0 and
1061    /// nothing else. An edit that introduces a splice therefore invalidates
1062    /// every checkpoint the previous revision recorded, but the reused one was
1063    /// never re-checked against the edited bytes, so the document went on
1064    /// advertising a restart point the edited source no longer has.
1065    #[test]
1066    fn an_edit_that_introduces_a_c_line_splice_invalidates_every_checkpoint() {
1067        let mut document = IncrementalDocument::new(
1068            b"int a;\nx\n/ hidden\nint c;\n".to_vec(),
1069            Language::C,
1070            ScanOptions::default(),
1071            1,
1072        );
1073        document
1074            .apply_changes(
1075                &[DocumentChange {
1076                    span: ByteSpan::new(7, 8),
1077                    replacement: b"/\\".to_vec(),
1078                }],
1079                2,
1080            )
1081            .unwrap();
1082        let (expected, expected_checkpoints) =
1083            scan_with_checkpoints(document.source(), Language::C, ScanOptions::default(), 0);
1084        assert_eq!(document.report().comments, expected.comments);
1085        assert_eq!(document.report(), &expected);
1086        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1087    }
1088
1089    /// Regression: how far a YAML block scalar body reaches is decided by the
1090    /// lines below it, so an edit under one can swallow an offset the previous
1091    /// revision recorded as a line start — appending a line to a document that
1092    /// ended inside a body is enough, and a restart there would read the
1093    /// content of a scalar as YAML. No body begins before its own header, so
1094    /// the checkpoints a YAML document offers stop at the first one, and a
1095    /// document that opens none offers every line start as before.
1096    #[test]
1097    fn a_yaml_block_scalar_ends_the_checkpoints_of_the_document_it_opens() {
1098        let plain = IncrementalDocument::new(
1099            b"a: 1\nb: 2 # note\n".to_vec(),
1100            Language::Yaml,
1101            ScanOptions::default(),
1102            1,
1103        );
1104        assert_eq!(plain.safe_checkpoints(), [0, 5, 17]);
1105
1106        let mut document = IncrementalDocument::new(
1107            b"key: |\n  body # content\n".to_vec(),
1108            Language::Yaml,
1109            ScanOptions::default(),
1110            1,
1111        );
1112        assert_eq!(document.safe_checkpoints(), [0]);
1113        document
1114            .apply_changes(
1115                &[DocumentChange {
1116                    span: ByteSpan::new(24, 24),
1117                    replacement: b"  more # content\n".to_vec(),
1118                }],
1119                2,
1120            )
1121            .unwrap();
1122        let (expected, expected_checkpoints) =
1123            scan_with_checkpoints(document.source(), Language::Yaml, ScanOptions::default(), 0);
1124        assert_eq!(document.report(), &expected);
1125        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1126        assert!(
1127            document.report().comments.is_empty(),
1128            "the body swallowed both lines: {:?}",
1129            document.report().comments
1130        );
1131    }
1132
1133    /// The keep a block scalar's trail decides is a property of the whole
1134    /// document, so a rescan that reuses a tail has to reach the same one. The
1135    /// checkpoints a YAML document offers stop at its first block scalar, which
1136    /// puts every trail inside the suffix a rescan reads or inside the tail it
1137    /// carries over untouched; either way the answer is the full scan's.
1138    #[test]
1139    fn a_yaml_structural_trail_keep_survives_an_incremental_rescan() {
1140        let source = b"a: 1
1141k: |
1142  x
1143# ends the block
1144  # yamllint disable
1145z: 1
1146";
1147        let mut document =
1148            IncrementalDocument::new(source.to_vec(), Language::Yaml, ScanOptions::default(), 1);
1149        assert_eq!(
1150            document.report().comments[0].disposition,
1151            Disposition::Keep {
1152                reason: "structural in a YAML block scalar trail".to_owned()
1153            },
1154        );
1155        /* NOTE: Deepening the body past the directive under it takes the value
1156         * away from that directive, and the comment above it stops being
1157         * structure the moment it does. */
1158        let deepen = source
1159            .windows(4)
1160            .position(|window| window == b"\n  x")
1161            .expect("the body line");
1162        document
1163            .apply_changes(
1164                &[DocumentChange {
1165                    span: ByteSpan::new(deepen + 1, deepen + 1),
1166                    replacement: b"  ".to_vec(),
1167                }],
1168                2,
1169            )
1170            .unwrap();
1171        let (expected, expected_checkpoints) =
1172            scan_with_checkpoints(document.source(), Language::Yaml, ScanOptions::default(), 0);
1173        assert_eq!(document.report(), &expected);
1174        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1175        assert!(
1176            document.report().comments[0].disposition.is_remove(),
1177            "the directive is outside the deeper body: {:?}",
1178            document.report().comments,
1179        );
1180    }
1181
1182    /// PHP mode is document state rather than line state: whether the `#` at a
1183    /// line start opens a comment depends on whether an unclosed `<?php` sits
1184    /// above it, and the bytes of the line itself say nothing about that. Only
1185    /// a line break the scanner meets in inline HTML is a restart point, so a
1186    /// file that is all PHP offers offset 0 and nothing else and a template
1187    /// offers the line starts of its HTML.
1188    #[test]
1189    fn a_php_line_start_is_a_restart_point_only_in_inline_html() {
1190        let html = IncrementalDocument::new(
1191            b"<p>a</p>\n<p>b</p>\n".to_vec(),
1192            Language::Php,
1193            ScanOptions::default(),
1194            1,
1195        );
1196        assert_eq!(html.safe_checkpoints(), [0, 9, 18]);
1197
1198        let code = IncrementalDocument::new(
1199            b"<?php\n$a = 1;\n$b = 2;\n".to_vec(),
1200            Language::Php,
1201            ScanOptions::default(),
1202            1,
1203        );
1204        assert_eq!(code.safe_checkpoints(), [0]);
1205
1206        /* NOTE: The line break behind a `?>` belongs to the tag, so the byte
1207         * after it is the start of the first inline-HTML line and a restart
1208         * point like any other. */
1209        let mut template = IncrementalDocument::new(
1210            b"<?php $a = 1; ?>\n<p>x</p>\n".to_vec(),
1211            Language::Php,
1212            ScanOptions::default(),
1213            1,
1214        );
1215        assert_eq!(template.safe_checkpoints(), [0, 17, 26]);
1216        template
1217            .apply_changes(
1218                &[DocumentChange {
1219                    span: ByteSpan::new(26, 26),
1220                    replacement: b"<?php # note\n".to_vec(),
1221                }],
1222                2,
1223            )
1224            .unwrap();
1225        let (expected, expected_checkpoints) =
1226            scan_with_checkpoints(template.source(), Language::Php, ScanOptions::default(), 0);
1227        assert_eq!(template.report(), &expected);
1228        assert_eq!(template.safe_checkpoints(), expected_checkpoints);
1229    }
1230
1231    /// Regression: a checkpoint sits immediately after a line terminator, and
1232    /// CRLF is one terminator. Inserting the LF of a CRLF pair right after an
1233    /// existing CR moves the boundary one byte on, so the offset the previous
1234    /// revision recorded now splits the pair — a full scan never offers it, and
1235    /// restarting there would resume in the middle of a line ending.
1236    #[test]
1237    fn an_edit_that_completes_a_crlf_pair_invalidates_the_checkpoint_it_splits() {
1238        let mut document = IncrementalDocument::new(
1239            b"let x = 1;\r\rlet y = 2;\r".to_vec(),
1240            Language::Rust,
1241            ScanOptions::default(),
1242            1,
1243        );
1244        assert_eq!(document.safe_checkpoints()[..3], [0, 11, 12]);
1245        document
1246            .apply_changes(
1247                &[DocumentChange {
1248                    span: ByteSpan::new(11, 11),
1249                    replacement: b"\n".to_vec(),
1250                }],
1251                2,
1252            )
1253            .unwrap();
1254        let (expected, expected_checkpoints) =
1255            scan_with_checkpoints(document.source(), Language::Rust, ScanOptions::default(), 0);
1256        assert_eq!(document.report(), &expected);
1257        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1258    }
1259
1260    #[test]
1261    fn incremental_matches_full_scan() {
1262        let mut document = IncrementalDocument::new(
1263            b"let x = 1; // old\n".to_vec(),
1264            Language::Rust,
1265            ScanOptions::default(),
1266            1,
1267        );
1268        document
1269            .apply_changes(
1270                &[DocumentChange {
1271                    span: ByteSpan::new(14, 17),
1272                    replacement: b"new".to_vec(),
1273                }],
1274                2,
1275            )
1276            .unwrap();
1277        assert_eq!(
1278            document.report(),
1279            &crate::scan(document.source(), Language::Rust, ScanOptions::default())
1280        );
1281        assert_eq!(
1282            document.transform(Layout::Lines),
1283            crate::transform(
1284                document.source(),
1285                Language::Rust,
1286                TransformOptions::default()
1287            )
1288        );
1289    }
1290
1291    #[test]
1292    fn rescans_from_a_lexically_safe_line_checkpoint() {
1293        let source =
1294            b"let text = r#\"first\nsecond\"#;\nlet value = 1; // old\nlet tail = 2; // tail\n"
1295                .to_vec();
1296        let mut document =
1297            IncrementalDocument::new(source, Language::Rust, ScanOptions::default(), 1);
1298        let comment = document.report().comments[0].span;
1299        document
1300            .apply_changes(
1301                &[DocumentChange {
1302                    span: ByteSpan::new(comment.start + 3, comment.end),
1303                    replacement: b"newer".to_vec(),
1304                }],
1305                2,
1306            )
1307            .unwrap();
1308        assert!(document.last_rescan_span().start > 0);
1309        assert!(document.last_rescan_span().start > b"let text = r#\"first\n".len());
1310        assert!(document.last_rescan_span().end < document.source().len());
1311        assert_eq!(
1312            document.report(),
1313            &crate::scan(document.source(), Language::Rust, ScanOptions::default())
1314        );
1315    }
1316
1317    #[test]
1318    fn suffix_scan_does_not_reclassify_a_late_python_encoding_comment() {
1319        let source = b"value = 1\nother = 2\n# coding: latin-1\n".to_vec();
1320        let mut document =
1321            IncrementalDocument::new(source, Language::Python, ScanOptions::default(), 1);
1322        document
1323            .apply_changes(
1324                &[DocumentChange {
1325                    span: ByteSpan::new(18, 19),
1326                    replacement: b"3".to_vec(),
1327                }],
1328                2,
1329            )
1330            .unwrap();
1331        assert!(document.last_rescan_span().start > 0);
1332        assert_eq!(document.report().comments[0].kind, crate::CommentKind::Line);
1333        assert_eq!(
1334            document.report(),
1335            &crate::scan(document.source(), Language::Python, ScanOptions::default())
1336        );
1337    }
1338
1339    #[test]
1340    fn lexical_divergence_falls_back_to_the_document_end() {
1341        let source = b"let first = 1;\nlet second = 2;\nlet tail = 3;\n".to_vec();
1342        let mut document =
1343            IncrementalDocument::new(source, Language::Rust, ScanOptions::default(), 1);
1344        document
1345            .apply_changes(
1346                &[DocumentChange {
1347                    span: ByteSpan::new(28, 29),
1348                    replacement: b"r#\"open".to_vec(),
1349                }],
1350                2,
1351            )
1352            .unwrap();
1353        assert_eq!(document.last_rescan_span().end, document.source().len());
1354        assert_eq!(
1355            document.report(),
1356            &crate::scan(document.source(), Language::Rust, ScanOptions::default())
1357        );
1358    }
1359
1360    /// Regression: the rescan window used to be scanned as a *truncated* byte
1361    /// slice, so lexical decisions that peek past the window end (here Rust's
1362    /// six-byte character-literal lookahead) saw a different document than a
1363    /// full scan and the unterminated literal was silently lost.
1364    #[test]
1365    fn a_truncated_rescan_window_still_reports_an_unterminated_char_literal() {
1366        let source = vec![
1367            0, 0, 35, 0, 39, 128, 34, 39, 10, 39, 0, 35, 35, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1368        ];
1369        let mut document =
1370            IncrementalDocument::new(source, Language::Rust, ScanOptions::default(), 1);
1371        document
1372            .apply_changes(
1373                &[DocumentChange {
1374                    span: ByteSpan::new(5, 8),
1375                    replacement: vec![128],
1376                }],
1377                2,
1378            )
1379            .unwrap();
1380        let (expected, expected_checkpoints) =
1381            scan_with_checkpoints(document.source(), Language::Rust, ScanOptions::default(), 0);
1382        assert_eq!(document.report().comments, expected.comments);
1383        assert_eq!(document.report().diagnostics, expected.diagnostics);
1384        assert_eq!(document.report().valid, expected.valid);
1385        assert_eq!(document.report(), &expected);
1386        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1387    }
1388
1389    /// Regression: `rust_char_start` decided whether an apostrophe opened a
1390    /// character literal by reading up to six bytes forward, and the window was
1391    /// allowed to run past a line terminator — while `scan_c_family` still
1392    /// offers a checkpoint at the line start behind it. The decision for a
1393    /// token on line 1 therefore depended on bytes on line 2, which is exactly
1394    /// what a checkpoint promises cannot happen: an edit on line 2 left the
1395    /// reused prefix describing a literal a full scan no longer sees.
1396    #[test]
1397    fn a_rust_character_literal_never_decides_across_a_line_terminator() {
1398        /* NOTE: The bare window: `'` at the end of line 1 and the apostrophe
1399         * that would close it two bytes on, past the terminator. */
1400        let mut document = IncrementalDocument::new(
1401            b"let a = '\nx;\n".to_vec(),
1402            Language::Rust,
1403            ScanOptions::default(),
1404            1,
1405        );
1406        assert_eq!(document.safe_checkpoints(), [0, 10, 13]);
1407        document
1408            .apply_changes(
1409                &[DocumentChange {
1410                    span: ByteSpan::new(10, 11),
1411                    replacement: b"'".to_vec(),
1412                }],
1413                2,
1414            )
1415            .unwrap();
1416        let (expected, expected_checkpoints) =
1417            scan_with_checkpoints(document.source(), Language::Rust, ScanOptions::default(), 0);
1418        assert_eq!(document.report().diagnostics, expected.diagnostics);
1419        assert_eq!(document.report().valid, expected.valid);
1420        assert_eq!(document.report(), &expected);
1421        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1422
1423        /* NOTE: The escaped window, which reaches one byte further: `'\` at the
1424         * end of line 1 and the closing apostrophe at the head of line 2. A
1425         * full scan used to read the terminator as the escaped character and
1426         * swallow it, dropping the checkpoint the line start had. */
1427        let mut escaped = IncrementalDocument::new(
1428            b"let a = '\\\nx;\n".to_vec(),
1429            Language::Rust,
1430            ScanOptions::default(),
1431            1,
1432        );
1433        assert_eq!(escaped.safe_checkpoints(), [0, 11, 14]);
1434        escaped
1435            .apply_changes(
1436                &[DocumentChange {
1437                    span: ByteSpan::new(11, 12),
1438                    replacement: b"'".to_vec(),
1439                }],
1440                2,
1441            )
1442            .unwrap();
1443        let (expected, expected_checkpoints) =
1444            scan_with_checkpoints(escaped.source(), Language::Rust, ScanOptions::default(), 0);
1445        assert_eq!(escaped.report(), &expected);
1446        assert_eq!(escaped.safe_checkpoints(), expected_checkpoints);
1447        assert_eq!(escaped.safe_checkpoints(), [0, 11, 14]);
1448    }
1449
1450    /// The OCaml half of the same rule. `ocaml_char_start` reads two bytes
1451    /// forward for a bare character and eight for an escaped one, and both
1452    /// windows used to run past a line terminator that `scan_ocaml` offers a
1453    /// checkpoint behind.
1454    #[test]
1455    fn an_ocaml_character_literal_never_decides_across_a_line_terminator() {
1456        let mut document = IncrementalDocument::new(
1457            b"let c = '\nz\n".to_vec(),
1458            Language::Ocaml,
1459            ScanOptions::default(),
1460            1,
1461        );
1462        assert_eq!(document.safe_checkpoints(), [0, 10, 12]);
1463        document
1464            .apply_changes(
1465                &[DocumentChange {
1466                    span: ByteSpan::new(10, 11),
1467                    replacement: b"'".to_vec(),
1468                }],
1469                2,
1470            )
1471            .unwrap();
1472        let (expected, expected_checkpoints) = scan_with_checkpoints(
1473            document.source(),
1474            Language::Ocaml,
1475            ScanOptions::default(),
1476            0,
1477        );
1478        assert_eq!(document.report().diagnostics, expected.diagnostics);
1479        assert_eq!(document.report().valid, expected.valid);
1480        assert_eq!(document.report(), &expected);
1481        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1482
1483        let mut escaped = IncrementalDocument::new(
1484            b"let c = '\\\nz;\n".to_vec(),
1485            Language::Ocaml,
1486            ScanOptions::default(),
1487            1,
1488        );
1489        assert_eq!(escaped.safe_checkpoints(), [0, 11, 14]);
1490        escaped
1491            .apply_changes(
1492                &[DocumentChange {
1493                    span: ByteSpan::new(12, 13),
1494                    replacement: b"'".to_vec(),
1495                }],
1496                2,
1497            )
1498            .unwrap();
1499        let (expected, expected_checkpoints) =
1500            scan_with_checkpoints(escaped.source(), Language::Ocaml, ScanOptions::default(), 0);
1501        assert_eq!(escaped.report(), &expected);
1502        assert_eq!(escaped.safe_checkpoints(), expected_checkpoints);
1503        assert_eq!(escaped.safe_checkpoints(), [0, 11, 14]);
1504    }
1505
1506    /// A here-document delimiter is a word, and a quoted word may span lines:
1507    /// `<<"EO`, a line break, `F"` names the delimiter `EO\nF`. The parse that
1508    /// reads it is therefore a lookahead with no line bound, and the path that
1509    /// gives up on an unterminated quote rewinds the scan to the byte after the
1510    /// operator and lexes the same bytes again from a state it already decided
1511    /// out of them. That the re-lex reaches the same end today is two lexers
1512    /// agreeing, not a promise, so the watermark withdraws every checkpoint
1513    /// the parse read through and the two edits below — one that opens the
1514    /// quote, one that closes it again — stay equal to a full scan.
1515    #[test]
1516    fn a_quoted_shell_heredoc_delimiter_withdraws_the_checkpoints_it_read_past() {
1517        let closed = b"cat <<\"EO\nF\"\nx\nEO\nF\n# c\n".to_vec();
1518        let mut document =
1519            IncrementalDocument::new(closed.clone(), Language::Shell, ScanOptions::default(), 1);
1520        assert_eq!(document.safe_checkpoints(), [0]);
1521        /* NOTE: deleting the closing quote leaves the delimiter word open, so
1522         * the parse reads to the end of the document and gives up there. */
1523        document
1524            .apply_changes(
1525                &[DocumentChange {
1526                    span: ByteSpan::new(11, 12),
1527                    replacement: Vec::new(),
1528                }],
1529                2,
1530            )
1531            .unwrap();
1532        let (expected, expected_checkpoints) = scan_with_checkpoints(
1533            document.source(),
1534            Language::Shell,
1535            ScanOptions::default(),
1536            0,
1537        );
1538        assert_eq!(document.report(), &expected);
1539        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1540
1541        let mut reopened = IncrementalDocument::new(
1542            document.source().to_vec(),
1543            Language::Shell,
1544            ScanOptions::default(),
1545            1,
1546        );
1547        reopened
1548            .apply_changes(
1549                &[DocumentChange {
1550                    span: ByteSpan::new(11, 11),
1551                    replacement: b"\"".to_vec(),
1552                }],
1553                2,
1554            )
1555            .unwrap();
1556        let (expected, expected_checkpoints) = scan_with_checkpoints(
1557            reopened.source(),
1558            Language::Shell,
1559            ScanOptions::default(),
1560            0,
1561        );
1562        assert_eq!(reopened.source(), &closed[..]);
1563        assert_eq!(reopened.report(), &expected);
1564        assert_eq!(reopened.safe_checkpoints(), expected_checkpoints);
1565        /* NOTE: The document above is invalid — a delimiter word holding a line
1566         * terminator matches no line of the body, so the here-document runs off
1567         * the end — and an invalid report is never reused, which leaves the
1568         * watermark unexercised. This one is valid, and its checkpoints are
1569         * withdrawn by nothing but the reach. `#` is an ordinary word character
1570         * to the delimiter parse, so `<<#"` reads a word that opens a quote;
1571         * the quote finds no partner and the parse gives up at the end of the
1572         * document, having read every byte of it. The scan rewinds to the byte
1573         * after the operator, where `#` is a comment opener instead, and lexes
1574         * a comment, a line, and a comment — line starts a checkpoint would
1575         * otherwise be offered at. */
1576        let mut giving_up = IncrementalDocument::new(
1577            b"cat <<#\"\nx\n# c\n".to_vec(),
1578            Language::Shell,
1579            ScanOptions::default(),
1580            1,
1581        );
1582        assert!(giving_up.report().valid);
1583        assert_eq!(giving_up.report().comments.len(), 2);
1584        assert_eq!(giving_up.safe_checkpoints(), [0]);
1585        /* NOTE: Closing the quote on line 3 gives the delimiter word the whole
1586         * file, and the here-document it opens is then the unterminated one: the
1587         * full scan finds no comment at all. A rescan restarted from the line
1588         * start at 11 — which is what stands there without the reach — would
1589         * keep both of the old comments and call the file valid. */
1590        giving_up
1591            .apply_changes(
1592                &[DocumentChange {
1593                    span: ByteSpan::new(14, 14),
1594                    replacement: b"\"".to_vec(),
1595                }],
1596                2,
1597            )
1598            .unwrap();
1599        let (expected, expected_checkpoints) = scan_with_checkpoints(
1600            giving_up.source(),
1601            Language::Shell,
1602            ScanOptions::default(),
1603            0,
1604        );
1605        assert!(!expected.valid);
1606        assert!(expected.comments.is_empty());
1607        assert_eq!(giving_up.report(), &expected);
1608        assert_eq!(giving_up.safe_checkpoints(), expected_checkpoints);
1609    }
1610
1611    /// OCaml's quoted strings are where that property first caught a
1612    /// checkpoint standing inside what a decision had read — and the answer is
1613    /// not to withdraw the rest of the document but to stop reading it. A
1614    /// quoted-string tag is `[a-z_]*` with the `|` directly behind it, so the
1615    /// search is bounded by that class: an ordinary `{` gives up at the first
1616    /// byte outside it, and every line under it keeps the restart point it
1617    /// earned. What the bound has to be worth is soundness, so both edits are
1618    /// checked against a full scan — the one on a later line, which restarts
1619    /// from a kept checkpoint, and the one that turns the `{` into a real
1620    /// quoted string, which lies before every checkpoint but 0.
1621    #[test]
1622    fn an_ocaml_quoted_string_tag_search_is_bounded_by_its_tag_class() {
1623        let stray = b"let x = {aa\n(* c *)\ny\n".to_vec();
1624        let mut document =
1625            IncrementalDocument::new(stray.clone(), Language::Ocaml, ScanOptions::default(), 1);
1626        assert!(document.report().valid);
1627        assert_eq!(document.report().comments.len(), 1);
1628        assert_eq!(document.safe_checkpoints(), [0, 12, 20, 22]);
1629
1630        /* NOTE: An edit on the last line restarts from one of those kept
1631         * checkpoints rather than from 0, which is the whole point of keeping
1632         * them, and it still answers what a full scan answers. */
1633        document
1634            .apply_changes(
1635                &[DocumentChange {
1636                    span: ByteSpan::new(21, 21),
1637                    replacement: b" (* d *)".to_vec(),
1638                }],
1639                2,
1640            )
1641            .unwrap();
1642        assert!(document.last_rescan_span().start >= 12);
1643        let (expected, expected_checkpoints) = scan_with_checkpoints(
1644            document.source(),
1645            Language::Ocaml,
1646            ScanOptions::default(),
1647            0,
1648        );
1649        assert_eq!(document.report().comments.len(), 2);
1650        assert_eq!(document.report(), &expected);
1651        assert_eq!(document.safe_checkpoints(), expected_checkpoints);
1652
1653        /* NOTE: The `|` is what makes the tag a tag, and it stands before every
1654         * checkpoint the file has but 0: the `{aa|` opens a quoted string that
1655         * no `|aa}` closes, so the file is one unterminated literal and the
1656         * comment on line 2 is inside it. */
1657        let mut opened =
1658            IncrementalDocument::new(stray, Language::Ocaml, ScanOptions::default(), 1);
1659        opened
1660            .apply_changes(
1661                &[DocumentChange {
1662                    span: ByteSpan::new(11, 11),
1663                    replacement: b"|".to_vec(),
1664                }],
1665                2,
1666            )
1667            .unwrap();
1668        let (expected, expected_checkpoints) =
1669            scan_with_checkpoints(opened.source(), Language::Ocaml, ScanOptions::default(), 0);
1670        assert!(!expected.valid);
1671        assert!(expected.comments.is_empty());
1672        assert_eq!(opened.report(), &expected);
1673        assert_eq!(opened.safe_checkpoints(), expected_checkpoints);
1674    }
1675
1676    #[test]
1677    fn invalid_change_batches_leave_the_document_untouched() {
1678        let source = b"abcdef".to_vec();
1679        let mut document =
1680            IncrementalDocument::new(source.clone(), Language::Rust, ScanOptions::default(), 1);
1681        assert_eq!(
1682            document.apply_changes(
1683                &[
1684                    DocumentChange {
1685                        span: ByteSpan::new(1, 3),
1686                        replacement: b"x".to_vec(),
1687                    },
1688                    DocumentChange {
1689                        span: ByteSpan::new(2, 4),
1690                        replacement: b"y".to_vec(),
1691                    },
1692                ],
1693                2,
1694            ),
1695            Err(IncrementalError::InvalidSpan)
1696        );
1697        assert_eq!(document.source(), source);
1698        assert_eq!(document.version(), 1);
1699    }
1700
1701    #[test]
1702    fn utf16_positions_handle_astral_characters() {
1703        let document = IncrementalDocument::new(
1704            "😀x".as_bytes().to_vec(),
1705            Language::Rust,
1706            ScanOptions::default(),
1707            1,
1708        );
1709        assert_eq!(
1710            document.byte_offset(0, 2, PositionEncoding::Utf16).unwrap(),
1711            4
1712        );
1713        assert!(document.byte_offset(0, 1, PositionEncoding::Utf16).is_err());
1714    }
1715
1716    #[test]
1717    fn positions_exclude_crlf_and_lone_cr_line_endings() {
1718        let document = IncrementalDocument::new(
1719            b"ab\r\ncd\ref".to_vec(),
1720            Language::Rust,
1721            ScanOptions::default(),
1722            1,
1723        );
1724        assert_eq!(document.byte_offset(0, 2, PositionEncoding::Utf8), Ok(2));
1725        assert!(document.byte_offset(0, 3, PositionEncoding::Utf8).is_err());
1726        assert_eq!(document.byte_offset(1, 2, PositionEncoding::Utf16), Ok(6));
1727        assert_eq!(document.byte_offset(2, 2, PositionEncoding::Utf32), Ok(9));
1728    }
1729}