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