1 #[cfg(feature = "alloc")]
2 use alloc::format;
3 use ansi_term::{
4 Colour::{Fixed, Green, Red},
5 Style,
6 };
7 use core::fmt;
8
9 macro_rules! paint {
10 ($f:expr, $colour:expr, $fmt:expr, $($args:tt)*) => (
11 write!($f, "{}", $colour.paint(format!($fmt, $($args)*)))
12 )
13 }
14
15 const SIGN_RIGHT: char = '>'; // + > →
16 const SIGN_LEFT: char = '<'; // - < ←
17
18 /// Present the diff output for two mutliline strings in a pretty, colorised manner.
write_header(f: &mut fmt::Formatter) -> fmt::Result19 pub(crate) fn write_header(f: &mut fmt::Formatter) -> fmt::Result {
20 writeln!(
21 f,
22 "{} {} / {} :",
23 Style::new().bold().paint("Diff"),
24 Red.paint(format!("{} left", SIGN_LEFT)),
25 Green.paint(format!("right {}", SIGN_RIGHT))
26 )
27 }
28
29 /// Delay formatting this deleted chunk until later.
30 ///
31 /// It can be formatted as a whole chunk by calling `flush`, or the inner value
32 /// obtained with `take` for further processing.
33 #[derive(Default)]
34 struct LatentDeletion<'a> {
35 // The most recent deleted line we've seen
36 value: Option<&'a str>,
37 // The number of deleted lines we've seen, including the current value
38 count: usize,
39 }
40
41 impl<'a> LatentDeletion<'a> {
42 /// Set the chunk value.
set(&mut self, value: &'a str)43 fn set(&mut self, value: &'a str) {
44 self.value = Some(value);
45 self.count += 1;
46 }
47
48 /// Take the underlying chunk value, if it's suitable for inline diffing.
49 ///
50 /// If there is no value of we've seen more than one line, return `None`.
take(&mut self) -> Option<&'a str>51 fn take(&mut self) -> Option<&'a str> {
52 if self.count == 1 {
53 self.value.take()
54 } else {
55 None
56 }
57 }
58
59 /// If a value is set, print it as a whole chunk, using the given formatter.
60 ///
61 /// If a value is not set, reset the count to zero (as we've called `flush` twice,
62 /// without seeing another deletion. Therefore the line in the middle was something else).
flush<TWrite: fmt::Write>(&mut self, f: &mut TWrite) -> fmt::Result63 fn flush<TWrite: fmt::Write>(&mut self, f: &mut TWrite) -> fmt::Result {
64 if let Some(value) = self.value {
65 paint!(f, Red, "{}{}", SIGN_LEFT, value)?;
66 writeln!(f)?;
67 self.value = None;
68 } else {
69 self.count = 0;
70 }
71
72 Ok(())
73 }
74 }
75
76 // Adapted from:
77 // https://github.com/johannhof/difference.rs/blob/c5749ad7d82aa3d480c15cb61af9f6baa08f116f/examples/github-style.rs
78 // Credits johannhof (MIT License)
79
80 /// Present the diff output for two mutliline strings in a pretty, colorised manner.
write_lines<TWrite: fmt::Write>( f: &mut TWrite, left: &str, right: &str, ) -> fmt::Result81 pub(crate) fn write_lines<TWrite: fmt::Write>(
82 f: &mut TWrite,
83 left: &str,
84 right: &str,
85 ) -> fmt::Result {
86 let diff = ::diff::lines(left, right);
87
88 let mut changes = diff.into_iter().peekable();
89 let mut previous_deletion = LatentDeletion::default();
90
91 while let Some(change) = changes.next() {
92 match (change, changes.peek()) {
93 // If the text is unchanged, just print it plain
94 (::diff::Result::Both(value, _), _) => {
95 previous_deletion.flush(f)?;
96 writeln!(f, " {}", value)?;
97 }
98 // Defer any deletions to next loop
99 (::diff::Result::Left(deleted), _) => {
100 previous_deletion.flush(f)?;
101 previous_deletion.set(deleted);
102 }
103 // Underlying diff library should never return this sequence
104 (::diff::Result::Right(_), Some(::diff::Result::Left(_))) => {
105 panic!("insertion followed by deletion");
106 }
107 // If we're being followed by more insertions, don't inline diff
108 (::diff::Result::Right(inserted), Some(::diff::Result::Right(_))) => {
109 previous_deletion.flush(f)?;
110 paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?;
111 writeln!(f)?;
112 }
113 // Otherwise, check if we need to inline diff with the previous line (if it was a deletion)
114 (::diff::Result::Right(inserted), _) => {
115 if let Some(deleted) = previous_deletion.take() {
116 write_inline_diff(f, deleted, inserted)?;
117 } else {
118 previous_deletion.flush(f)?;
119 paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?;
120 writeln!(f)?;
121 }
122 }
123 };
124 }
125
126 previous_deletion.flush(f)?;
127 Ok(())
128 }
129
130 /// Group character styling for an inline diff, to prevent wrapping each single
131 /// character in terminal styling codes.
132 ///
133 /// Styles are applied automatically each time a new style is given in `write_with_style`.
134 struct InlineWriter<'a, Writer> {
135 f: &'a mut Writer,
136 style: Style,
137 }
138
139 impl<'a, Writer> InlineWriter<'a, Writer>
140 where
141 Writer: fmt::Write,
142 {
new(f: &'a mut Writer) -> Self143 fn new(f: &'a mut Writer) -> Self {
144 InlineWriter {
145 f,
146 style: Style::new(),
147 }
148 }
149
150 /// Push a new character into the buffer, specifying the style it should be written in.
write_with_style(&mut self, c: &char, style: Style) -> fmt::Result151 fn write_with_style(&mut self, c: &char, style: Style) -> fmt::Result {
152 // If the style is the same as previously, just write character
153 if style == self.style {
154 write!(self.f, "{}", c)?;
155 } else {
156 // Close out previous style
157 write!(self.f, "{}", self.style.suffix())?;
158
159 // Store new style and start writing it
160 write!(self.f, "{}{}", style.prefix(), c)?;
161 self.style = style;
162 }
163 Ok(())
164 }
165
166 /// Finish any existing style and reset to default state.
finish(&mut self) -> fmt::Result167 fn finish(&mut self) -> fmt::Result {
168 // Close out previous style
169 writeln!(self.f, "{}", self.style.suffix())?;
170 self.style = Default::default();
171 Ok(())
172 }
173 }
174
175 /// Format a single line to show an inline diff of the two strings given.
176 ///
177 /// The given strings should not have a trailing newline.
178 ///
179 /// The output of this function will be two lines, each with a trailing newline.
write_inline_diff<TWrite: fmt::Write>(f: &mut TWrite, left: &str, right: &str) -> fmt::Result180 fn write_inline_diff<TWrite: fmt::Write>(f: &mut TWrite, left: &str, right: &str) -> fmt::Result {
181 let diff = ::diff::chars(left, right);
182 let mut writer = InlineWriter::new(f);
183
184 // Print the left string on one line, with differences highlighted
185 let light = Red.into();
186 let heavy = Red.on(Fixed(52)).bold();
187 writer.write_with_style(&SIGN_LEFT, light)?;
188 for change in diff.iter() {
189 match change {
190 ::diff::Result::Both(value, _) => writer.write_with_style(value, light)?,
191 ::diff::Result::Left(value) => writer.write_with_style(value, heavy)?,
192 _ => (),
193 }
194 }
195 writer.finish()?;
196
197 // Print the right string on one line, with differences highlighted
198 let light = Green.into();
199 let heavy = Green.on(Fixed(22)).bold();
200 writer.write_with_style(&SIGN_RIGHT, light)?;
201 for change in diff.iter() {
202 match change {
203 ::diff::Result::Both(value, _) => writer.write_with_style(value, light)?,
204 ::diff::Result::Right(value) => writer.write_with_style(value, heavy)?,
205 _ => (),
206 }
207 }
208 writer.finish()
209 }
210
211 #[cfg(test)]
212 mod test {
213 use super::*;
214
215 #[cfg(feature = "alloc")]
216 use alloc::string::String;
217
218 // ANSI terminal codes used in our outputs.
219 //
220 // Interpolate these into test strings to make expected values easier to read.
221 const RED_LIGHT: &str = "\u{1b}[31m";
222 const GREEN_LIGHT: &str = "\u{1b}[32m";
223 const RED_HEAVY: &str = "\u{1b}[1;48;5;52;31m";
224 const GREEN_HEAVY: &str = "\u{1b}[1;48;5;22;32m";
225 const RESET: &str = "\u{1b}[0m";
226
227 /// Given that both of our diff printing functions have the same
228 /// type signature, we can reuse the same test code for them.
229 ///
230 /// This could probably be nicer with traits!
check_printer<TPrint>(printer: TPrint, left: &str, right: &str, expected: &str) where TPrint: Fn(&mut String, &str, &str) -> fmt::Result,231 fn check_printer<TPrint>(printer: TPrint, left: &str, right: &str, expected: &str)
232 where
233 TPrint: Fn(&mut String, &str, &str) -> fmt::Result,
234 {
235 let mut actual = String::new();
236 printer(&mut actual, left, right).expect("printer function failed");
237
238 // Cannot use IO without stdlib
239 #[cfg(feature = "std")]
240 println!(
241 "## left ##\n\
242 {}\n\
243 ## right ##\n\
244 {}\n\
245 ## actual diff ##\n\
246 {}\n\
247 ## expected diff ##\n\
248 {}",
249 left, right, actual, expected
250 );
251 assert_eq!(actual, expected);
252 }
253
254 #[test]
write_inline_diff_empty()255 fn write_inline_diff_empty() {
256 let left = "";
257 let right = "";
258 let expected = format!(
259 "{red_light}<{reset}\n\
260 {green_light}>{reset}\n",
261 red_light = RED_LIGHT,
262 green_light = GREEN_LIGHT,
263 reset = RESET,
264 );
265
266 check_printer(write_inline_diff, left, right, &expected);
267 }
268
269 #[test]
write_inline_diff_added()270 fn write_inline_diff_added() {
271 let left = "";
272 let right = "polymerase";
273 let expected = format!(
274 "{red_light}<{reset}\n\
275 {green_light}>{reset}{green_heavy}polymerase{reset}\n",
276 red_light = RED_LIGHT,
277 green_light = GREEN_LIGHT,
278 green_heavy = GREEN_HEAVY,
279 reset = RESET,
280 );
281
282 check_printer(write_inline_diff, left, right, &expected);
283 }
284
285 #[test]
write_inline_diff_removed()286 fn write_inline_diff_removed() {
287 let left = "polyacrylamide";
288 let right = "";
289 let expected = format!(
290 "{red_light}<{reset}{red_heavy}polyacrylamide{reset}\n\
291 {green_light}>{reset}\n",
292 red_light = RED_LIGHT,
293 green_light = GREEN_LIGHT,
294 red_heavy = RED_HEAVY,
295 reset = RESET,
296 );
297
298 check_printer(write_inline_diff, left, right, &expected);
299 }
300
301 #[test]
write_inline_diff_changed()302 fn write_inline_diff_changed() {
303 let left = "polymerase";
304 let right = "polyacrylamide";
305 let expected = format!(
306 "{red_light}<poly{reset}{red_heavy}me{reset}{red_light}ra{reset}{red_heavy}s{reset}{red_light}e{reset}\n\
307 {green_light}>poly{reset}{green_heavy}ac{reset}{green_light}r{reset}{green_heavy}yl{reset}{green_light}a{reset}{green_heavy}mid{reset}{green_light}e{reset}\n",
308 red_light = RED_LIGHT,
309 green_light = GREEN_LIGHT,
310 red_heavy = RED_HEAVY,
311 green_heavy = GREEN_HEAVY,
312 reset = RESET,
313 );
314
315 check_printer(write_inline_diff, left, right, &expected);
316 }
317
318 /// If one of our strings is empty, it should not be shown at all in the output.
319 #[test]
write_lines_empty_string()320 fn write_lines_empty_string() {
321 let left = "";
322 let right = "content";
323 let expected = format!(
324 "{green_light}>content{reset}\n",
325 green_light = GREEN_LIGHT,
326 reset = RESET,
327 );
328
329 check_printer(write_lines, left, right, &expected);
330 }
331
332 /// Realistic multiline struct diffing case.
333 #[test]
write_lines_struct()334 fn write_lines_struct() {
335 let left = r#"Some(
336 Foo {
337 lorem: "Hello World!",
338 ipsum: 42,
339 dolor: Ok(
340 "hey",
341 ),
342 },
343 )"#;
344 let right = r#"Some(
345 Foo {
346 lorem: "Hello Wrold!",
347 ipsum: 42,
348 dolor: Ok(
349 "hey ho!",
350 ),
351 },
352 )"#;
353 let expected = format!(
354 r#" Some(
355 Foo {{
356 {red_light}< lorem: "Hello W{reset}{red_heavy}o{reset}{red_light}rld!",{reset}
357 {green_light}> lorem: "Hello Wr{reset}{green_heavy}o{reset}{green_light}ld!",{reset}
358 ipsum: 42,
359 dolor: Ok(
360 {red_light}< "hey",{reset}
361 {green_light}> "hey{reset}{green_heavy} ho!{reset}{green_light}",{reset}
362 ),
363 }},
364 )
365 "#,
366 red_light = RED_LIGHT,
367 red_heavy = RED_HEAVY,
368 green_light = GREEN_LIGHT,
369 green_heavy = GREEN_HEAVY,
370 reset = RESET,
371 );
372
373 check_printer(write_lines, left, right, &expected);
374 }
375
376 /// Relistic multiple line chunks
377 ///
378 /// We can't support realistic line diffing in large blocks
379 /// (also, it's unclear how usefult this is)
380 ///
381 /// So if we have more than one line in a single removal chunk, disable inline diffing.
382 #[test]
write_lines_multiline_block()383 fn write_lines_multiline_block() {
384 let left = r#"Proboscis
385 Cabbage"#;
386 let right = r#"Probed
387 Caravaggio"#;
388 let expected = format!(
389 r#"{red_light}<Proboscis{reset}
390 {red_light}<Cabbage{reset}
391 {green_light}>Probed{reset}
392 {green_light}>Caravaggio{reset}
393 "#,
394 red_light = RED_LIGHT,
395 green_light = GREEN_LIGHT,
396 reset = RESET,
397 );
398
399 check_printer(write_lines, left, right, &expected);
400 }
401
402 /// Single deletion line, multiple insertions - no inline diffing.
403 #[test]
write_lines_multiline_insert()404 fn write_lines_multiline_insert() {
405 let left = r#"Cabbage"#;
406 let right = r#"Probed
407 Caravaggio"#;
408 let expected = format!(
409 r#"{red_light}<Cabbage{reset}
410 {green_light}>Probed{reset}
411 {green_light}>Caravaggio{reset}
412 "#,
413 red_light = RED_LIGHT,
414 green_light = GREEN_LIGHT,
415 reset = RESET,
416 );
417
418 check_printer(write_lines, left, right, &expected);
419 }
420
421 /// Multiple deletion, single insertion - no inline diffing.
422 #[test]
write_lines_multiline_delete()423 fn write_lines_multiline_delete() {
424 let left = r#"Proboscis
425 Cabbage"#;
426 let right = r#"Probed"#;
427 let expected = format!(
428 r#"{red_light}<Proboscis{reset}
429 {red_light}<Cabbage{reset}
430 {green_light}>Probed{reset}
431 "#,
432 red_light = RED_LIGHT,
433 green_light = GREEN_LIGHT,
434 reset = RESET,
435 );
436
437 check_printer(write_lines, left, right, &expected);
438 }
439
440 /// Regression test for multiline highlighting issue
441 #[test]
write_lines_issue12()442 fn write_lines_issue12() {
443 let left = r#"[
444 0,
445 0,
446 0,
447 128,
448 10,
449 191,
450 5,
451 64,
452 ]"#;
453 let right = r#"[
454 84,
455 248,
456 45,
457 64,
458 ]"#;
459 let expected = format!(
460 r#" [
461 {red_light}< 0,{reset}
462 {red_light}< 0,{reset}
463 {red_light}< 0,{reset}
464 {red_light}< 128,{reset}
465 {red_light}< 10,{reset}
466 {red_light}< 191,{reset}
467 {red_light}< 5,{reset}
468 {green_light}> 84,{reset}
469 {green_light}> 248,{reset}
470 {green_light}> 45,{reset}
471 64,
472 ]
473 "#,
474 red_light = RED_LIGHT,
475 green_light = GREEN_LIGHT,
476 reset = RESET,
477 );
478
479 check_printer(write_lines, left, right, &expected);
480 }
481 }
482