1//! Utils for diff text
2pub use ansi_term::Style;
3
4use crate::basic;
5cfg_prettytable! {
6 use crate::format_table;
7 use prettytable::{Cell, Row};
8}
9use ansi_term::Colour;
10use pad::{Alignment, PadStr};
11use std::{
12 cmp::{max, min},
13 fmt,
14};
15
16pub struct StringSplitIter<'a, F>
17where
18 F: Fn(char) -> bool,
19{
20 last: usize,
21 text: &'a str,
22 matched: Option<&'a str>,
23 iter: std::str::MatchIndices<'a, F>,
24}
25
26impl<'a, F> Iterator for StringSplitIter<'a, F>
27where
28 F: Fn(char) -> bool,
29{
30 type Item = &'a str;
31 fn next(&mut self) -> Option<Self::Item> {
32 if let Some(m: &str) = self.matched {
33 self.matched = None;
34 Some(m)
35 } else if let Some((idx: usize, matched: &str)) = self.iter.next() {
36 let res: &str = if self.last != idx {
37 self.matched = Some(matched);
38 &self.text[self.last..idx]
39 } else {
40 matched
41 };
42 self.last = idx + matched.len();
43 Some(res)
44 } else if self.last < self.text.len() {
45 let res: &str = &self.text[self.last..];
46 self.last = self.text.len();
47 Some(res)
48 } else {
49 None
50 }
51 }
52}
53
54pub fn collect_strings<T: ToString>(it: impl Iterator<Item = T>) -> Vec<String> {
55 it.map(|s: T| s.to_string()).collect::<Vec<String>>()
56}
57
58/// Split string by clousure (Fn(char)->bool) keeping delemiters
59pub fn split_by_char_fn<F>(text: &'_ str, pat: F) -> StringSplitIter<'_, F>
60where
61 F: Fn(char) -> bool,
62{
63 StringSplitIter {
64 last: 0,
65 text,
66 matched: None,
67 iter: text.match_indices(pat),
68 }
69}
70
71/// Split string by non-alphanumeric characters keeping delemiters
72pub fn split_words(text: &str) -> impl Iterator<Item = &str> {
73 split_by_char_fn(text, |c: char| !c.is_alphanumeric())
74}
75
76/// Container for inline text diff result. Can be pretty-printed by Display trait.
77#[derive(Debug, PartialEq)]
78pub struct InlineChangeset<'a> {
79 old: Vec<&'a str>,
80 new: Vec<&'a str>,
81 separator: &'a str,
82 highlight_whitespace: bool,
83 insert_style: Style,
84 insert_whitespace_style: Style,
85 remove_style: Style,
86 remove_whitespace_style: Style,
87}
88
89impl<'a> InlineChangeset<'a> {
90 pub fn new(old: Vec<&'a str>, new: Vec<&'a str>) -> InlineChangeset<'a> {
91 InlineChangeset {
92 old,
93 new,
94 separator: "",
95 highlight_whitespace: true,
96 insert_style: Colour::Green.normal(),
97 insert_whitespace_style: Colour::White.on(Colour::Green),
98 remove_style: Colour::Red.strikethrough(),
99 remove_whitespace_style: Colour::White.on(Colour::Red),
100 }
101 }
102 /// Highlight whitespaces in case of insert/remove?
103 pub fn set_highlight_whitespace(mut self, val: bool) -> Self {
104 self.highlight_whitespace = val;
105 self
106 }
107
108 /// Style of inserted text
109 pub fn set_insert_style(mut self, val: Style) -> Self {
110 self.insert_style = val;
111 self
112 }
113
114 /// Style of inserted whitespace
115 pub fn set_insert_whitespace_style(mut self, val: Style) -> Self {
116 self.insert_whitespace_style = val;
117 self
118 }
119
120 /// Style of removed text
121 pub fn set_remove_style(mut self, val: Style) -> Self {
122 self.remove_style = val;
123 self
124 }
125
126 /// Style of removed whitespace
127 pub fn set_remove_whitespace_style(mut self, val: Style) -> Self {
128 self.remove_whitespace_style = val;
129 self
130 }
131
132 /// Set output separator
133 pub fn set_separator(mut self, val: &'a str) -> Self {
134 self.separator = val;
135 self
136 }
137
138 /// Returns Vec of changes
139 pub fn diff(&self) -> Vec<basic::DiffOp<'a, &str>> {
140 basic::diff(&self.old, &self.new)
141 }
142
143 fn apply_style(&self, style: Style, whitespace_style: Style, a: &[&str]) -> String {
144 let s = a.join(self.separator);
145 if self.highlight_whitespace {
146 collect_strings(split_by_char_fn(&s, |c| c.is_whitespace()).map(|s| {
147 let style = if s
148 .chars()
149 .next()
150 .map_or_else(|| false, |c| c.is_whitespace())
151 {
152 whitespace_style
153 } else {
154 style
155 };
156 style.paint(s)
157 }))
158 .join("")
159 } else {
160 style.paint(s).to_string()
161 }
162 }
163
164 fn remove_color(&self, a: &[&str]) -> String {
165 self.apply_style(self.remove_style, self.remove_whitespace_style, a)
166 }
167
168 fn insert_color(&self, a: &[&str]) -> String {
169 self.apply_style(self.insert_style, self.insert_whitespace_style, a)
170 }
171 /// Returns formatted string with colors
172 pub fn format(&self) -> String {
173 let diff = self.diff();
174 let mut out: Vec<String> = Vec::with_capacity(diff.len());
175 for op in diff {
176 match op {
177 basic::DiffOp::Equal(a) => out.push(a.join(self.separator)),
178 basic::DiffOp::Insert(a) => out.push(self.insert_color(a)),
179 basic::DiffOp::Remove(a) => out.push(self.remove_color(a)),
180 basic::DiffOp::Replace(a, b) => {
181 out.push(self.remove_color(a));
182 out.push(self.insert_color(b));
183 }
184 }
185 }
186 out.join(self.separator)
187 }
188}
189
190impl<'a> fmt::Display for InlineChangeset<'a> {
191 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
192 write!(formatter, "{}", self.format())
193 }
194}
195
196pub fn diff_chars<'a>(old: &'a str, new: &'a str) -> InlineChangeset<'a> {
197 let old: Vec<&str> = old.split("").filter(|&i: &str| i != "").collect();
198 let new: Vec<&str> = new.split("").filter(|&i: &str| i != "").collect();
199
200 InlineChangeset::new(old, new)
201}
202
203/// Diff two strings by words (contiguous)
204pub fn diff_words<'a>(old: &'a str, new: &'a str) -> InlineChangeset<'a> {
205 InlineChangeset::new(old:split_words(old).collect(), new:split_words(text:new).collect())
206}
207
208#[cfg(feature = "prettytable-rs")]
209fn color_multilines(color: Colour, s: &str) -> String {
210 collect_strings(s.split('\n').map(|i| color.paint(i))).join("\n")
211}
212
213#[derive(Debug)]
214pub struct ContextConfig<'a> {
215 pub context_size: usize,
216 pub skipping_marker: &'a str,
217}
218
219/// Container for line-by-line text diff result. Can be pretty-printed by Display trait.
220#[derive(Debug, PartialEq, Eq)]
221pub struct LineChangeset<'a> {
222 old: Vec<&'a str>,
223 new: Vec<&'a str>,
224
225 names: Option<(&'a str, &'a str)>,
226 diff_only: bool,
227 show_lines: bool,
228 trim_new_lines: bool,
229 aling_new_lines: bool,
230}
231
232impl<'a> LineChangeset<'a> {
233 pub fn new(old: Vec<&'a str>, new: Vec<&'a str>) -> LineChangeset<'a> {
234 LineChangeset {
235 old,
236 new,
237 names: None,
238 diff_only: false,
239 show_lines: true,
240 trim_new_lines: true,
241 aling_new_lines: false,
242 }
243 }
244
245 /// Sets names for side-by-side diff
246 pub fn names(mut self, old: &'a str, new: &'a str) -> Self {
247 self.names = Some((old, new));
248 self
249 }
250 /// Show only differences for side-by-side diff
251 pub fn set_diff_only(mut self, val: bool) -> Self {
252 self.diff_only = val;
253 self
254 }
255 /// Show lines in side-by-side diff
256 pub fn set_show_lines(mut self, val: bool) -> Self {
257 self.show_lines = val;
258 self
259 }
260 /// Trim new lines in side-by-side diff
261 pub fn set_trim_new_lines(mut self, val: bool) -> Self {
262 self.trim_new_lines = val;
263 self
264 }
265 /// Align new lines inside diff
266 pub fn set_align_new_lines(mut self, val: bool) -> Self {
267 self.aling_new_lines = val;
268 self
269 }
270 /// Returns Vec of changes
271 pub fn diff(&self) -> Vec<basic::DiffOp<'a, &str>> {
272 basic::diff(&self.old, &self.new)
273 }
274
275 #[cfg(feature = "prettytable-rs")]
276 fn prettytable_process(&self, a: &[&str], color: Option<Colour>) -> (String, usize) {
277 let mut start = 0;
278 let mut stop = a.len();
279 if self.trim_new_lines {
280 for (index, element) in a.iter().enumerate() {
281 if *element != "" {
282 break;
283 }
284 start = index + 1;
285 }
286 for (index, element) in a.iter().enumerate().rev() {
287 if *element != "" {
288 stop = index + 1;
289 break;
290 }
291 }
292 }
293 let out = &a[start..stop];
294 if let Some(color) = color {
295 (
296 collect_strings(out.iter().map(|i| color.paint(*i).to_string()))
297 .join("\n")
298 .replace("\t", " "),
299 start,
300 )
301 } else {
302 (out.join("\n").replace("\t", " "), start)
303 }
304 }
305
306 #[cfg(feature = "prettytable-rs")]
307 fn prettytable_process_replace(
308 &self,
309 old: &[&str],
310 new: &[&str],
311 ) -> ((String, String), (usize, usize)) {
312 let (old, old_offset) = self.prettytable_process(old, None);
313 let (new, new_offset) = self.prettytable_process(new, None);
314
315 let mut old_out = String::new();
316 let mut new_out = String::new();
317
318 for op in diff_words(&old, &new).diff() {
319 match op {
320 basic::DiffOp::Equal(a) => {
321 old_out.push_str(&a.join(""));
322 new_out.push_str(&a.join(""));
323 }
324 basic::DiffOp::Insert(a) => {
325 new_out.push_str(&color_multilines(Colour::Green, &a.join("")));
326 }
327 basic::DiffOp::Remove(a) => {
328 old_out.push_str(&color_multilines(Colour::Red, &a.join("")));
329 }
330 basic::DiffOp::Replace(a, b) => {
331 old_out.push_str(&color_multilines(Colour::Red, &a.join("")));
332 new_out.push_str(&color_multilines(Colour::Green, &b.join("")));
333 }
334 }
335 }
336
337 ((old_out, new_out), (old_offset, new_offset))
338 }
339
340 #[cfg(feature = "prettytable-rs")]
341 /// Prints side-by-side diff in table
342 pub fn prettytable(&self) {
343 let mut table = format_table::new();
344 if let Some((old, new)) = &self.names {
345 let mut header = vec![];
346 if self.show_lines {
347 header.push(Cell::new(""));
348 }
349 header.push(Cell::new(&Colour::Cyan.paint(old.to_string()).to_string()));
350 if self.show_lines {
351 header.push(Cell::new(""));
352 }
353 header.push(Cell::new(&Colour::Cyan.paint(new.to_string()).to_string()));
354 table.set_titles(Row::new(header));
355 }
356 let mut old_lines = 1;
357 let mut new_lines = 1;
358 let mut out: Vec<(usize, String, usize, String)> = Vec::new();
359 for op in &self.diff() {
360 match op {
361 basic::DiffOp::Equal(a) => {
362 let (old, offset) = self.prettytable_process(a, None);
363 if !self.diff_only {
364 out.push((old_lines + offset, old.clone(), new_lines + offset, old));
365 }
366 old_lines += a.len();
367 new_lines += a.len();
368 }
369 basic::DiffOp::Insert(a) => {
370 let (new, offset) = self.prettytable_process(a, Some(Colour::Green));
371 out.push((old_lines, "".to_string(), new_lines + offset, new));
372 new_lines += a.len();
373 }
374 basic::DiffOp::Remove(a) => {
375 let (old, offset) = self.prettytable_process(a, Some(Colour::Red));
376 out.push((old_lines + offset, old, new_lines, "".to_string()));
377 old_lines += a.len();
378 }
379 basic::DiffOp::Replace(a, b) => {
380 let ((old, new), (old_offset, new_offset)) =
381 self.prettytable_process_replace(a, b);
382 out.push((old_lines + old_offset, old, new_lines + new_offset, new));
383 old_lines += a.len();
384 new_lines += b.len();
385 }
386 };
387 }
388 for (old_lines, old, new_lines, new) in out {
389 if self.trim_new_lines && old.trim() == "" && new.trim() == "" {
390 continue;
391 }
392 if self.show_lines {
393 table.add_row(row![old_lines, old, new_lines, new]);
394 } else {
395 table.add_row(row![old, new]);
396 }
397 }
398 table.printstd();
399 }
400
401 fn remove_color(&self, a: &str) -> String {
402 Colour::Red.strikethrough().paint(a).to_string()
403 }
404
405 fn insert_color(&self, a: &str) -> String {
406 Colour::Green.paint(a).to_string()
407 }
408
409 /// Returns formatted string with colors
410 pub fn format(&self) -> String {
411 self.format_with_context(None, false)
412 }
413
414 /// Formats lines in DiffOp::Equal
415 fn format_equal(
416 &self,
417 lines: &[&str],
418 display_line_numbers: bool,
419 prefix_size: usize,
420 line_counter: &mut usize,
421 ) -> Option<String> {
422 lines
423 .iter()
424 .map(|line| {
425 let res = if display_line_numbers {
426 format!("{} ", *line_counter)
427 .pad_to_width_with_alignment(prefix_size, Alignment::Right)
428 + line
429 } else {
430 "".pad_to_width(prefix_size) + line
431 };
432 *line_counter += 1;
433 res
434 })
435 .reduce(|acc, line| acc + "\n" + &line)
436 }
437
438 /// Formats lines in DiffOp::Remove
439 fn format_remove(
440 &self,
441 lines: &[&str],
442 display_line_numbers: bool,
443 prefix_size: usize,
444 line_counter: &mut usize,
445 ) -> String {
446 lines
447 .iter()
448 .map(|line| {
449 let res = if display_line_numbers {
450 format!("{} ", *line_counter)
451 .pad_to_width_with_alignment(prefix_size, Alignment::Right)
452 + &self.remove_color(line)
453 } else {
454 "".pad_to_width(prefix_size) + &self.remove_color(line)
455 };
456 *line_counter += 1;
457 res
458 })
459 .reduce(|acc, line| acc + "\n" + &line)
460 .unwrap()
461 }
462
463 /// Formats lines in DiffOp::Insert
464 fn format_insert(&self, lines: &[&str], prefix_size: usize) -> String {
465 lines
466 .iter()
467 .map(|line| "".pad_to_width(prefix_size) + &self.insert_color(line))
468 .reduce(|acc, line| acc + "\n" + &line)
469 .unwrap()
470 }
471
472 /// Returns formatted string with colors.
473 /// May omit identical lines, if `context_size` is `Some(k)`.
474 /// In this case, only print identical lines if they are within `k` lines
475 /// of a changed line (as in `diff -C`).
476 pub fn format_with_context(
477 &self,
478 context_config: Option<ContextConfig>,
479 display_line_numbers: bool,
480 ) -> String {
481 let line_number_size = if display_line_numbers {
482 (self.old.len() as f64).log10().ceil() as usize
483 } else {
484 0
485 };
486 let skipping_marker_size = if let Some(ContextConfig {
487 skipping_marker, ..
488 }) = context_config
489 {
490 skipping_marker.len()
491 } else {
492 0
493 };
494 let prefix_size = max(line_number_size, skipping_marker_size) + 1;
495
496 let mut next_line = 1;
497
498 let mut diff = self.diff().into_iter().peekable();
499 let mut out: Vec<String> = Vec::with_capacity(diff.len());
500 let mut at_beginning = true;
501 while let Some(op) = diff.next() {
502 match op {
503 basic::DiffOp::Equal(a) => match context_config {
504 None => out.push(a.join("\n")),
505 Some(ContextConfig {
506 context_size,
507 skipping_marker,
508 }) => {
509 let mut lines = a;
510 if !at_beginning {
511 let upper_bound = min(context_size, lines.len());
512 if let Some(newlines) = self.format_equal(
513 &lines[..upper_bound],
514 display_line_numbers,
515 prefix_size,
516 &mut next_line,
517 ) {
518 out.push(newlines)
519 }
520 lines = &lines[upper_bound..];
521 }
522 if lines.len() == 0 {
523 continue;
524 }
525 let lower_bound = if lines.len() > context_size {
526 lines.len() - context_size
527 } else {
528 0
529 };
530 if lower_bound > 0 {
531 out.push(skipping_marker.to_string());
532 next_line += lower_bound
533 }
534 if diff.peek().is_none() {
535 continue;
536 }
537 if let Some(newlines) = self.format_equal(
538 &lines[lower_bound..],
539 display_line_numbers,
540 prefix_size,
541 &mut next_line,
542 ) {
543 out.push(newlines)
544 }
545 }
546 },
547 basic::DiffOp::Insert(a) => out.push(self.format_insert(a, prefix_size)),
548 basic::DiffOp::Remove(a) => out.push(self.format_remove(
549 a,
550 display_line_numbers,
551 prefix_size,
552 &mut next_line,
553 )),
554 basic::DiffOp::Replace(a, b) => {
555 out.push(self.format_remove(
556 a,
557 display_line_numbers,
558 prefix_size,
559 &mut next_line,
560 ));
561 out.push(self.format_insert(b, prefix_size));
562 }
563 }
564 at_beginning = false;
565 }
566 out.join("\n")
567 }
568}
569
570impl<'a> fmt::Display for LineChangeset<'a> {
571 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
572 write!(formatter, "{}", self.format())
573 }
574}
575
576pub fn diff_lines<'a>(old: &'a str, new: &'a str) -> LineChangeset<'a> {
577 let old: Vec<&str> = old.lines().collect();
578 let new: Vec<&str> = new.lines().collect();
579
580 LineChangeset::new(old, new)
581}
582
583fn _test_splitter_basic(text: &str, exp: &[&str]) {
584 let res: Vec = collect_strings(
585 it:split_by_char_fn(&text, |c: char| c.is_whitespace()).map(|s: &str| s.to_string()),
586 );
587 assert_eq!(res, exp)
588}
589
590#[test]
591fn test_splitter() {
592 _test_splitter_basic(
593 text:" blah test2 test3 ",
594 &[" ", " ", "blah", " ", "test2", " ", "test3", " ", " "],
595 );
596 _test_splitter_basic(
597 text:"\tblah test2 test3 ",
598 &["\t", "blah", " ", "test2", " ", "test3", " ", " "],
599 );
600 _test_splitter_basic(
601 text:"\tblah test2 test3 t",
602 &["\t", "blah", " ", "test2", " ", "test3", " ", " ", "t"],
603 );
604 _test_splitter_basic(
605 text:"\tblah test2 test3 tt",
606 &["\t", "blah", " ", "test2", " ", "test3", " ", " ", "tt"],
607 );
608}
609
610#[test]
611fn test_basic() {
612 println!("diff_chars: {}", diff_chars("abefcd", "zadqwc"));
613 println!(
614 "diff_chars: {}",
615 diff_chars(
616 "The quick brown fox jumps over the lazy dog",
617 "The quick brown dog leaps over the lazy cat"
618 )
619 );
620 println!(
621 "diff_chars: {}",
622 diff_chars(
623 "The red brown fox jumped over the rolling log",
624 "The brown spotted fox leaped over the rolling log"
625 )
626 );
627 println!(
628 "diff_chars: {}",
629 diff_chars(
630 "The red brown fox jumped over the rolling log",
631 "The brown spotted fox leaped over the rolling log"
632 )
633 .set_highlight_whitespace(true)
634 );
635 println!(
636 "diff_words: {}",
637 diff_words(
638 "The red brown fox jumped over the rolling log",
639 "The brown spotted fox leaped over the rolling log"
640 )
641 );
642 println!(
643 "diff_words: {}",
644 diff_words(
645 "The quick brown fox jumps over the lazy dog",
646 "The quick, brown dog leaps over the lazy cat"
647 )
648 );
649}
650
651#[test]
652fn test_split_words() {
653 assert_eq!(
654 collect_strings(split_words("Hello World")),
655 ["Hello", " ", "World"]
656 );
657 assert_eq!(
658 collect_strings(split_words("Hello😋World")),
659 ["Hello", "😋", "World"]
660 );
661 assert_eq!(
662 collect_strings(split_words(
663 "The red brown fox\tjumped, over the rolling log"
664 )),
665 [
666 "The", " ", "red", " ", "brown", " ", "fox", "\t", "jumped", ",", " ", "over", " ",
667 "the", " ", "rolling", " ", "log"
668 ]
669 );
670}
671
672#[test]
673fn test_diff_lines() {
674 let code1_a = r#"
675void func1() {
676 x += 1
677}
678
679void func2() {
680 x += 2
681}
682 "#;
683 let code1_b = r#"
684void func1(a: u32) {
685 x += 1
686}
687
688void functhreehalves() {
689 x += 1.5
690}
691
692void func2() {
693 x += 2
694}
695
696void func3(){}
697"#;
698 println!("diff_lines:");
699 println!("{}", diff_lines(code1_a, code1_b));
700 println!("====");
701 diff_lines(code1_a, code1_b)
702 .names("left", "right")
703 .set_align_new_lines(true)
704 .prettytable();
705}
706
707fn _test_colors(changeset: &InlineChangeset, exp: &[(Option<Style>, &str)]) {
708 let color_s: String = collect_strings(exp.iter().map(|(style_opt, s)| {
709 if let Some(style) = style_opt {
710 style.paint(s.to_string()).to_string()
711 } else {
712 s.to_string()
713 }
714 }))
715 .join(sep:"");
716 assert_eq!(format!("{}", changeset), color_s);
717}
718
719#[test]
720fn test_diff_words_issue_1() {
721 let insert_style = Colour::Green.normal();
722 let insert_whitespace_style = Colour::White.on(Colour::Green);
723 let remove_style = Colour::Red.strikethrough();
724 let remove_whitespace_style = Colour::White.on(Colour::Red);
725 let d1 = diff_words(
726 "und meine Unschuld beweisen!",
727 "und ich werde meine Unschuld beweisen!",
728 )
729 .set_insert_style(insert_style)
730 .set_insert_whitespace_style(insert_whitespace_style)
731 .set_remove_style(remove_style)
732 .set_remove_whitespace_style(remove_whitespace_style);
733
734 println!("diff_words: {} {:?}", d1, d1.diff());
735
736 _test_colors(
737 &d1,
738 &[
739 (None, "und "),
740 (Some(insert_style), "ich"),
741 (Some(insert_whitespace_style), " "),
742 (Some(insert_style), "werde"),
743 (Some(insert_whitespace_style), " "),
744 (None, "meine Unschuld beweisen!"),
745 ],
746 );
747 _test_colors(
748 &d1.set_highlight_whitespace(false),
749 &[
750 (None, "und "),
751 (Some(insert_style), "ich werde "),
752 (None, "meine Unschuld beweisen!"),
753 ],
754 );
755 let d2 = diff_words(
756 "Campaignings aus dem Ausland gegen meine Person ausfindig",
757 "Campaignings ausfindig",
758 );
759 println!("diff_words: {} {:?}", d2, d2.diff());
760 _test_colors(
761 &d2,
762 &[
763 (None, "Campaignings "),
764 (Some(remove_style), "aus"),
765 (Some(remove_whitespace_style), " "),
766 (Some(remove_style), "dem"),
767 (Some(remove_whitespace_style), " "),
768 (Some(remove_style), "Ausland"),
769 (Some(remove_whitespace_style), " "),
770 (Some(remove_style), "gegen"),
771 (Some(remove_whitespace_style), " "),
772 (Some(remove_style), "meine"),
773 (Some(remove_whitespace_style), " "),
774 (Some(remove_style), "Person"),
775 (Some(remove_whitespace_style), " "),
776 (None, "ausfindig"),
777 ],
778 );
779 let d3 = diff_words("des kriminellen Videos", "des kriminell erstellten Videos");
780 println!("diff_words: {} {:?}", d3, d3.diff());
781 _test_colors(
782 &d3,
783 &[
784 (None, "des "),
785 (Some(remove_style), "kriminellen"),
786 (Some(insert_style), "kriminell"),
787 (None, " "),
788 (Some(insert_style), "erstellten"),
789 (Some(insert_whitespace_style), " "),
790 (None, "Videos"),
791 ],
792 );
793}
794
795#[test]
796fn test_prettytable_process() {
797 let d1: LineChangeset<'_> = diff_lines(
798 old:r#"line1
799old: line2
800old: line3
801old: "#,
802 new:r#"line1
803new: line2
804new: line2.5
805new: line3
806new: "#,
807 );
808
809 println!("diff_lines: {} {:?}", d1, d1.diff());
810 assert_eq!(d1.prettytable_process(&["a", "b", "c"], None), (String::from("a\nb\nc"), 0));
811 assert_eq!(d1.prettytable_process(&["a", "b", "c", ""], None), (String::from("a\nb\nc"), 0));
812 assert_eq!(d1.prettytable_process(&["", "a", "b", "c"], None), (String::from("a\nb\nc"), 1));
813 assert_eq!(d1.prettytable_process(&["", "a", "b", "c", ""], None), (String::from("a\nb\nc"), 1));
814}
815
816#[test]
817fn test_format_with_context() {
818 let d = diff_lines(
819 r#"line1
820 line2
821 line3
822 line4
823 line5
824 line6
825 line7
826 line8
827 line9
828 line10
829 line11
830 line12"#,
831 r#"line1
832 line2
833 line4
834 line5
835 line6.5
836 line7
837 line8
838 line9
839 line10
840 line11.5
841 line12"#,
842 );
843 let context = |n| ContextConfig {
844 context_size: n,
845 skipping_marker: "...",
846 };
847 println!(
848 "diff_lines:\n{}\n{:?}",
849 d.format_with_context(Some(context(0)), true),
850 d.diff()
851 );
852 let formatted_none = d.format_with_context(None, true);
853 let formatted_some_0 = d.format_with_context(Some(context(0)), true);
854 let formatted_some_1 = d.format_with_context(Some(context(1)), true);
855 let formatted_some_2 = d.format_with_context(Some(context(2)), true);
856 // With a context of size 2, every line is present
857 assert_eq!(
858 formatted_none.lines().count(),
859 formatted_some_2.lines().count()
860 );
861 // with a context of size 1:
862 // * line 1 is replaced by '...' (-0 lines)
863 // * line 8-9 are replaced by '...' (-1 line)
864 assert_eq!(
865 formatted_none.lines().count() - 1,
866 formatted_some_1.lines().count()
867 );
868 // with a context of size 0:
869 // * lines 1-2 are replaced by '...' (-1 line)
870 // * lines 4-5 are replaced by '...' (-1 line)
871 // * lines 7-10 are replaced by '...' (-3 lines)
872 // * line 12 is replaced by '...' (-0 lines)
873 assert_eq!(
874 formatted_none.lines().count() - 5,
875 formatted_some_0.lines().count()
876 );
877}
878