1//! Text diffing utilities.
2use std::borrow::Cow;
3use std::cmp::Reverse;
4use std::collections::BinaryHeap;
5use std::time::{Duration, Instant};
6
7mod abstraction;
8#[cfg(feature = "inline")]
9mod inline;
10mod utils;
11
12pub use self::abstraction::{DiffableStr, DiffableStrRef};
13#[cfg(feature = "inline")]
14pub use self::inline::InlineChange;
15
16use self::utils::{upper_seq_ratio, QuickSeqRatio};
17use crate::algorithms::IdentifyDistinct;
18use crate::iter::{AllChangesIter, ChangesIter};
19use crate::udiff::UnifiedDiff;
20use crate::{capture_diff_deadline, get_diff_ratio, group_diff_ops, Algorithm, DiffOp};
21
22#[derive(Debug, Clone, Copy)]
23enum Deadline {
24 Absolute(Instant),
25 Relative(Duration),
26}
27
28impl Deadline {
29 fn into_instant(self) -> Instant {
30 match self {
31 Deadline::Absolute(instant: Instant) => instant,
32 Deadline::Relative(duration: Duration) => Instant::now() + duration,
33 }
34 }
35}
36
37/// A builder type config for more complex uses of [`TextDiff`].
38///
39/// Requires the `text` feature.
40#[derive(Clone, Debug, Default)]
41pub struct TextDiffConfig {
42 algorithm: Algorithm,
43 newline_terminated: Option<bool>,
44 deadline: Option<Deadline>,
45}
46
47impl TextDiffConfig {
48 /// Changes the algorithm.
49 ///
50 /// The default algorithm is [`Algorithm::Myers`].
51 pub fn algorithm(&mut self, alg: Algorithm) -> &mut Self {
52 self.algorithm = alg;
53 self
54 }
55
56 /// Sets a deadline for the diff operation.
57 ///
58 /// By default a diff will take as long as it takes. For certain diff
59 /// algorithms like Myer's and Patience a maximum running time can be
60 /// defined after which the algorithm gives up and approximates.
61 pub fn deadline(&mut self, deadline: Instant) -> &mut Self {
62 self.deadline = Some(Deadline::Absolute(deadline));
63 self
64 }
65
66 /// Sets a timeout for thediff operation.
67 ///
68 /// This is like [`deadline`](Self::deadline) but accepts a duration.
69 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
70 self.deadline = Some(Deadline::Relative(timeout));
71 self
72 }
73
74 /// Changes the newline termination flag.
75 ///
76 /// The default is automatic based on input. This flag controls the
77 /// behavior of [`TextDiff::iter_changes`] and unified diff generation
78 /// with regards to newlines. When the flag is set to `false` (which
79 /// is the default) then newlines are added. Otherwise the newlines
80 /// from the source sequences are reused.
81 pub fn newline_terminated(&mut self, yes: bool) -> &mut Self {
82 self.newline_terminated = Some(yes);
83 self
84 }
85
86 /// Creates a diff of lines.
87 ///
88 /// This splits the text `old` and `new` into lines preserving newlines
89 /// in the input. Line diffs are very common and because of that enjoy
90 /// special handling in similar. When a line diff is created with this
91 /// method the `newline_terminated` flag is flipped to `true` and will
92 /// influence the behavior of unified diff generation.
93 ///
94 /// ```rust
95 /// use similar::{TextDiff, ChangeTag};
96 ///
97 /// let diff = TextDiff::configure().diff_lines("a\nb\nc", "a\nb\nC");
98 /// let changes: Vec<_> = diff
99 /// .iter_all_changes()
100 /// .map(|x| (x.tag(), x.value()))
101 /// .collect();
102 ///
103 /// assert_eq!(changes, vec![
104 /// (ChangeTag::Equal, "a\n"),
105 /// (ChangeTag::Equal, "b\n"),
106 /// (ChangeTag::Delete, "c"),
107 /// (ChangeTag::Insert, "C"),
108 /// ]);
109 /// ```
110 pub fn diff_lines<'old, 'new, 'bufs, T: DiffableStrRef + ?Sized>(
111 &self,
112 old: &'old T,
113 new: &'new T,
114 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
115 self.diff(
116 Cow::Owned(old.as_diffable_str().tokenize_lines()),
117 Cow::Owned(new.as_diffable_str().tokenize_lines()),
118 true,
119 )
120 }
121
122 /// Creates a diff of words.
123 ///
124 /// This splits the text into words and whitespace.
125 ///
126 /// Note on word diffs: because the text differ will tokenize the strings
127 /// into small segments it can be inconvenient to work with the results
128 /// depending on the use case. You might also want to combine word level
129 /// diffs with the [`TextDiffRemapper`](crate::utils::TextDiffRemapper)
130 /// which lets you remap the diffs back to the original input strings.
131 ///
132 /// ```rust
133 /// use similar::{TextDiff, ChangeTag};
134 ///
135 /// let diff = TextDiff::configure().diff_words("foo bar baz", "foo BAR baz");
136 /// let changes: Vec<_> = diff
137 /// .iter_all_changes()
138 /// .map(|x| (x.tag(), x.value()))
139 /// .collect();
140 ///
141 /// assert_eq!(changes, vec![
142 /// (ChangeTag::Equal, "foo"),
143 /// (ChangeTag::Equal, " "),
144 /// (ChangeTag::Delete, "bar"),
145 /// (ChangeTag::Insert, "BAR"),
146 /// (ChangeTag::Equal, " "),
147 /// (ChangeTag::Equal, "baz"),
148 /// ]);
149 /// ```
150 pub fn diff_words<'old, 'new, 'bufs, T: DiffableStrRef + ?Sized>(
151 &self,
152 old: &'old T,
153 new: &'new T,
154 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
155 self.diff(
156 Cow::Owned(old.as_diffable_str().tokenize_words()),
157 Cow::Owned(new.as_diffable_str().tokenize_words()),
158 false,
159 )
160 }
161
162 /// Creates a diff of characters.
163 ///
164 /// Note on character diffs: because the text differ will tokenize the strings
165 /// into small segments it can be inconvenient to work with the results
166 /// depending on the use case. You might also want to combine word level
167 /// diffs with the [`TextDiffRemapper`](crate::utils::TextDiffRemapper)
168 /// which lets you remap the diffs back to the original input strings.
169 ///
170 /// ```rust
171 /// use similar::{TextDiff, ChangeTag};
172 ///
173 /// let diff = TextDiff::configure().diff_chars("abcdef", "abcDDf");
174 /// let changes: Vec<_> = diff
175 /// .iter_all_changes()
176 /// .map(|x| (x.tag(), x.value()))
177 /// .collect();
178 ///
179 /// assert_eq!(changes, vec![
180 /// (ChangeTag::Equal, "a"),
181 /// (ChangeTag::Equal, "b"),
182 /// (ChangeTag::Equal, "c"),
183 /// (ChangeTag::Delete, "d"),
184 /// (ChangeTag::Delete, "e"),
185 /// (ChangeTag::Insert, "D"),
186 /// (ChangeTag::Insert, "D"),
187 /// (ChangeTag::Equal, "f"),
188 /// ]);
189 /// ```
190 pub fn diff_chars<'old, 'new, 'bufs, T: DiffableStrRef + ?Sized>(
191 &self,
192 old: &'old T,
193 new: &'new T,
194 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
195 self.diff(
196 Cow::Owned(old.as_diffable_str().tokenize_chars()),
197 Cow::Owned(new.as_diffable_str().tokenize_chars()),
198 false,
199 )
200 }
201
202 /// Creates a diff of unicode words.
203 ///
204 /// This splits the text into words according to unicode rules. This is
205 /// generally recommended over [`TextDiffConfig::diff_words`] but
206 /// requires a dependency.
207 ///
208 /// This requires the `unicode` feature.
209 ///
210 /// Note on word diffs: because the text differ will tokenize the strings
211 /// into small segments it can be inconvenient to work with the results
212 /// depending on the use case. You might also want to combine word level
213 /// diffs with the [`TextDiffRemapper`](crate::utils::TextDiffRemapper)
214 /// which lets you remap the diffs back to the original input strings.
215 ///
216 /// ```rust
217 /// use similar::{TextDiff, ChangeTag};
218 ///
219 /// let diff = TextDiff::configure().diff_unicode_words("ah(be)ce", "ah(ah)ce");
220 /// let changes: Vec<_> = diff
221 /// .iter_all_changes()
222 /// .map(|x| (x.tag(), x.value()))
223 /// .collect();
224 ///
225 /// assert_eq!(changes, vec![
226 /// (ChangeTag::Equal, "ah"),
227 /// (ChangeTag::Equal, "("),
228 /// (ChangeTag::Delete, "be"),
229 /// (ChangeTag::Insert, "ah"),
230 /// (ChangeTag::Equal, ")"),
231 /// (ChangeTag::Equal, "ce"),
232 /// ]);
233 /// ```
234 #[cfg(feature = "unicode")]
235 pub fn diff_unicode_words<'old, 'new, 'bufs, T: DiffableStrRef + ?Sized>(
236 &self,
237 old: &'old T,
238 new: &'new T,
239 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
240 self.diff(
241 Cow::Owned(old.as_diffable_str().tokenize_unicode_words()),
242 Cow::Owned(new.as_diffable_str().tokenize_unicode_words()),
243 false,
244 )
245 }
246
247 /// Creates a diff of graphemes.
248 ///
249 /// This requires the `unicode` feature.
250 ///
251 /// Note on grapheme diffs: because the text differ will tokenize the strings
252 /// into small segments it can be inconvenient to work with the results
253 /// depending on the use case. You might also want to combine word level
254 /// diffs with the [`TextDiffRemapper`](crate::utils::TextDiffRemapper)
255 /// which lets you remap the diffs back to the original input strings.
256 ///
257 /// ```rust
258 /// use similar::{TextDiff, ChangeTag};
259 ///
260 /// let diff = TextDiff::configure().diff_graphemes("💩🇦🇹🦠", "💩🇦🇱❄️");
261 /// let changes: Vec<_> = diff
262 /// .iter_all_changes()
263 /// .map(|x| (x.tag(), x.value()))
264 /// .collect();
265 ///
266 /// assert_eq!(changes, vec![
267 /// (ChangeTag::Equal, "💩"),
268 /// (ChangeTag::Delete, "🇦🇹"),
269 /// (ChangeTag::Delete, "🦠"),
270 /// (ChangeTag::Insert, "🇦🇱"),
271 /// (ChangeTag::Insert, "❄️"),
272 /// ]);
273 /// ```
274 #[cfg(feature = "unicode")]
275 pub fn diff_graphemes<'old, 'new, 'bufs, T: DiffableStrRef + ?Sized>(
276 &self,
277 old: &'old T,
278 new: &'new T,
279 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
280 self.diff(
281 Cow::Owned(old.as_diffable_str().tokenize_graphemes()),
282 Cow::Owned(new.as_diffable_str().tokenize_graphemes()),
283 false,
284 )
285 }
286
287 /// Creates a diff of arbitrary slices.
288 ///
289 /// ```rust
290 /// use similar::{TextDiff, ChangeTag};
291 ///
292 /// let old = &["foo", "bar", "baz"];
293 /// let new = &["foo", "BAR", "baz"];
294 /// let diff = TextDiff::configure().diff_slices(old, new);
295 /// let changes: Vec<_> = diff
296 /// .iter_all_changes()
297 /// .map(|x| (x.tag(), x.value()))
298 /// .collect();
299 ///
300 /// assert_eq!(changes, vec![
301 /// (ChangeTag::Equal, "foo"),
302 /// (ChangeTag::Delete, "bar"),
303 /// (ChangeTag::Insert, "BAR"),
304 /// (ChangeTag::Equal, "baz"),
305 /// ]);
306 /// ```
307 pub fn diff_slices<'old, 'new, 'bufs, T: DiffableStr + ?Sized>(
308 &self,
309 old: &'bufs [&'old T],
310 new: &'bufs [&'new T],
311 ) -> TextDiff<'old, 'new, 'bufs, T> {
312 self.diff(Cow::Borrowed(old), Cow::Borrowed(new), false)
313 }
314
315 fn diff<'old, 'new, 'bufs, T: DiffableStr + ?Sized>(
316 &self,
317 old: Cow<'bufs, [&'old T]>,
318 new: Cow<'bufs, [&'new T]>,
319 newline_terminated: bool,
320 ) -> TextDiff<'old, 'new, 'bufs, T> {
321 let deadline = self.deadline.map(|x| x.into_instant());
322 let ops = if old.len() > 100 || new.len() > 100 {
323 let ih = IdentifyDistinct::<u32>::new(&old[..], 0..old.len(), &new[..], 0..new.len());
324 capture_diff_deadline(
325 self.algorithm,
326 ih.old_lookup(),
327 ih.old_range(),
328 ih.new_lookup(),
329 ih.new_range(),
330 deadline,
331 )
332 } else {
333 capture_diff_deadline(
334 self.algorithm,
335 &old[..],
336 0..old.len(),
337 &new[..],
338 0..new.len(),
339 deadline,
340 )
341 };
342 TextDiff {
343 old,
344 new,
345 ops,
346 newline_terminated: self.newline_terminated.unwrap_or(newline_terminated),
347 algorithm: self.algorithm,
348 }
349 }
350}
351
352/// Captures diff op codes for textual diffs.
353///
354/// The exact diff behavior is depending on the underlying [`DiffableStr`].
355/// For instance diffs on bytes and strings are slightly different. You can
356/// create a text diff from constructors such as [`TextDiff::from_lines`] or
357/// the [`TextDiffConfig`] created by [`TextDiff::configure`].
358///
359/// Requires the `text` feature.
360pub struct TextDiff<'old, 'new, 'bufs, T: DiffableStr + ?Sized> {
361 old: Cow<'bufs, [&'old T]>,
362 new: Cow<'bufs, [&'new T]>,
363 ops: Vec<DiffOp>,
364 newline_terminated: bool,
365 algorithm: Algorithm,
366}
367
368impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs, str> {
369 /// Configures a text differ before diffing.
370 pub fn configure() -> TextDiffConfig {
371 TextDiffConfig::default()
372 }
373
374 /// Creates a diff of lines.
375 ///
376 /// For more information see [`TextDiffConfig::diff_lines`].
377 pub fn from_lines<T: DiffableStrRef + ?Sized>(
378 old: &'old T,
379 new: &'new T,
380 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
381 TextDiff::configure().diff_lines(old, new)
382 }
383
384 /// Creates a diff of words.
385 ///
386 /// For more information see [`TextDiffConfig::diff_words`].
387 pub fn from_words<T: DiffableStrRef + ?Sized>(
388 old: &'old T,
389 new: &'new T,
390 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
391 TextDiff::configure().diff_words(old, new)
392 }
393
394 /// Creates a diff of chars.
395 ///
396 /// For more information see [`TextDiffConfig::diff_chars`].
397 pub fn from_chars<T: DiffableStrRef + ?Sized>(
398 old: &'old T,
399 new: &'new T,
400 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
401 TextDiff::configure().diff_chars(old, new)
402 }
403
404 /// Creates a diff of unicode words.
405 ///
406 /// For more information see [`TextDiffConfig::diff_unicode_words`].
407 ///
408 /// This requires the `unicode` feature.
409 #[cfg(feature = "unicode")]
410 pub fn from_unicode_words<T: DiffableStrRef + ?Sized>(
411 old: &'old T,
412 new: &'new T,
413 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
414 TextDiff::configure().diff_unicode_words(old, new)
415 }
416
417 /// Creates a diff of graphemes.
418 ///
419 /// For more information see [`TextDiffConfig::diff_graphemes`].
420 ///
421 /// This requires the `unicode` feature.
422 #[cfg(feature = "unicode")]
423 pub fn from_graphemes<T: DiffableStrRef + ?Sized>(
424 old: &'old T,
425 new: &'new T,
426 ) -> TextDiff<'old, 'new, 'bufs, T::Output> {
427 TextDiff::configure().diff_graphemes(old, new)
428 }
429}
430
431impl<'old, 'new, 'bufs, T: DiffableStr + ?Sized + 'old + 'new> TextDiff<'old, 'new, 'bufs, T> {
432 /// Creates a diff of arbitrary slices.
433 ///
434 /// For more information see [`TextDiffConfig::diff_slices`].
435 pub fn from_slices(
436 old: &'bufs [&'old T],
437 new: &'bufs [&'new T],
438 ) -> TextDiff<'old, 'new, 'bufs, T> {
439 TextDiff::configure().diff_slices(old, new)
440 }
441
442 /// The name of the algorithm that created the diff.
443 pub fn algorithm(&self) -> Algorithm {
444 self.algorithm
445 }
446
447 /// Returns `true` if items in the slice are newline terminated.
448 ///
449 /// This flag is used by the unified diff writer to determine if extra
450 /// newlines have to be added.
451 pub fn newline_terminated(&self) -> bool {
452 self.newline_terminated
453 }
454
455 /// Returns all old slices.
456 pub fn old_slices(&self) -> &[&'old T] {
457 &self.old
458 }
459
460 /// Returns all new slices.
461 pub fn new_slices(&self) -> &[&'new T] {
462 &self.new
463 }
464
465 /// Return a measure of the sequences' similarity in the range `0..=1`.
466 ///
467 /// A ratio of `1.0` means the two sequences are a complete match, a
468 /// ratio of `0.0` would indicate completely distinct sequences.
469 ///
470 /// ```rust
471 /// # use similar::TextDiff;
472 /// let diff = TextDiff::from_chars("abcd", "bcde");
473 /// assert_eq!(diff.ratio(), 0.75);
474 /// ```
475 pub fn ratio(&self) -> f32 {
476 get_diff_ratio(self.ops(), self.old.len(), self.new.len())
477 }
478
479 /// Iterates over the changes the op expands to.
480 ///
481 /// This method is a convenient way to automatically resolve the different
482 /// ways in which a change could be encoded (insert/delete vs replace), look
483 /// up the value from the appropriate slice and also handle correct index
484 /// handling.
485 pub fn iter_changes<'x, 'slf>(
486 &'slf self,
487 op: &DiffOp,
488 ) -> ChangesIter<'slf, [&'x T], [&'x T], &'x T>
489 where
490 'x: 'slf,
491 'old: 'x,
492 'new: 'x,
493 {
494 op.iter_changes(self.old_slices(), self.new_slices())
495 }
496
497 /// Returns the captured diff ops.
498 pub fn ops(&self) -> &[DiffOp] {
499 &self.ops
500 }
501
502 /// Isolate change clusters by eliminating ranges with no changes.
503 ///
504 /// This is equivalent to calling [`group_diff_ops`] on [`TextDiff::ops`].
505 pub fn grouped_ops(&self, n: usize) -> Vec<Vec<DiffOp>> {
506 group_diff_ops(self.ops().to_vec(), n)
507 }
508
509 /// Flattens out the diff into all changes.
510 ///
511 /// This is a shortcut for combining [`TextDiff::ops`] with
512 /// [`TextDiff::iter_changes`].
513 pub fn iter_all_changes<'x, 'slf>(&'slf self) -> AllChangesIter<'slf, 'x, T>
514 where
515 'x: 'slf + 'old + 'new,
516 'old: 'x,
517 'new: 'x,
518 {
519 AllChangesIter::new(&self.old[..], &self.new[..], self.ops())
520 }
521
522 /// Utility to return a unified diff formatter.
523 pub fn unified_diff<'diff>(&'diff self) -> UnifiedDiff<'diff, 'old, 'new, 'bufs, T> {
524 UnifiedDiff::from_text_diff(self)
525 }
526
527 /// Iterates over the changes the op expands to with inline emphasis.
528 ///
529 /// This is very similar to [`TextDiff::iter_changes`] but it performs a second
530 /// level diff on adjacent line replacements. The exact behavior of
531 /// this function with regards to how it detects those inline changes
532 /// is currently not defined and will likely change over time.
533 ///
534 /// This method has a hardcoded 500ms deadline which is often not ideal. For
535 /// fine tuning use [`iter_inline_changes_deadline`](Self::iter_inline_changes_deadline).
536 ///
537 /// As of similar 1.2.0 the behavior of this function changes depending on
538 /// if the `unicode` feature is enabled or not. It will prefer unicode word
539 /// splitting over word splitting depending on the feature flag.
540 ///
541 /// Requires the `inline` feature.
542 #[cfg(feature = "inline")]
543 pub fn iter_inline_changes<'slf>(
544 &'slf self,
545 op: &DiffOp,
546 ) -> impl Iterator<Item = InlineChange<'slf, T>> + '_
547 where
548 'slf: 'old + 'new,
549 {
550 inline::iter_inline_changes(self, op, Some(Instant::now() + Duration::from_millis(500)))
551 }
552
553 /// Iterates over the changes the op expands to with inline emphasis with a deadline.
554 ///
555 /// Like [`iter_inline_changes`](Self::iter_inline_changes) but with an explicit deadline.
556 #[cfg(feature = "inline")]
557 pub fn iter_inline_changes_deadline<'slf>(
558 &'slf self,
559 op: &DiffOp,
560 deadline: Option<Instant>,
561 ) -> impl Iterator<Item = InlineChange<'slf, T>> + '_
562 where
563 'slf: 'old + 'new,
564 {
565 inline::iter_inline_changes(self, op, deadline)
566 }
567}
568
569/// Use the text differ to find `n` close matches.
570///
571/// `cutoff` defines the threshold which needs to be reached for a word
572/// to be considered similar. See [`TextDiff::ratio`] for more information.
573///
574/// ```
575/// # use similar::get_close_matches;
576/// let matches = get_close_matches(
577/// "appel",
578/// &["ape", "apple", "peach", "puppy"][..],
579/// 3,
580/// 0.6
581/// );
582/// assert_eq!(matches, vec!["apple", "ape"]);
583/// ```
584///
585/// Requires the `text` feature.
586pub fn get_close_matches<'a, T: DiffableStr + ?Sized>(
587 word: &T,
588 possibilities: &[&'a T],
589 n: usize,
590 cutoff: f32,
591) -> Vec<&'a T> {
592 let mut matches = BinaryHeap::new();
593 let seq1 = word.tokenize_chars();
594 let quick_ratio = QuickSeqRatio::new(&seq1);
595
596 for &possibility in possibilities {
597 let seq2 = possibility.tokenize_chars();
598
599 if upper_seq_ratio(&seq1, &seq2) < cutoff || quick_ratio.calc(&seq2) < cutoff {
600 continue;
601 }
602
603 let diff = TextDiff::from_slices(&seq1, &seq2);
604 let ratio = diff.ratio();
605 if ratio >= cutoff {
606 // we're putting the word itself in reverse in so that matches with
607 // the same ratio are ordered lexicographically.
608 matches.push(((ratio * u32::MAX as f32) as u32, Reverse(possibility)));
609 }
610 }
611
612 let mut rv = vec![];
613 for _ in 0..n {
614 if let Some((_, elt)) = matches.pop() {
615 rv.push(elt.0);
616 } else {
617 break;
618 }
619 }
620
621 rv
622}
623
624#[test]
625fn test_captured_ops() {
626 let diff = TextDiff::from_lines(
627 "Hello World\nsome stuff here\nsome more stuff here\n",
628 "Hello World\nsome amazing stuff here\nsome more stuff here\n",
629 );
630 insta::assert_debug_snapshot!(&diff.ops());
631}
632
633#[test]
634fn test_captured_word_ops() {
635 let diff = TextDiff::from_words(
636 "Hello World\nsome stuff here\nsome more stuff here\n",
637 "Hello World\nsome amazing stuff here\nsome more stuff here\n",
638 );
639 let changes = diff
640 .ops()
641 .iter()
642 .flat_map(|op| diff.iter_changes(op))
643 .collect::<Vec<_>>();
644 insta::assert_debug_snapshot!(&changes);
645}
646
647#[test]
648fn test_unified_diff() {
649 let diff = TextDiff::from_lines(
650 "Hello World\nsome stuff here\nsome more stuff here\n",
651 "Hello World\nsome amazing stuff here\nsome more stuff here\n",
652 );
653 assert!(diff.newline_terminated());
654 insta::assert_snapshot!(&diff
655 .unified_diff()
656 .context_radius(3)
657 .header("old", "new")
658 .to_string());
659}
660
661#[test]
662fn test_line_ops() {
663 let a = "Hello World\nsome stuff here\nsome more stuff here\n";
664 let b = "Hello World\nsome amazing stuff here\nsome more stuff here\n";
665 let diff = TextDiff::from_lines(a, b);
666 assert!(diff.newline_terminated());
667 let changes = diff
668 .ops()
669 .iter()
670 .flat_map(|op| diff.iter_changes(op))
671 .collect::<Vec<_>>();
672 insta::assert_debug_snapshot!(&changes);
673
674 #[cfg(feature = "bytes")]
675 {
676 let byte_diff = TextDiff::from_lines(a.as_bytes(), b.as_bytes());
677 let byte_changes = byte_diff
678 .ops()
679 .iter()
680 .flat_map(|op| byte_diff.iter_changes(op))
681 .collect::<Vec<_>>();
682 for (change, byte_change) in changes.iter().zip(byte_changes.iter()) {
683 assert_eq!(change.to_string_lossy(), byte_change.to_string_lossy());
684 }
685 }
686}
687
688#[test]
689fn test_virtual_newlines() {
690 let diff = TextDiff::from_lines("a\nb", "a\nc\n");
691 assert!(diff.newline_terminated());
692 let changes = diff
693 .ops()
694 .iter()
695 .flat_map(|op| diff.iter_changes(op))
696 .collect::<Vec<_>>();
697 insta::assert_debug_snapshot!(&changes);
698}
699
700#[test]
701fn test_char_diff() {
702 let diff = TextDiff::from_chars("Hello World", "Hallo Welt");
703 insta::assert_debug_snapshot!(diff.ops());
704
705 #[cfg(feature = "bytes")]
706 {
707 let byte_diff = TextDiff::from_chars("Hello World".as_bytes(), "Hallo Welt".as_bytes());
708 assert_eq!(diff.ops(), byte_diff.ops());
709 }
710}
711
712#[test]
713fn test_ratio() {
714 let diff = TextDiff::from_chars("abcd", "bcde");
715 assert_eq!(diff.ratio(), 0.75);
716 let diff = TextDiff::from_chars("", "");
717 assert_eq!(diff.ratio(), 1.0);
718}
719
720#[test]
721fn test_get_close_matches() {
722 let matches = get_close_matches("appel", &["ape", "apple", "peach", "puppy"][..], 3, 0.6);
723 assert_eq!(matches, vec!["apple", "ape"]);
724 let matches = get_close_matches(
725 "hulo",
726 &[
727 "hi", "hulu", "hali", "hoho", "amaz", "zulo", "blah", "hopp", "uulo", "aulo",
728 ][..],
729 5,
730 0.7,
731 );
732 assert_eq!(matches, vec!["aulo", "hulu", "uulo", "zulo"]);
733}
734
735#[test]
736fn test_lifetimes_on_iter() {
737 use crate::Change;
738
739 fn diff_lines<'x, T>(old: &'x T, new: &'x T) -> Vec<Change<&'x T::Output>>
740 where
741 T: DiffableStrRef + ?Sized,
742 {
743 TextDiff::from_lines(old, new).iter_all_changes().collect()
744 }
745
746 let a = "1\n2\n3\n".to_string();
747 let b = "1\n99\n3\n".to_string();
748 let changes = diff_lines(&a, &b);
749 insta::assert_debug_snapshot!(&changes);
750}
751
752#[test]
753#[cfg(feature = "serde")]
754fn test_serde() {
755 let diff = TextDiff::from_lines(
756 "Hello World\nsome stuff here\nsome more stuff here\n\nAha stuff here\nand more stuff",
757 "Stuff\nHello World\nsome amazing stuff here\nsome more stuff here\n",
758 );
759 let changes = diff
760 .ops()
761 .iter()
762 .flat_map(|op| diff.iter_changes(op))
763 .collect::<Vec<_>>();
764 let json = serde_json::to_string_pretty(&changes).unwrap();
765 insta::assert_snapshot!(&json);
766}
767
768#[test]
769#[cfg(feature = "serde")]
770fn test_serde_ops() {
771 let diff = TextDiff::from_lines(
772 "Hello World\nsome stuff here\nsome more stuff here\n\nAha stuff here\nand more stuff",
773 "Stuff\nHello World\nsome amazing stuff here\nsome more stuff here\n",
774 );
775 let changes = diff.ops();
776 let json = serde_json::to_string_pretty(&changes).unwrap();
777 insta::assert_snapshot!(&json);
778}
779
780#[test]
781fn test_regression_issue_37() {
782 let config = TextDiffConfig::default();
783 let diff = config.diff_lines("\u{18}\n\n", "\n\n\r");
784 let mut output = diff.unified_diff();
785 assert_eq!(
786 output.context_radius(0).to_string(),
787 "@@ -1 +1,0 @@\n-\u{18}\n@@ -2,0 +2,2 @@\n+\n+\r"
788 );
789}
790