scooter_core/
diff.rs

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(); // group_by should never return an empty group
48        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}