Skip to main content

ochrepad_core/
diff.rs

1//! Line and word diff for the compare view (PLAN M12), using `similar`.
2//!
3//! The result is a list of aligned rows: each row shows a line of the left
4//! document, a line of the right one, or both. A missing side is a filler
5//! row, so equal text stays side by side. Each side of a compare view is an
6//! editor view whose [`Padding`] (from [`LineDiff::padding`]) puts those
7//! blank rows into its display rows.
8
9use std::ops::Range;
10use std::time::Duration;
11
12use similar::{Algorithm, ChangeTag, DiffOp, TextDiff};
13
14use crate::display::Padding;
15use crate::text::Document;
16
17/// Give up refining a diff after this long and report a coarser one.
18pub const DIFF_TIMEOUT: Duration = Duration::from_secs(2);
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum RowKind {
22    Equal,
23    /// Only on the right.
24    Added,
25    /// Only on the left.
26    Removed,
27    /// On both sides, with different text.
28    Changed,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct DiffRow {
33    pub kind: RowKind,
34    pub left: Option<usize>,
35    pub right: Option<usize>,
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub struct DiffStats {
40    pub added: usize,
41    pub removed: usize,
42    pub changed: usize,
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Eq)]
46pub struct LineDiff {
47    pub rows: Vec<DiffRow>,
48    /// Row ranges of consecutive non-equal rows, for next/previous change.
49    pub hunks: Vec<Range<usize>>,
50    pub stats: DiffStats,
51}
52
53fn lines(doc: &Document) -> Vec<String> {
54    (0..doc.line_count()).map(|l| doc.line_text(l)).collect()
55}
56
57/// Aligns the lines of `left` and `right`. Replaced blocks are paired line
58/// by line as `Changed`; the extra lines of the longer side are `Removed` or
59/// `Added`.
60pub fn diff_lines(left: &Document, right: &Document) -> LineDiff {
61    let a = lines(left);
62    let b = lines(right);
63    let a_refs: Vec<&str> = a.iter().map(String::as_str).collect();
64    let b_refs: Vec<&str> = b.iter().map(String::as_str).collect();
65    let diff = TextDiff::configure()
66        .algorithm(Algorithm::Myers)
67        .timeout(DIFF_TIMEOUT)
68        .diff_slices(&a_refs, &b_refs);
69    let mut out = LineDiff::default();
70    let push = |out: &mut LineDiff, kind, left, right| {
71        if kind != RowKind::Equal {
72            let row = out.rows.len();
73            match out.hunks.last_mut() {
74                Some(h) if h.end == row => h.end += 1,
75                _ => out.hunks.push(row..row + 1),
76            }
77            match kind {
78                RowKind::Added => out.stats.added += 1,
79                RowKind::Removed => out.stats.removed += 1,
80                RowKind::Changed => out.stats.changed += 1,
81                RowKind::Equal => {}
82            }
83        }
84        out.rows.push(DiffRow { kind, left, right });
85    };
86    for op in diff.ops() {
87        match *op {
88            DiffOp::Equal {
89                old_index,
90                new_index,
91                len,
92            } => {
93                for i in 0..len {
94                    push(
95                        &mut out,
96                        RowKind::Equal,
97                        Some(old_index + i),
98                        Some(new_index + i),
99                    );
100                }
101            }
102            DiffOp::Delete {
103                old_index, old_len, ..
104            } => {
105                for i in 0..old_len {
106                    push(&mut out, RowKind::Removed, Some(old_index + i), None);
107                }
108            }
109            DiffOp::Insert {
110                new_index, new_len, ..
111            } => {
112                for i in 0..new_len {
113                    push(&mut out, RowKind::Added, None, Some(new_index + i));
114                }
115            }
116            DiffOp::Replace {
117                old_index,
118                old_len,
119                new_index,
120                new_len,
121            } => {
122                for i in 0..old_len.max(new_len) {
123                    let l = (i < old_len).then_some(old_index + i);
124                    let r = (i < new_len).then_some(new_index + i);
125                    let kind = match (l, r) {
126                        (Some(_), Some(_)) => RowKind::Changed,
127                        (Some(_), None) => RowKind::Removed,
128                        _ => RowKind::Added,
129                    };
130                    push(&mut out, kind, l, r);
131                }
132            }
133        }
134    }
135    out
136}
137
138/// Byte ranges of the words that differ between two changed lines, on each
139/// side. Adjacent ranges are merged.
140pub fn diff_words(left: &str, right: &str) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
141    let diff = TextDiff::configure()
142        .timeout(DIFF_TIMEOUT)
143        .diff_words(left, right);
144    let (mut a, mut b) = (Vec::new(), Vec::new());
145    let (mut ia, mut ib) = (0, 0);
146    let add = |ranges: &mut Vec<Range<usize>>, start: usize, len: usize| match ranges.last_mut() {
147        Some(last) if last.end == start => last.end += len,
148        _ => ranges.push(start..start + len),
149    };
150    for change in diff.iter_all_changes() {
151        let len = change.value().len();
152        match change.tag() {
153            ChangeTag::Equal => {
154                ia += len;
155                ib += len;
156            }
157            ChangeTag::Delete => {
158                add(&mut a, ia, len);
159                ia += len;
160            }
161            ChangeTag::Insert => {
162                add(&mut b, ib, len);
163                ib += len;
164            }
165        }
166    }
167    (a, b)
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum Side {
172    Left,
173    Right,
174}
175
176impl DiffRow {
177    /// The line of `side` on this row, or `None` for a blank row there.
178    pub fn line(&self, side: Side) -> Option<usize> {
179        match side {
180            Side::Left => self.left,
181            Side::Right => self.right,
182        }
183    }
184}
185
186impl LineDiff {
187    /// The row each line of `side` is on.
188    pub fn line_rows(&self, side: Side) -> Vec<usize> {
189        self.rows
190            .iter()
191            .enumerate()
192            .filter(|(_, r)| r.line(side).is_some())
193            .map(|(i, _)| i)
194            .collect()
195    }
196
197    /// The blank rows that line `side` up with the rows: one opposite each
198    /// line only the other side has.
199    pub fn padding(&self, side: Side) -> Padding {
200        let mut p = Padding::default();
201        let mut blank = 0;
202        for r in &self.rows {
203            if r.line(side).is_some() {
204                p.before.push(blank);
205                blank = 0;
206            } else {
207                blank += 1;
208            }
209        }
210        p.after_last = blank;
211        p
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn kinds(d: &LineDiff) -> Vec<(RowKind, Option<usize>, Option<usize>)> {
220        d.rows.iter().map(|r| (r.kind, r.left, r.right)).collect()
221    }
222
223    #[test]
224    fn rows_align_with_fillers() {
225        let left = Document::new("a\nb\nc\nd");
226        let right = Document::new("a\nB\nc\nnew\nd");
227        let d = diff_lines(&left, &right);
228        use RowKind::*;
229        assert_eq!(
230            kinds(&d),
231            vec![
232                (Equal, Some(0), Some(0)),
233                (Changed, Some(1), Some(1)),
234                (Equal, Some(2), Some(2)),
235                (Added, None, Some(3)),
236                (Equal, Some(3), Some(4)),
237            ]
238        );
239        assert_eq!(d.hunks, vec![1..2, 3..4]);
240        assert_eq!(
241            d.stats,
242            DiffStats {
243                added: 1,
244                removed: 0,
245                changed: 1
246            }
247        );
248    }
249
250    #[test]
251    fn uneven_replacements_pair_then_spill() {
252        let left = Document::new("x\none\ntwo\nthree\ny");
253        let right = Document::new("x\nuno\ny");
254        let d = diff_lines(&left, &right);
255        use RowKind::*;
256        assert_eq!(
257            kinds(&d),
258            vec![
259                (Equal, Some(0), Some(0)),
260                (Changed, Some(1), Some(1)),
261                (Removed, Some(2), None),
262                (Removed, Some(3), None),
263                (Equal, Some(4), Some(2)),
264            ]
265        );
266        assert_eq!(d.hunks, vec![1..4]);
267    }
268
269    #[test]
270    fn identical_documents_have_no_hunks() {
271        let doc = Document::new("same\ntext\n");
272        let d = diff_lines(&doc, &doc.clone());
273        assert!(d.hunks.is_empty());
274        assert_eq!(d.rows.len(), 3);
275    }
276
277    #[test]
278    fn word_ranges() {
279        let (a, b) = diff_words("let total = price * qty;", "let total = cost * qty + tax;");
280        let left = "let total = price * qty;";
281        let right = "let total = cost * qty + tax;";
282        let words = |s: &str, r: &[Range<usize>]| -> Vec<String> {
283            r.iter().map(|r| s[r.clone()].to_owned()).collect()
284        };
285        assert_eq!(words(left, &a), vec!["price", "qty;"]);
286        assert_eq!(words(right, &b), vec!["cost", "qty + tax;"]);
287    }
288
289    #[test]
290    fn padding_lines_up_both_sides() {
291        let d = diff_lines(
292            &Document::new(
293                "a
294b",
295            ),
296            &Document::new(
297                "new
298a
299new
300b
301end",
302            ),
303        );
304        let left = d.padding(Side::Left);
305        assert_eq!(left.before, vec![1, 1]);
306        assert_eq!(left.after_last, 1);
307        assert_eq!(
308            d.padding(Side::Right),
309            Padding {
310                before: vec![0; 5],
311                after_last: 0
312            }
313        );
314        assert_eq!(d.line_rows(Side::Left), vec![1, 3]);
315        assert_eq!(d.line_rows(Side::Right), vec![0, 1, 2, 3, 4]);
316    }
317}