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