1use std::{ops::Range, sync::Arc};
2
3use gpui::SharedString;
4
5use super::conflict::DiffConflict;
6use super::parser::{DiffParseError, DiffParseOptions};
7use smallvec::{SmallVec, smallvec};
8use unicode_segmentation::UnicodeSegmentation as _;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum DiffSide {
13 Original,
14 Modified,
15}
16
17impl DiffSide {
18 pub(crate) fn other(self) -> Self {
19 match self {
20 Self::Original => Self::Modified,
21 Self::Modified => Self::Original,
22 }
23 }
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
31pub struct DiffLinePosition {
32 path: SharedString,
33 side: DiffSide,
34 line: usize,
35}
36
37impl DiffLinePosition {
38 pub fn new(path: impl Into<SharedString>, side: DiffSide, line: usize) -> Self {
40 Self {
41 path: path.into(),
42 side,
43 line: line.max(1),
44 }
45 }
46 pub fn path(&self) -> &SharedString {
48 &self.path
49 }
50 pub fn side(&self) -> DiffSide {
51 self.side
52 }
53 pub fn line(&self) -> usize {
54 self.line
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct DiffLineRange {
68 path: SharedString,
69 side: DiffSide,
70 start: usize,
71 end_side: DiffSide,
72 end: usize,
73}
74
75impl DiffLineRange {
76 pub fn new(path: impl Into<SharedString>, side: DiffSide, start: usize, end: usize) -> Self {
78 Self {
79 path: path.into(),
80 side,
81 start: start.max(1),
82 end_side: side,
83 end: end.max(1),
84 }
85 }
86 pub fn with_end_side(mut self, side: DiffSide) -> Self {
88 self.end_side = side;
89 self
90 }
91 pub fn path(&self) -> &SharedString {
93 &self.path
94 }
95 pub fn side(&self) -> DiffSide {
97 self.side
98 }
99 pub fn start(&self) -> usize {
100 self.start
101 }
102 pub fn end_side(&self) -> DiffSide {
104 self.end_side
105 }
106 pub fn end(&self) -> usize {
107 self.end
108 }
109 pub(crate) fn start_position(&self) -> DiffLinePosition {
110 DiffLinePosition::new(self.path.clone(), self.side, self.start)
111 }
112 pub(crate) fn end_position(&self) -> DiffLinePosition {
113 DiffLinePosition::new(self.path.clone(), self.end_side, self.end)
114 }
115 pub(crate) fn is_single_side(&self) -> bool {
117 self.side == self.end_side
118 }
119}
120
121#[derive(Debug)]
123pub(crate) struct SourceLine {
124 line_number: usize,
125 text: SharedString,
126 display: SharedString,
127 chunks: SmallVec<[DisplayChunk; 1]>,
128 counterpart: Option<usize>,
129 display_tabs: Vec<DisplayTab>,
132 source: Range<usize>,
133 content_end: usize,
134}
135
136#[derive(Debug)]
137struct DisplayChunk {
138 range: Range<usize>,
139 text: SharedString,
140}
141
142#[derive(Debug)]
143struct DisplayTab {
144 source_offset: usize,
145 display_offset: usize,
146 width: usize,
147}
148
149impl SourceLine {
150 pub(crate) fn line_number(&self) -> usize {
152 self.line_number
153 }
154 pub(crate) fn text(&self) -> &SharedString {
156 &self.text
157 }
158 pub(crate) fn display(&self) -> &SharedString {
160 &self.display
161 }
162 pub(crate) fn chunk_ranges(&self) -> impl Iterator<Item = Range<usize>> + '_ {
164 self.chunks.iter().map(|chunk| chunk.range.clone())
165 }
166 pub(crate) fn counterpart(&self) -> Option<usize> {
168 self.counterpart
169 }
170 pub(crate) fn source(&self) -> Range<usize> {
172 self.source.clone()
173 }
174 pub(crate) fn content_end(&self) -> usize {
176 self.content_end
177 }
178
179 pub(crate) fn chunk_text(&self, range: &Range<usize>) -> SharedString {
182 if range.start == 0 && range.end == self.display.len() {
183 return self.display.clone();
184 }
185 let ix = self
186 .chunks
187 .partition_point(|chunk| chunk.range.start < range.start);
188 if let Some(chunk) = self.chunks.get(ix)
189 && chunk.range == *range
190 {
191 return chunk.text.clone();
192 }
193 self.display
194 .get(range.clone())
195 .map_or_else(SharedString::default, SharedString::from)
196 }
197
198 pub(crate) fn source_offset(&self, display_offset: usize) -> usize {
199 let display_offset = display_offset.min(self.display.len());
200 let preceding = self
201 .display_tabs
202 .partition_point(|tab| tab.display_offset <= display_offset);
203 let Some(tab) = preceding.checked_sub(1).map(|ix| &self.display_tabs[ix]) else {
204 return display_offset;
205 };
206 if display_offset < tab.display_offset + tab.width {
207 tab.source_offset
208 } else {
209 display_offset - (tab.display_offset + tab.width - tab.source_offset - 1)
210 }
211 }
212 pub(crate) fn display_range(&self, source: Range<usize>) -> Range<usize> {
213 self.display_offset(source.start)..self.display_offset(source.end)
214 }
215
216 fn display_offset(&self, source_offset: usize) -> usize {
217 let source_offset = source_offset.min(self.text.len());
218 let preceding = self
219 .display_tabs
220 .partition_point(|tab| tab.source_offset < source_offset);
221 let expansion = preceding.checked_sub(1).map_or(0, |ix| {
222 let tab = &self.display_tabs[ix];
223 tab.display_offset + tab.width - tab.source_offset - 1
224 });
225 source_offset + expansion
226 }
227}
228
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub(crate) struct LinePair {
232 original: Option<usize>,
233 modified: Option<usize>,
234 changed: bool,
235}
236
237impl LinePair {
238 pub(crate) fn new(original: Option<usize>, modified: Option<usize>, changed: bool) -> Self {
239 Self {
240 original,
241 modified,
242 changed,
243 }
244 }
245 pub(crate) fn original(&self) -> Option<usize> {
246 self.original
247 }
248 pub(crate) fn modified(&self) -> Option<usize> {
249 self.modified
250 }
251 pub(crate) fn is_changed(&self) -> bool {
252 self.changed
253 }
254}
255
256#[derive(Clone, Debug)]
258pub(crate) struct Hunk {
259 pairs: Range<usize>,
260 original: Range<usize>,
261 modified: Range<usize>,
262 label: SharedString,
263 original_lines: Range<usize>,
265 truncated: bool,
267}
268
269impl Hunk {
270 pub(crate) fn new(
271 pairs: Range<usize>,
272 original: Range<usize>,
273 modified: Range<usize>,
274 label: SharedString,
275 original_lines: Range<usize>,
276 truncated: bool,
277 ) -> Self {
278 Self {
279 pairs,
280 original,
281 modified,
282 label,
283 original_lines,
284 truncated,
285 }
286 }
287 pub(crate) fn hidden_lines_before(&self, previous: Option<&Hunk>) -> usize {
290 let after = previous.map_or(1, |previous| previous.original_lines.end);
291 self.original_lines.start.saturating_sub(after)
292 }
293 pub(crate) fn pairs(&self) -> Range<usize> {
294 self.pairs.clone()
295 }
296 pub(crate) fn lines(&self, side: DiffSide) -> Range<usize> {
297 match side {
298 DiffSide::Original => self.original.clone(),
299 DiffSide::Modified => self.modified.clone(),
300 }
301 }
302 pub(crate) fn label(&self) -> &SharedString {
304 &self.label
305 }
306 pub(crate) fn is_truncated(&self) -> bool {
307 self.truncated
308 }
309}
310
311#[derive(Default)]
314pub(crate) struct FileSide {
315 path: Option<SharedString>,
316 source: SharedString,
317 lines: Vec<SourceLine>,
318}
319
320impl FileSide {
321 pub(crate) fn new(
324 path: Option<SharedString>,
325 source: String,
326 lines: impl IntoIterator<Item = (usize, Range<usize>)>,
327 ) -> Self {
328 let lines = lines
329 .into_iter()
330 .map(|(line_number, range)| source_line(&source, range, line_number))
331 .collect();
332 Self {
333 path,
334 source: source.into(),
335 lines,
336 }
337 }
338}
339
340#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
342#[non_exhaustive]
343pub enum DiffFileStatus {
344 Added,
345 Deleted,
346 Modified,
347 Renamed,
348 Copied,
349 Unchanged,
351 Conflicted,
353}
354
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
358pub(crate) struct PatchLine {
359 original: Option<usize>,
360 modified: Option<usize>,
361}
362
363impl PatchLine {
364 pub(crate) fn line(&self, side: DiffSide) -> Option<usize> {
365 match side {
366 DiffSide::Original => self.original,
367 DiffSide::Modified => self.modified,
368 }
369 }
370}
371
372struct DocumentInner {
373 path: SharedString,
374 status: DiffFileStatus,
375 original: FileSide,
376 modified: FileSide,
377 pairs: Vec<LinePair>,
378 hunks: Vec<Hunk>,
379 additions: usize,
380 deletions: usize,
381 extended_headers: Vec<SharedString>,
382 binary: bool,
383 line_number_digits: usize,
385 patch_lines: Vec<PatchLine>,
387 patch_ixs: [Vec<usize>; 2],
388 conflicts: Vec<DiffConflict>,
389}
390
391#[derive(Clone)]
396pub struct DiffFile {
397 inner: Arc<DocumentInner>,
398 language: Option<SharedString>,
399}
400
401impl DiffFile {
402 pub fn parse(patch: &str) -> Result<Vec<Self>, DiffParseError> {
406 super::parser::parse(patch, DiffParseOptions::default())
407 }
408
409 pub fn parse_with(patch: &str, options: DiffParseOptions) -> Result<Vec<Self>, DiffParseError> {
413 super::parser::parse(patch, options)
414 }
415
416 pub(crate) fn new(
419 status: DiffFileStatus,
420 mut original: FileSide,
421 mut modified: FileSide,
422 pairs: Vec<LinePair>,
423 hunks: Vec<Hunk>,
424 extended_headers: Vec<SharedString>,
425 binary: bool,
426 ) -> Self {
427 let (mut additions, mut deletions) = (0, 0);
428 for pair in &pairs {
429 if pair.changed && status != DiffFileStatus::Conflicted {
431 additions += usize::from(pair.modified.is_some());
432 deletions += usize::from(pair.original.is_some());
433 }
434 if let (Some(old), Some(new)) = (pair.original, pair.modified) {
435 original.lines[old].counterpart = Some(new);
436 modified.lines[new].counterpart = Some(old);
437 }
438 }
439 let widest = [&original, &modified]
440 .iter()
441 .filter_map(|side| side.lines.last())
442 .map(|line| line.line_number)
443 .max()
444 .unwrap_or(1);
445 let path = modified
446 .path
447 .clone()
448 .or_else(|| original.path.clone())
449 .unwrap_or_default();
450 let (patch_lines, patch_ixs) = patch_order(&pairs, &hunks, &original, &modified);
451 Self {
452 inner: Arc::new(DocumentInner {
453 path,
454 status,
455 original,
456 modified,
457 pairs,
458 hunks,
459 additions,
460 deletions,
461 extended_headers,
462 binary,
463 line_number_digits: widest.max(1).ilog10() as usize + 1,
464 patch_lines,
465 patch_ixs,
466 conflicts: Vec::new(),
467 }),
468 language: None,
469 }
470 }
471
472 pub fn with_language(mut self, language: impl Into<SharedString>) -> Self {
475 self.language = Some(language.into());
476 self
477 }
478 pub fn language(&self) -> SharedString {
480 self.language
481 .clone()
482 .unwrap_or_else(|| super::presentation::detected_language(self.path()))
483 }
484 pub(crate) fn language_override(&self) -> Option<&SharedString> {
485 self.language.as_ref()
486 }
487 pub fn status(&self) -> DiffFileStatus {
489 self.inner.status
490 }
491
492 pub fn path(&self) -> &SharedString {
495 &self.inner.path
496 }
497 pub fn original_path(&self) -> Option<&SharedString> {
499 self.inner.original.path.as_ref()
500 }
501 pub fn modified_path(&self) -> Option<&SharedString> {
503 self.inner.modified.path.as_ref()
504 }
505 pub fn extended_headers(&self) -> &[SharedString] {
508 &self.inner.extended_headers
509 }
510 pub fn is_binary(&self) -> bool {
512 self.inner.binary
513 }
514 pub fn is_truncated(&self) -> bool {
517 self.inner.hunks.iter().any(Hunk::is_truncated)
518 }
519 pub fn additions(&self) -> usize {
520 self.inner.additions
521 }
522 pub fn deletions(&self) -> usize {
523 self.inner.deletions
524 }
525 pub fn has_changes(&self) -> bool {
528 self.additions() > 0
529 || self.deletions() > 0
530 || !self.inner.extended_headers.is_empty()
531 || self.inner.binary
532 || self.original_path().is_none()
533 || self.modified_path().is_none()
534 || self.status() == DiffFileStatus::Conflicted
535 }
536
537 pub(crate) fn is_single_column(&self) -> bool {
539 matches!(
540 self.status(),
541 DiffFileStatus::Unchanged | DiffFileStatus::Conflicted
542 )
543 }
544 pub fn conflicts(&self) -> &[DiffConflict] {
546 &self.inner.conflicts
547 }
548 pub(crate) fn with_conflicts(mut self, conflicts: Vec<DiffConflict>) -> Self {
549 Arc::get_mut(&mut self.inner)
550 .expect("a newly built file is not shared")
551 .conflicts = conflicts;
552 self
553 }
554
555 pub(crate) fn lines_count(&self, side: DiffSide) -> usize {
556 self.lines(side).len()
557 }
558
559 pub(crate) fn text_for_lines(&self, side: DiffSide, start: usize, end: usize) -> String {
562 let lines = self.lines(side);
563 let start_ix = lines.partition_point(|line| line.line_number < start);
564 let end_ix = lines.partition_point(|line| line.line_number <= end);
565 let selected = &lines[start_ix..end_ix];
566 let (Some(first), Some(last)) = (selected.first(), selected.last()) else {
567 return String::new();
568 };
569 self.source(side)[first.source.start..last.source.end].to_owned()
570 }
571
572 pub(crate) fn line_number(&self, side: DiffSide, index: usize) -> usize {
574 self.lines(side)[index].line_number
575 }
576 pub(crate) fn line_index(&self, side: DiffSide, line: usize) -> Option<usize> {
577 self.lines(side)
578 .binary_search_by_key(&line, |source| source.line_number)
579 .ok()
580 }
581
582 pub(crate) fn pairs(&self) -> &[LinePair] {
583 &self.inner.pairs
584 }
585 pub(crate) fn hunks(&self) -> &[Hunk] {
586 &self.inner.hunks
587 }
588 pub(crate) fn line_number_digits(&self) -> usize {
590 self.inner.line_number_digits
591 }
592
593 pub(crate) fn patch_lines(&self) -> &[PatchLine] {
594 &self.inner.patch_lines
595 }
596 pub(crate) fn patch_ix(&self, side: DiffSide, ix: usize) -> usize {
598 self.inner.patch_ixs[side as usize][ix]
599 }
600
601 pub(crate) fn side_path(&self, side: DiffSide) -> Option<&SharedString> {
602 self.side(side).path.as_ref()
603 }
604
605 fn side(&self, side: DiffSide) -> &FileSide {
606 match side {
607 DiffSide::Original => &self.inner.original,
608 DiffSide::Modified => &self.inner.modified,
609 }
610 }
611 pub(crate) fn lines(&self, side: DiffSide) -> &[SourceLine] {
612 &self.side(side).lines
613 }
614 pub(crate) fn source(&self, side: DiffSide) -> &str {
615 &self.side(side).source
616 }
617}
618
619fn patch_order(
622 pairs: &[LinePair],
623 hunks: &[Hunk],
624 original: &FileSide,
625 modified: &FileSide,
626) -> (Vec<PatchLine>, [Vec<usize>; 2]) {
627 let mut lines = Vec::with_capacity(pairs.len());
628 let mut ixs = [vec![0; original.lines.len()], vec![0; modified.lines.len()]];
629 let ranges = if hunks.is_empty() {
630 vec![0..pairs.len()]
631 } else {
632 hunks.iter().map(Hunk::pairs).collect()
633 };
634 for range in ranges {
635 let mut ix = range.start;
636 while ix < range.end {
637 if !pairs[ix].changed {
638 let pair = pairs[ix];
639 lines.push(PatchLine {
640 original: pair.original,
641 modified: pair.modified,
642 });
643 ix += 1;
644 continue;
645 }
646 let start = ix;
647 while ix < range.end && pairs[ix].changed {
648 ix += 1;
649 }
650 for pair in &pairs[start..ix] {
651 if let Some(line) = pair.original {
652 lines.push(PatchLine {
653 original: Some(line),
654 modified: None,
655 });
656 }
657 }
658 for pair in &pairs[start..ix] {
659 if let Some(line) = pair.modified {
660 lines.push(PatchLine {
661 original: None,
662 modified: Some(line),
663 });
664 }
665 }
666 }
667 }
668 for (patch_ix, line) in lines.iter().enumerate() {
669 if let Some(ix) = line.original {
670 ixs[0][ix] = patch_ix;
671 }
672 if let Some(ix) = line.modified {
673 ixs[1][ix] = patch_ix;
674 }
675 }
676 (lines, ixs)
677}
678
679fn source_line(source: &str, range: Range<usize>, line_number: usize) -> SourceLine {
681 let raw = &source[range.clone()];
682 let content = match raw.strip_suffix('\n') {
683 Some(content) => content.strip_suffix('\r').unwrap_or(content),
684 None => raw,
685 };
686 let text = SharedString::from(content.to_owned());
687 let (display, display_tabs): (SharedString, Vec<DisplayTab>) = if content.contains('\t') {
688 let mut display = String::with_capacity(content.len());
689 let mut display_tabs = Vec::new();
690 let mut column = 0;
691 for (ix, ch) in content.char_indices() {
692 if ch == '\t' {
693 let spaces = 4 - column % 4;
694 display_tabs.push(DisplayTab {
695 source_offset: ix,
696 display_offset: display.len(),
697 width: spaces,
698 });
699 for _ in 0..spaces {
700 display.push(' ');
701 }
702 column += spaces;
703 } else {
704 display.push(ch);
705 column += 1;
706 }
707 }
708 (display.into(), display_tabs)
709 } else {
710 (text.clone(), Vec::new())
711 };
712 let chunks = display_chunks(&display)
713 .into_iter()
714 .map(|range| {
715 let text = if range.start == 0 && range.end == display.len() {
716 display.clone()
717 } else {
718 SharedString::from(display[range.clone()].to_owned())
719 };
720 DisplayChunk { range, text }
721 })
722 .collect();
723 SourceLine {
724 line_number,
725 text,
726 display,
727 chunks,
728 counterpart: None,
729 display_tabs,
730 content_end: range.start + content.len(),
731 source: range,
732 }
733}
734
735#[cfg(test)]
736pub(crate) fn source_lines(text: &str) -> Vec<SourceLine> {
737 let mut offset = 0;
738 text.split_inclusive('\n')
739 .enumerate()
740 .map(|(index, line)| {
741 let range = offset..offset + line.len();
742 offset = range.end;
743 source_line(text, range, index + 1)
744 })
745 .collect()
746}
747
748pub(crate) fn display_chunks(text: &str) -> SmallVec<[Range<usize>; 1]> {
751 const MAX_BYTES: usize = 512;
752 if text.len() <= MAX_BYTES {
753 return smallvec![0..text.len()];
754 }
755 let mut chunks = SmallVec::new();
756 let mut start = 0;
757 let mut end = 0;
758 for (ix, grapheme) in text.grapheme_indices(true) {
759 if ix + grapheme.len() - start > MAX_BYTES && start < ix {
760 chunks.push(start..ix);
761 start = ix;
762 }
763 if grapheme.len() > MAX_BYTES {
764 for (relative, character) in grapheme.char_indices() {
765 let offset = ix + relative;
766 if offset + character.len_utf8() - start > MAX_BYTES {
767 chunks.push(start..offset);
768 start = offset;
769 }
770 }
771 }
772 end = ix + grapheme.len();
773 }
774 if start < end {
775 chunks.push(start..end);
776 }
777 chunks
778}
779
780#[cfg(test)]
783pub(crate) mod fixture {
784 use similar::{ChangeTag, TextDiff};
785
786 use super::DiffFile;
787
788 fn quoted(name: &str) -> String {
789 format!(
790 "\"{}\"",
791 name.replace('\\', "\\\\")
792 .replace('\"', "\\\"")
793 .replace('\n', "\\n")
794 .replace('\r', "\\r")
795 .replace('\t', "\\t")
796 )
797 }
798
799 pub(crate) fn document(
802 original: Option<(&str, &str)>,
803 modified: Option<(&str, &str)>,
804 ) -> DiffFile {
805 let old = original.map_or("", |(_, text)| text);
806 let new = modified.map_or("", |(_, text)| text);
807 let old_lines: Vec<&str> = old.split_inclusive('\n').collect();
808 let new_lines: Vec<&str> = new.split_inclusive('\n').collect();
809 let old_name = original.map_or_else(|| "/dev/null".to_owned(), |(name, _)| quoted(name));
810 let new_name = modified.map_or_else(|| "/dev/null".to_owned(), |(name, _)| quoted(name));
811 let diff = TextDiff::from_slices(&old_lines, &new_lines);
814 let mut patch = format!("--- {old_name}\n+++ {new_name}\n");
815 if !old_lines.is_empty() || !new_lines.is_empty() {
816 patch.push_str(&format!(
817 "@@ -{},{} +{},{} @@\n",
818 usize::from(!old_lines.is_empty()),
819 old_lines.len(),
820 usize::from(!new_lines.is_empty()),
821 new_lines.len()
822 ));
823 for op in diff.ops() {
824 for change in diff.iter_changes(op) {
825 patch.push(match change.tag() {
826 ChangeTag::Equal => ' ',
827 ChangeTag::Delete => '-',
828 ChangeTag::Insert => '+',
829 });
830 patch.push_str(change.value());
831 if !change.value().ends_with('\n') {
832 patch.push_str("\n\\ No newline at end of file\n");
833 }
834 }
835 }
836 }
837 if original.is_none() {
838 patch.insert_str(0, "diff --git a/fixture b/fixture\nnew file mode 100644\n");
839 } else if modified.is_none() {
840 patch.insert_str(
841 0,
842 "diff --git a/fixture b/fixture\ndeleted file mode 100644\n",
843 );
844 }
845 let mut document = DiffFile::parse(&patch)
846 .unwrap_or_else(|error| panic!("invalid fixture patch: {error}\n{patch}"))
847 .remove(0);
848 std::sync::Arc::get_mut(&mut document.inner)
849 .unwrap()
850 .hunks
851 .clear();
852 document
853 }
854
855 pub(crate) fn modified(name: &str, original: &str, modified: &str) -> DiffFile {
856 document(Some((name, original)), Some((name, modified)))
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::{fixture, *};
863
864 #[test]
865 fn comparison_preserves_source_and_alignment() {
866 fn assert_send_sync<T: Send + Sync>() {}
867 assert_send_sync::<DiffFile>();
868 let doc = fixture::modified(
869 "a.rs",
870 "same\r\n旧值\r\nremoved\r\ntail",
871 "same\r\n新值\r\ntail\r\n",
872 );
873 assert_eq!((doc.additions(), doc.deletions()), (2, 3));
874 assert_eq!(
875 doc.inner.pairs.first().unwrap(),
876 &LinePair {
877 original: Some(0),
878 modified: Some(0),
879 changed: false
880 }
881 );
882 for side in [DiffSide::Original, DiffSide::Modified] {
883 assert_eq!(doc.text_for_lines(side, 1, usize::MAX), doc.source(side));
884 }
885 let same = fixture::modified("empty", "", "");
886 assert!(!same.has_changes());
887 assert_eq!(same.lines_count(DiffSide::Original), 0);
888 let added = fixture::document(None, Some(("new", "one\n\n")));
889 assert_eq!((added.additions(), added.deletions()), (2, 0));
890 assert!(added.inner.pairs.iter().all(|p| p.original.is_none()));
891 let deleted = fixture::document(Some(("old", "one\n")), None);
892 assert_eq!((deleted.additions(), deleted.deletions()), (0, 1));
893 assert!(fixture::document(None, Some(("empty", ""))).has_changes());
894 }
895
896 #[test]
897 fn alignment_covers_each_source_line_once() {
898 for (old, new) in [
899 ("one\ntwo\n", "one\ninserted\ntwo\n"),
900 ("one\nremoved\ntwo\n", "one\ntwo\n"),
901 ("old one\nold two\n", "new one\n"),
902 (
903 "repeat\nold\nrepeat\nsame\nold again\n",
904 "repeat\nnew\nrepeat\nsame\nnew again\n",
905 ),
906 ("same\n", "same\r\n"),
907 ("same\n", "same"),
908 ("a\rb", "a\rc"),
909 ("a\rb\nlast\r", "a\rc\nlast\r\n"),
910 ("", "\n"),
911 ("\n", ""),
912 ] {
913 let doc = fixture::modified("a.txt", old, new);
914 assert_eq!(
915 doc.inner
916 .pairs
917 .iter()
918 .filter_map(|pair| pair.original)
919 .collect::<Vec<_>>(),
920 (0..doc.lines_count(DiffSide::Original)).collect::<Vec<_>>()
921 );
922 assert_eq!(
923 doc.inner
924 .pairs
925 .iter()
926 .filter_map(|pair| pair.modified)
927 .collect::<Vec<_>>(),
928 (0..doc.lines_count(DiffSide::Modified)).collect::<Vec<_>>()
929 );
930 assert_eq!(
931 doc.deletions(),
932 doc.inner
933 .pairs
934 .iter()
935 .filter(|pair| pair.changed && pair.original.is_some())
936 .count()
937 );
938 assert_eq!(
939 doc.additions(),
940 doc.inner
941 .pairs
942 .iter()
943 .filter(|pair| pair.changed && pair.modified.is_some())
944 .count()
945 );
946 assert!(doc.has_changes());
947 for pair in &doc.inner.pairs {
948 if !pair.changed {
949 let old_line = &doc.lines(DiffSide::Original)[pair.original.unwrap()];
950 let new_line = &doc.lines(DiffSide::Modified)[pair.modified.unwrap()];
951 assert_eq!(&old[old_line.source.clone()], &new[new_line.source.clone()]);
952 }
953 }
954 }
955 let doc = fixture::modified("a.txt", "same\nold\ntail\n", "same\nnew\nextra\ntail\n");
956 assert_eq!(
957 doc.inner.pairs[1],
958 LinePair {
959 original: Some(1),
960 modified: Some(1),
961 changed: true
962 }
963 );
964 assert_eq!(
965 doc.inner.pairs[2],
966 LinePair {
967 original: None,
968 modified: Some(2),
969 changed: true
970 }
971 );
972 assert_eq!(doc.text_for_lines(DiffSide::Modified, 2, 3), "new\nextra\n");
973 assert_eq!(doc.text_for_lines(DiffSide::Original, 99, 100), "");
974 let bare_cr = fixture::modified("a.txt", "a\rb", "a\rb");
975 assert!(!bare_cr.has_changes());
976 assert_eq!(bare_cr.lines_count(DiffSide::Original), 1);
977 assert_eq!(bare_cr.inner.pairs.len(), 1);
978 }
979
980 #[test]
981 fn source_display_mapping_preserves_tabs_and_unicode() {
982 let lines = source_lines("é\t中\t\r\nlast\r");
983 let first = &lines[0];
984 assert_eq!(first.text.as_str(), "é\t中\t");
985 assert_eq!(first.display.as_str(), "é 中 ");
986 assert_eq!(first.source, 0..9);
987 assert_eq!(first.content_end, 7);
988 assert_eq!(first.display_tabs.len(), 2);
989 assert_eq!(first.display_range(2..3), 2..5);
990 assert_eq!(first.display_range(6..7), 8..11);
991 assert_eq!(
992 first.display_range(0..first.text.len()),
993 0..first.display.len()
994 );
995 for offset in 2..5 {
996 assert_eq!(first.source_offset(offset), 2);
997 }
998 assert_eq!(first.source_offset(5), 3);
999 assert_eq!(first.source_offset(first.display.len()), first.text.len());
1000 assert_eq!(first.source_offset(usize::MAX), first.text.len());
1001 assert_eq!(lines[1].text.as_str(), "last\r");
1002 assert!(lines[1].display_tabs.is_empty());
1003 assert_eq!(lines[1].source_offset(2), 2);
1004 assert_eq!(lines[1].source_offset(usize::MAX), lines[1].text.len());
1005 assert_eq!(lines[1].display_range(1..4), 1..4);
1006 let plain_unicode = source_lines("中é");
1007 assert!(plain_unicode[0].display_tabs.is_empty());
1008 assert_eq!(plain_unicode[0].display_range(3..5), 3..5);
1009 assert_eq!(plain_unicode[0].source_offset(3), 3);
1010 assert!(source_lines("").is_empty());
1011 assert_eq!(source_lines("\n").len(), 1);
1012 for (display_offset, source_offset) in
1015 [0, 1, 2, 2, 2, 3, 4, 5, 6, 6, 6, 7].into_iter().enumerate()
1016 {
1017 assert_eq!(first.source_offset(display_offset), source_offset);
1018 }
1019 for (source_offset, display_offset) in [0, 1, 2, 5, 6, 7, 8, 11].into_iter().enumerate() {
1020 assert_eq!(
1021 first.display_range(source_offset..source_offset),
1022 display_offset..display_offset
1023 );
1024 }
1025 let adjacent_tabs = source_lines("\t\té\t");
1026 assert_eq!(adjacent_tabs[0].display.as_str(), " é ");
1027 assert_eq!(adjacent_tabs[0].display_tabs.len(), 3);
1028 for (source_offset, display_offset) in [0, 4, 8, 9, 10, 13].into_iter().enumerate() {
1029 assert_eq!(
1030 adjacent_tabs[0].display_range(source_offset..source_offset),
1031 display_offset..display_offset
1032 );
1033 }
1034 let long_line = source_lines("a long source line without tabs uses shared heap storage");
1035 assert_eq!(long_line[0].text.as_ptr(), long_line[0].display.as_ptr());
1036 let long_tab_line = source_lines(&format!("{}\t", "a".repeat(100_000)));
1037 assert_eq!(long_tab_line[0].display_tabs.len(), 1);
1038 }
1039
1040 #[test]
1041 fn display_chunks_retain_complete_utf8_source() {
1042 let ordinary = "an ordinary line uses the existing shared display string";
1043 let long_grapheme = format!("a{}", "\u{301}".repeat(600));
1044 for text in [
1045 String::new(),
1046 ordinary.to_owned(),
1047 "é👩💻中".repeat(200),
1048 long_grapheme,
1049 ] {
1050 let chunks = display_chunks(&text);
1051 let mut previous_end = 0;
1052 for range in &chunks {
1053 assert_eq!(range.start, previous_end);
1054 assert!(range.len() <= 512);
1055 assert!(text.is_char_boundary(range.start) && text.is_char_boundary(range.end));
1056 previous_end = range.end;
1057 }
1058 assert_eq!(previous_end, text.len());
1059 assert_eq!(
1060 chunks
1061 .iter()
1062 .map(|range| &text[range.clone()])
1063 .collect::<String>(),
1064 text
1065 );
1066 if text.is_empty() {
1067 continue;
1068 }
1069 let lines = source_lines(&text);
1070 let line = &lines[0];
1071 assert_eq!(line.chunks.len(), chunks.len());
1072 assert_eq!(line.chunk_text(&(0..line.display.len())), line.display);
1073 for chunk in &line.chunks {
1074 let retained = line.chunk_text(&chunk.range);
1075 assert_eq!(retained.as_str(), &text[chunk.range.clone()]);
1076 if retained.len() > 23 {
1077 assert_eq!(retained.as_ptr(), chunk.text.as_ptr());
1078 }
1079 }
1080 }
1081 let normal_graphemes = "é👩💻中".repeat(200);
1082 let boundaries = normal_graphemes
1083 .grapheme_indices(true)
1084 .map(|(ix, _)| ix)
1085 .chain([normal_graphemes.len()])
1086 .collect::<Vec<_>>();
1087 for range in display_chunks(&normal_graphemes) {
1088 assert!(boundaries.contains(&range.start) && boundaries.contains(&range.end));
1089 }
1090 let line = &source_lines(ordinary)[0];
1091 assert_eq!(line.display.as_ptr(), line.chunks[0].text.as_ptr());
1092 }
1093}