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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub enum PositionEncoding {
19 Utf8,
21 #[default]
23 Utf16,
24 Utf32,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct DocumentChange {
34 pub span: ByteSpan,
36 pub replacement: Vec<u8>,
38}
39
40#[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#[derive(Clone, Debug, Error, Eq, PartialEq)]
112pub enum IncrementalError {
113 #[error("stale document version {received}; current version is {current}")]
116 StaleVersion {
117 received: i64,
119 current: i64,
121 },
122 #[error("change span lies outside the document")]
125 InvalidSpan,
126 #[error("position does not lie on a valid encoding boundary")]
129 InvalidPosition,
130}
131
132impl IncrementalDocument {
133 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 pub fn source(&self) -> &[u8] {
158 &self.source
159 }
160 pub fn report(&self) -> &ScanReport {
162 &self.report
163 }
164 pub const fn language(&self) -> Language {
166 self.language
167 }
168 pub fn scan_options(&self) -> &ScanOptions {
170 &self.options
171 }
172 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 pub const fn version(&self) -> i64 {
192 self.version
193 }
194 pub const fn last_rescan_span(&self) -> ByteSpan {
200 self.last_rescan
201 }
202 pub fn checkpoints(&self) -> &[usize] {
207 &self.checkpoints
208 }
209 pub fn safe_checkpoints(&self) -> &[usize] {
215 &self.safe_checkpoints
216 }
217
218 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 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 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 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 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 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 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 fn weight(length: usize) -> u32 {
547 u32::try_from(length).expect("the pool is far smaller than a weight")
548 }
549
550 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 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 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 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 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 #[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 #![proptest_config(ProptestConfig { failure_persistence: None, ..ProptestConfig::default() })]
657
658 #[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 #[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 #[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 #[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 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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 #[test]
1397 fn a_rust_character_literal_never_decides_across_a_line_terminator() {
1398 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 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 #[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 #[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 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 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 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 #[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 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 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}