Skip to main content

lattice_diff/
patch.rs

1//! Patch application for round-trip testing.
2//!
3//! Given a `HunkIndex` produced by [`compute_two_way(a, b)`]
4//! and the original ropes `a` + `b`, [`apply_two_way`] applies
5//! the hunks to `a` to produce a result equivalent to `b`.
6//! This is the primitive that drives the property test
7//! `apply(diff(a, b), a) == b` for randomly-generated rope
8//! pairs.
9//!
10//! Not the public surface for editing — production hunk
11//! transfer (`do` / `dp`) lands in D.5 and routes through the
12//! standard edit pipeline rather than this helper.
13
14use ropey::Rope;
15
16use crate::types::HunkIndex;
17
18/// Apply a two-way `HunkIndex` produced by
19/// `compute_two_way(a, b)` to `a`, reading replacement
20/// content from `b`. The result equals `b` (this is the
21/// round-trip invariant the property tests assert).
22///
23/// Walks hunks in reverse so earlier indices stay valid as
24/// we mutate later ones first.
25pub fn apply_two_way(a: &Rope, b: &Rope, hunks: &HunkIndex) -> Rope {
26    let mut result = a.clone();
27    for hunk in hunks.hunks.iter().rev() {
28        let a_range = hunk.ranges[0];
29        let b_range = hunk.ranges[1];
30
31        let a_char_start = line_to_char_clamped(&result, a_range.start);
32        let a_char_end = line_to_char_clamped(&result, a_range.end);
33
34        let b_text = if b_range.is_empty() {
35            String::new()
36        } else {
37            let b_char_start = line_to_char_clamped(b, b_range.start);
38            let b_char_end = line_to_char_clamped(b, b_range.end);
39            b.slice(b_char_start..b_char_end).to_string()
40        };
41
42        if a_char_start < a_char_end {
43            result.remove(a_char_start..a_char_end);
44        }
45        if !b_text.is_empty() {
46            result.insert(a_char_start, &b_text);
47        }
48    }
49    result
50}
51
52/// `Rope::line_to_char` panics if `line > rope.len_lines()`;
53/// clamp to `len_lines()` for safety. Ropey treats
54/// `line == len_lines()` as past-the-end (returns
55/// `len_chars()`), which is what we want for the "end of
56/// range" case.
57fn line_to_char_clamped(rope: &Rope, line: u32) -> usize {
58    let max = rope.len_lines();
59    let clamped = (line as usize).min(max);
60    rope.line_to_char(clamped)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::DiffAlgorithm;
67    use crate::compute::two_way;
68
69    /// Convenience: diff then apply. Reconstructs `b` from
70    /// `a` via the round-trip path.
71    fn round_trip(a: &Rope, b: &Rope, algorithm: DiffAlgorithm) -> Rope {
72        let hunks = two_way(a, b, algorithm);
73        apply_two_way(a, b, &hunks)
74    }
75
76    fn rt_eq(a: &str, b: &str) {
77        for alg in [
78            DiffAlgorithm::Histogram,
79            DiffAlgorithm::Myers,
80            DiffAlgorithm::MyersMinimal,
81        ] {
82            let a_rope = Rope::from(a);
83            let b_rope = Rope::from(b);
84            let result = round_trip(&a_rope, &b_rope, alg);
85            assert_eq!(
86                result.to_string(),
87                b,
88                "round trip failed for algorithm {alg:?}\n  a = {a:?}\n  b = {b:?}"
89            );
90        }
91    }
92
93    #[test]
94    fn round_trip_identical() {
95        rt_eq("alpha\nbeta\ngamma\n", "alpha\nbeta\ngamma\n");
96    }
97
98    #[test]
99    fn round_trip_empty_both() {
100        rt_eq("", "");
101    }
102
103    #[test]
104    fn round_trip_empty_to_content() {
105        rt_eq("", "alpha\nbeta\n");
106    }
107
108    #[test]
109    fn round_trip_content_to_empty() {
110        rt_eq("alpha\nbeta\n", "");
111    }
112
113    #[test]
114    fn round_trip_single_change() {
115        rt_eq("alpha\nbeta\ngamma\n", "alpha\nBETA\ngamma\n");
116    }
117
118    #[test]
119    fn round_trip_insert_middle() {
120        rt_eq("alpha\ngamma\n", "alpha\nbeta\ngamma\n");
121    }
122
123    #[test]
124    fn round_trip_delete_middle() {
125        rt_eq("alpha\nbeta\ngamma\n", "alpha\ngamma\n");
126    }
127
128    #[test]
129    fn round_trip_multiple_hunks() {
130        rt_eq(
131            "alpha\nbeta\ngamma\ndelta\nepsilon\nzeta\n",
132            "alpha\nBETA\ngamma\nDELTA\nepsilon\nZETA\n",
133        );
134    }
135
136    #[test]
137    fn round_trip_block_replacement() {
138        rt_eq(
139            "alpha\nbeta\ngamma\ndelta\n",
140            "alpha\nfoo\nbar\nbaz\nquux\ndelta\n",
141        );
142    }
143
144    #[test]
145    fn round_trip_no_trailing_newline() {
146        rt_eq("alpha\nbeta", "alpha\nBETA");
147    }
148
149    #[test]
150    fn round_trip_mixed_trailing_newline() {
151        rt_eq("alpha\nbeta", "alpha\nbeta\n");
152    }
153}