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