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