1 | //! Utils for diff text |
2 | pub use ansi_term::Style; |
3 | |
4 | use crate::basic; |
5 | cfg_prettytable! { |
6 | use crate::format_table; |
7 | use prettytable::{Cell, Row}; |
8 | } |
9 | use ansi_term::Colour; |
10 | use pad::{Alignment, PadStr}; |
11 | use std::{ |
12 | cmp::{max, min}, |
13 | fmt, |
14 | }; |
15 | |
16 | pub struct StringSplitIter<'a, F> |
17 | where |
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 | |
26 | impl<'a, F> Iterator for StringSplitIter<'a, F> |
27 | where |
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 | |
54 | pub 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 |
59 | pub fn split_by_char_fn<F>(text: &'_ str, pat: F) -> StringSplitIter<'_, F> |
60 | where |
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 |
72 | pub 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)] |
78 | pub 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 | |
89 | impl<'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 | |
190 | impl<'a> fmt::Display for InlineChangeset<'a> { |
191 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
192 | write!(formatter, " {}" , self.format()) |
193 | } |
194 | } |
195 | |
196 | pub 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) |
204 | pub 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" )] |
209 | fn 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)] |
214 | pub 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)] |
221 | pub 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 | |
232 | impl<'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 | |
570 | impl<'a> fmt::Display for LineChangeset<'a> { |
571 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
572 | write!(formatter, " {}" , self.format()) |
573 | } |
574 | } |
575 | |
576 | pub 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 | |
583 | fn _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 ] |
591 | fn 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 ] |
611 | fn 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 ] |
652 | fn 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 ] |
673 | fn test_diff_lines() { |
674 | let code1_a = r#" |
675 | void func1() { |
676 | x += 1 |
677 | } |
678 | |
679 | void func2() { |
680 | x += 2 |
681 | } |
682 | "# ; |
683 | let code1_b = r#" |
684 | void func1(a: u32) { |
685 | x += 1 |
686 | } |
687 | |
688 | void functhreehalves() { |
689 | x += 1.5 |
690 | } |
691 | |
692 | void func2() { |
693 | x += 2 |
694 | } |
695 | |
696 | void 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 | |
707 | fn _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 ] |
720 | fn 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 ] |
796 | fn test_prettytable_process() { |
797 | let d1: LineChangeset<'_> = diff_lines( |
798 | old:r#"line1 |
799 | old: line2 |
800 | old: line3 |
801 | old: "# , |
802 | new:r#"line1 |
803 | new: line2 |
804 | new: line2.5 |
805 | new: line3 |
806 | new: "# , |
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 ] |
817 | fn 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 | |