1use similar::{Change, ChangeTag, TextDiff};
2
3use crate::utils::group_by;
4
5#[derive(Debug, PartialEq, Eq, Clone)]
6pub enum DiffColour {
7 Red,
8 Green,
9 Black,
10}
11
12impl DiffColour {
13 pub fn to_str(&self) -> &str {
14 match self {
15 DiffColour::Red => "red",
16 DiffColour::Green => "green",
17 DiffColour::Black => "black",
18 }
19 }
20}
21
22#[derive(Debug, PartialEq, Eq, Clone)]
23pub struct Diff {
24 pub text: String,
25 pub fg_colour: DiffColour,
26 pub bg_colour: Option<DiffColour>,
27}
28
29pub fn line_diff<'a>(old_line: &'a str, new_line: &'a str) -> (Vec<Diff>, Vec<Diff>) {
30 let diff = TextDiff::configure()
31 .algorithm(similar::Algorithm::Myers)
32 .timeout(std::time::Duration::from_millis(100))
33 .diff_chars(old_line, new_line);
34
35 let mut old_spans = vec![Diff {
36 text: "- ".to_owned(),
37 fg_colour: DiffColour::Red,
38 bg_colour: None,
39 }];
40 let mut new_spans = vec![Diff {
41 text: "+ ".to_owned(),
42 fg_colour: DiffColour::Green,
43 bg_colour: None,
44 }];
45
46 for change_group in group_by(diff.iter_all_changes(), |c1, c2| c1.tag() == c2.tag()) {
47 let first_change = change_group.first().unwrap(); let text = change_group.iter().map(Change::value).collect();
49 match first_change.tag() {
50 ChangeTag::Delete => {
51 old_spans.push(Diff {
52 text,
53 fg_colour: DiffColour::Black,
54 bg_colour: Some(DiffColour::Red),
55 });
56 }
57 ChangeTag::Insert => {
58 new_spans.push(Diff {
59 text,
60 fg_colour: DiffColour::Black,
61 bg_colour: Some(DiffColour::Green),
62 });
63 }
64 ChangeTag::Equal => {
65 old_spans.push(Diff {
66 text: text.clone(),
67 fg_colour: DiffColour::Red,
68 bg_colour: None,
69 });
70 new_spans.push(Diff {
71 text,
72 fg_colour: DiffColour::Green,
73 bg_colour: None,
74 });
75 }
76 }
77 }
78
79 (old_spans, new_spans)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_identical_lines() {
88 let (old_actual, new_actual) = line_diff("hello", "hello");
89
90 let old_expected = vec![
91 Diff {
92 text: "- ".to_owned(),
93 fg_colour: DiffColour::Red,
94 bg_colour: None,
95 },
96 Diff {
97 text: "hello".to_owned(),
98 fg_colour: DiffColour::Red,
99 bg_colour: None,
100 },
101 ];
102
103 let new_expected = vec![
104 Diff {
105 text: "+ ".to_owned(),
106 fg_colour: DiffColour::Green,
107 bg_colour: None,
108 },
109 Diff {
110 text: "hello".to_owned(),
111 fg_colour: DiffColour::Green,
112 bg_colour: None,
113 },
114 ];
115
116 assert_eq!(old_expected, old_actual);
117 assert_eq!(new_expected, new_actual);
118 }
119
120 #[test]
121 fn test_single_char_difference() {
122 let (old_actual, new_actual) = line_diff("hello", "hallo");
123
124 let old_expected = vec![
125 Diff {
126 text: "- ".to_owned(),
127 fg_colour: DiffColour::Red,
128 bg_colour: None,
129 },
130 Diff {
131 text: "h".to_owned(),
132 fg_colour: DiffColour::Red,
133 bg_colour: None,
134 },
135 Diff {
136 text: "e".to_owned(),
137 fg_colour: DiffColour::Black,
138 bg_colour: Some(DiffColour::Red),
139 },
140 Diff {
141 text: "llo".to_owned(),
142 fg_colour: DiffColour::Red,
143 bg_colour: None,
144 },
145 ];
146
147 let new_expected = vec![
148 Diff {
149 text: "+ ".to_owned(),
150 fg_colour: DiffColour::Green,
151 bg_colour: None,
152 },
153 Diff {
154 text: "h".to_owned(),
155 fg_colour: DiffColour::Green,
156 bg_colour: None,
157 },
158 Diff {
159 text: "a".to_owned(),
160 fg_colour: DiffColour::Black,
161 bg_colour: Some(DiffColour::Green),
162 },
163 Diff {
164 text: "llo".to_owned(),
165 fg_colour: DiffColour::Green,
166 bg_colour: None,
167 },
168 ];
169
170 assert_eq!(old_expected, old_actual);
171 assert_eq!(new_expected, new_actual);
172 }
173
174 #[test]
175 fn test_completely_different_strings() {
176 let (old_actual, new_actual) = line_diff("foo", "bar");
177
178 let old_expected = vec![
179 Diff {
180 text: "- ".to_owned(),
181 fg_colour: DiffColour::Red,
182 bg_colour: None,
183 },
184 Diff {
185 text: "foo".to_owned(),
186 fg_colour: DiffColour::Black,
187 bg_colour: Some(DiffColour::Red),
188 },
189 ];
190
191 let new_expected = vec![
192 Diff {
193 text: "+ ".to_owned(),
194 fg_colour: DiffColour::Green,
195 bg_colour: None,
196 },
197 Diff {
198 text: "bar".to_owned(),
199 fg_colour: DiffColour::Black,
200 bg_colour: Some(DiffColour::Green),
201 },
202 ];
203
204 assert_eq!(old_expected, old_actual);
205 assert_eq!(new_expected, new_actual);
206 }
207
208 #[test]
209 fn test_empty_strings() {
210 let (old_actual, new_actual) = line_diff("", "");
211
212 let old_expected = vec![Diff {
213 text: "- ".to_owned(),
214 fg_colour: DiffColour::Red,
215 bg_colour: None,
216 }];
217
218 let new_expected = vec![Diff {
219 text: "+ ".to_owned(),
220 fg_colour: DiffColour::Green,
221 bg_colour: None,
222 }];
223
224 assert_eq!(old_expected, old_actual);
225 assert_eq!(new_expected, new_actual);
226 }
227
228 #[test]
229 fn test_addition_at_end() {
230 let (old_actual, new_actual) = line_diff("hello", "hello!");
231
232 let old_expected = vec![
233 Diff {
234 text: "- ".to_owned(),
235 fg_colour: DiffColour::Red,
236 bg_colour: None,
237 },
238 Diff {
239 text: "hello".to_owned(),
240 fg_colour: DiffColour::Red,
241 bg_colour: None,
242 },
243 ];
244
245 let new_expected = vec![
246 Diff {
247 text: "+ ".to_owned(),
248 fg_colour: DiffColour::Green,
249 bg_colour: None,
250 },
251 Diff {
252 text: "hello".to_owned(),
253 fg_colour: DiffColour::Green,
254 bg_colour: None,
255 },
256 Diff {
257 text: "!".to_owned(),
258 fg_colour: DiffColour::Black,
259 bg_colour: Some(DiffColour::Green),
260 },
261 ];
262
263 assert_eq!(old_expected, old_actual);
264 assert_eq!(new_expected, new_actual);
265 }
266
267 #[test]
268 fn test_addition_at_start() {
269 let (old_actual, new_actual) = line_diff("hello", "!hello");
270
271 let old_expected = vec![
272 Diff {
273 text: "- ".to_owned(),
274 fg_colour: DiffColour::Red,
275 bg_colour: None,
276 },
277 Diff {
278 text: "hello".to_owned(),
279 fg_colour: DiffColour::Red,
280 bg_colour: None,
281 },
282 ];
283
284 let new_expected = vec![
285 Diff {
286 text: "+ ".to_owned(),
287 fg_colour: DiffColour::Green,
288 bg_colour: None,
289 },
290 Diff {
291 text: "!".to_owned(),
292 fg_colour: DiffColour::Black,
293 bg_colour: Some(DiffColour::Green),
294 },
295 Diff {
296 text: "hello".to_owned(),
297 fg_colour: DiffColour::Green,
298 bg_colour: None,
299 },
300 ];
301
302 assert_eq!(old_expected, old_actual);
303 assert_eq!(new_expected, new_actual);
304 }
305}