assert2/__assert2_impl/print/
diff.rs

1use std::fmt::Write;
2use yansi::Paint;
3
4/// A line diff between two inputs.
5pub struct MultiLineDiff<'a> {
6	/// The actual diff results from the [`diff`] crate.
7	line_diffs: Vec<LineDiff<'a>>,
8}
9
10impl<'a> MultiLineDiff<'a> {
11	/// Create a new diff between a left and right input.
12	pub fn new(left: &'a str, right: &'a str) -> Self {
13		let line_diffs = LineDiff::from_diff(diff::lines(left, right));
14		Self {
15			line_diffs
16		}
17	}
18
19	/// Write the left and right input interleaved with eachother, highlighting the differences between the two.
20	pub fn write_interleaved(&self, buffer: &mut String) {
21		for diff in &self.line_diffs {
22			match *diff {
23				LineDiff::LeftOnly(left) => {
24					writeln!(buffer, "{}", Paint::cyan(&format_args!("< {left}"))).unwrap();
25				},
26				LineDiff::RightOnly(right) => {
27					writeln!(buffer, "{}", Paint::yellow(&format_args!("> {right}"))).unwrap();
28				},
29				LineDiff::Different(left, right) => {
30					let diff = SingleLineDiff::new(left, right);
31					write!(buffer, "{} ", "<".paint(diff.left_highlights.normal)).unwrap();
32					diff.write_left(buffer);
33					write!(buffer, "\n{} ", ">".paint(diff.right_highlights.normal)).unwrap();
34					diff.write_right(buffer);
35					buffer.push('\n');
36				},
37				LineDiff::Equal(text) => {
38					writeln!(buffer, "  {}", text.primary().on_primary().dim()).unwrap();
39				},
40			}
41		}
42		// Remove last newline.
43		buffer.pop();
44	}
45}
46
47enum LineDiff<'a> {
48	// There is only a left line.
49	LeftOnly(&'a str),
50	// There is only a right line.
51	RightOnly(&'a str),
52	// There is a left and a right line, but they are different.
53	Different(&'a str, &'a str),
54	// There is a left and a right line, and they are equal.
55	Equal(&'a str),
56}
57
58impl<'a> LineDiff<'a> {
59	fn from_diff(diffs: Vec<diff::Result<&'a str>>) -> Vec<Self> {
60		let mut output = Vec::with_capacity(diffs.len());
61
62		let mut seen_left = 0;
63		for item in diffs {
64			match item {
65				diff::Result::Left(l) => {
66					output.push(LineDiff::LeftOnly(l));
67					seen_left += 1;
68				},
69				diff::Result::Right(r) => {
70					if let Some(last) = output.last_mut() {
71						match last {
72							// If we see exactly one left line followed by a right line,
73							// make it a `Self::Different` entry so we perform word diff later.
74							Self::LeftOnly(old_l) if seen_left == 1 => {
75								*last = Self::Different(old_l, r);
76								seen_left = 0;
77								continue;
78							},
79							// If we see another right line, turn the `Self::Different` back into individual lines.
80							// This way, we dont do word diffs when one left line was replaced by multiple right lines.
81							Self::Different(old_l, old_r) => {
82								let old_r = *old_r;
83								*last = Self::LeftOnly(old_l);
84								output.push(Self::RightOnly(old_r));
85								output.push(Self::RightOnly(r));
86								seen_left = 0;
87								continue;
88							},
89							// In other cases, just continue to the default behaviour of adding a `RightOnly` entry.
90							Self::LeftOnly(_) => (),
91							Self::RightOnly(_) => (),
92							Self::Equal(_) => (),
93						}
94					}
95					output.push(LineDiff::RightOnly(r));
96					seen_left = 0;
97				},
98				diff::Result::Both(l, _r) => {
99					output.push(Self::Equal(l));
100					seen_left = 0;
101				}
102			}
103		}
104
105		output
106	}
107}
108
109/// A character/word based diff between two single-line inputs.
110pub struct SingleLineDiff<'a> {
111	/// The left line.
112	left: &'a str,
113
114	/// The right line.
115	right: &'a str,
116
117	/// The highlighting for the left line.
118	left_highlights: Highlighter,
119
120	/// The highlighting for the right line.
121	right_highlights: Highlighter,
122}
123
124impl<'a> SingleLineDiff<'a> {
125	/// Create a new word diff between two input lines.
126	pub fn new(left: &'a str, right: &'a str) -> Self {
127		let left_words = Self::split_words(left);
128		let right_words = Self::split_words(right);
129		let diffs = diff::slice(&left_words, &right_words);
130
131		let mut left_highlights = Highlighter::new(yansi::Color::Cyan);
132		let mut right_highlights = Highlighter::new(yansi::Color::Yellow);
133		for diff in &diffs {
134			match diff {
135				diff::Result::Left(left) => {
136					left_highlights.push(left.len(), true);
137				},
138				diff::Result::Right(right) => {
139					right_highlights.push(right.len(), true);
140				},
141				diff::Result::Both(left, right) => {
142					left_highlights.push(left.len(), false);
143					right_highlights.push(right.len(), false);
144				}
145			}
146		}
147
148		Self {
149			left,
150			right,
151			left_highlights,
152			right_highlights,
153		}
154	}
155
156	/// Write the left line with highlighting.
157	///
158	/// This does not write a line break to the buffer.
159	pub fn write_left(&self, buffer: &mut String) {
160		self.left_highlights.write_highlighted(buffer, self.left);
161	}
162
163	/// Write the right line with highlighting.
164	///
165	/// This does not write a line break to the buffer.
166	pub fn write_right(&self, buffer: &mut String) {
167		self.right_highlights.write_highlighted(buffer, self.right);
168	}
169
170	/// Split an input line into individual words.
171	fn split_words(mut input: &str) -> Vec<&str> {
172		/// Check if there should be a word break between character `a` and `b`.
173		fn is_break_point(a: char, b: char) -> bool {
174			if a.is_alphabetic() {
175				!b.is_alphabetic() || (a.is_lowercase() && !b.is_lowercase())
176			} else if a.is_ascii_digit() {
177				!b.is_ascii_digit()
178			} else if a.is_whitespace() {
179				!b.is_whitespace()
180			} else {
181				true
182			}
183		}
184
185		let mut output = Vec::new();
186		while !input.is_empty() {
187			let split = input.chars()
188				.zip(input.char_indices().skip(1))
189				.find_map(|(a, (pos, b))| Some(pos).filter(|_| is_break_point(a, b)))
190				.unwrap_or(input.len());
191			let (head, tail) = input.split_at(split);
192			output.push(head);
193			input = tail;
194		}
195		output
196	}
197}
198
199/// Highlighter that incrementaly builds a range of alternating styles.
200struct Highlighter {
201	/// The ranges of alternating highlighting.
202	///
203	/// If the boolean is true, the range should be printed with the `highlight` style.
204	/// If the boolean is false, the range should be printed with the `normal` style.
205	ranges: Vec<(bool, std::ops::Range<usize>)>,
206
207	/// The total length of the highlighted ranges (in bytes, not characters or terminal cells).
208	total_highlighted: usize,
209
210	/// The style for non-highlighted words.
211	normal: yansi::Style,
212
213	/// The style for highlighted words.
214	highlight: yansi::Style,
215}
216
217impl Highlighter {
218	/// Create a new highlighter with the given color.
219	fn new(color: yansi::Color) -> Self {
220		let normal = yansi::Style::new().fg(color);
221		let highlight = yansi::Style::new().fg(yansi::Color::Black).bg(color).bold();
222		Self {
223			ranges: Vec::new(),
224			total_highlighted: 0,
225			normal,
226			highlight,
227		}
228	}
229
230	/// Push a range to the end of the highlighter.
231	fn push(&mut self, len: usize, highlight: bool) {
232		if highlight {
233			self.total_highlighted += len;
234		}
235		if let Some(last) = self.ranges.last_mut() {
236			if last.0 == highlight {
237				last.1.end += len;
238			} else {
239				let start = last.1.end;
240				self.ranges.push((highlight, start..start + len));
241			}
242		} else {
243			self.ranges.push((highlight, 0..len))
244		}
245	}
246
247	/// Write the data using the highlight ranges.
248	fn write_highlighted(&self, buffer: &mut String, data: &str) {
249		let not_highlighted = data.len() - self.total_highlighted;
250		if not_highlighted < div_ceil(self.total_highlighted, 2) {
251			write!(buffer, "{}", data.paint(self.normal)).unwrap();
252		} else {
253			for (highlight, range) in self.ranges.iter().cloned() {
254				let piece = if highlight {
255					data[range].paint(self.highlight)
256				} else {
257					data[range].paint(self.normal)
258				};
259				write!(buffer, "{}", piece).unwrap();
260			}
261		}
262	}
263}
264
265fn div_ceil(a: usize, b: usize) -> usize {
266	if b == 0 {
267		a / b
268	} else {
269		let d = a / b;
270		let r = a % b;
271		if r > 0 {
272			d + 1
273		} else {
274			d
275		}
276	}
277}
278
279#[test]
280fn test_div_ceil() {
281	use crate::assert;
282	assert!(div_ceil(0, 2) == 0);
283	assert!(div_ceil(1, 2) == 1);
284	assert!(div_ceil(2, 2) == 1);
285	assert!(div_ceil(3, 2) == 2);
286	assert!(div_ceil(4, 2) == 2);
287
288	assert!(div_ceil(20, 7) == 3);
289	assert!(div_ceil(21, 7) == 3);
290	assert!(div_ceil(22, 7) == 4);
291	assert!(div_ceil(27, 7) == 4);
292	assert!(div_ceil(28, 7) == 4);
293	assert!(div_ceil(29, 7) == 5);
294}