assert2/__assert2_impl/print/
diff.rs1use std::fmt::Write;
2use yansi::Paint;
3
4pub struct MultiLineDiff<'a> {
6 line_diffs: Vec<LineDiff<'a>>,
8}
9
10impl<'a> MultiLineDiff<'a> {
11 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 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 buffer.pop();
44 }
45}
46
47enum LineDiff<'a> {
48 LeftOnly(&'a str),
50 RightOnly(&'a str),
52 Different(&'a str, &'a str),
54 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 Self::LeftOnly(old_l) if seen_left == 1 => {
75 *last = Self::Different(old_l, r);
76 seen_left = 0;
77 continue;
78 },
79 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 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
109pub struct SingleLineDiff<'a> {
111 left: &'a str,
113
114 right: &'a str,
116
117 left_highlights: Highlighter,
119
120 right_highlights: Highlighter,
122}
123
124impl<'a> SingleLineDiff<'a> {
125 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 pub fn write_left(&self, buffer: &mut String) {
160 self.left_highlights.write_highlighted(buffer, self.left);
161 }
162
163 pub fn write_right(&self, buffer: &mut String) {
167 self.right_highlights.write_highlighted(buffer, self.right);
168 }
169
170 fn split_words(mut input: &str) -> Vec<&str> {
172 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
199struct Highlighter {
201 ranges: Vec<(bool, std::ops::Range<usize>)>,
206
207 total_highlighted: usize,
209
210 normal: yansi::Style,
212
213 highlight: yansi::Style,
215}
216
217impl Highlighter {
218 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 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 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}