patcher/differ/
myers.rs

1use crate::Differ;
2use crate::differ::{Change, DiffAlgorithm};
3
4use super::{create_patch, handle_empty_files, process_changes_to_chunks};
5
6/// The Myers differ implementation that uses Myers algorithm for diffing
7///
8/// This implementation uses the Longest Common Subsequence (LCS) approach,
9/// which is a dynamic programming solution that provides the foundation of Myers' algorithm.
10/// While the full Myers O(ND) optimization uses a greedy approach with diagonal paths,
11/// this implementation prioritizes correctness and readability based on LCS.
12pub struct MyersDiffer<'a> {
13    differ: &'a Differ,
14}
15
16impl<'a> MyersDiffer<'a> {
17    /// Create a new MyersDiffer from a base Differ instance
18    pub fn new(differ: &'a Differ) -> Self {
19        Self { differ }
20    }
21
22    /// Implements a diffing algorithm based on Myers' principles (using LCS)
23    /// Finds the shortest edit script (SES) between old_lines and new_lines
24    fn myers_diff(&self, old_lines: &[&str], new_lines: &[&str]) -> Vec<Change> {
25        // Special cases for empty inputs
26        if old_lines.is_empty() && new_lines.is_empty() {
27            return Vec::new();
28        }
29        if old_lines.is_empty() {
30            // All new lines are insertions
31            return vec![Change::Insert(0, new_lines.len())];
32        }
33        if new_lines.is_empty() {
34            // All old lines are deletions
35            return vec![Change::Delete(0, old_lines.len())];
36        }
37        // If files are identical, return no changes
38        if old_lines == new_lines {
39            return Vec::new();
40        }
41
42        // Use Longest Common Subsequence (LCS) table to find the differences.
43        // This is equivalent to finding the shortest edit path in Myers' algorithm,
44        // although the O(ND) version avoids constructing the full table explicitly.
45        let n = old_lines.len();
46        let m = new_lines.len();
47        // `lcs[i][j]` stores the length of the LCS between old_lines[0..i] and new_lines[0..j]
48        let mut lcs = vec![vec![0; m + 1]; n + 1];
49        for i in 1..=n {
50            for j in 1..=m {
51                if old_lines[i - 1] == new_lines[j - 1] {
52                    lcs[i][j] = lcs[i - 1][j - 1] + 1; // Match: extend LCS diagonally
53                } else {
54                    // No match: take max LCS from deletion (up) or insertion (left)
55                    lcs[i][j] = std::cmp::max(lcs[i - 1][j], lcs[i][j - 1]);
56                }
57            }
58        }
59
60        // Backtrack through the LCS table to reconstruct the edit script (Changes)
61        let mut changes = Vec::new();
62        let mut i = n;
63        let mut j = m;
64        while i > 0 || j > 0 {
65            if i > 0 && j > 0 && old_lines[i - 1] == new_lines[j - 1] {
66                // Match found: move diagonally up-left
67                changes.push(Change::Equal(i - 1, j - 1));
68                i -= 1;
69                j -= 1;
70            } else if j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j]) {
71                // Insertion preferred (or only choice): move left
72                changes.push(Change::Insert(j - 1, 1));
73                j -= 1;
74            } else if i > 0 {
75                // Deletion preferred (or only choice): move up
76                changes.push(Change::Delete(i - 1, 1));
77                i -= 1;
78            } else {
79                // Should be unreachable if LCS table and backtracking are correct
80                break;
81            }
82        }
83
84        // Changes were collected in reverse order during backtrack
85        changes.reverse();
86
87        // Merging adjacent operations is not needed here, as process_changes_to_chunks
88        // expects individual changes (including single Change::Equal). The old logic
89        // for merging Delete/Insert/Equal has been removed.
90        changes
91    }
92}
93
94impl DiffAlgorithm for MyersDiffer<'_> {
95    /// Generate a patch between the old and new content using the Myers diffing algorithm (LCS based)
96    fn generate(&self) -> crate::Patch {
97        let old_lines: Vec<&str> = self.differ.old.lines().collect();
98        let new_lines: Vec<&str> = self.differ.new.lines().collect();
99        // Handle special cases for empty files
100        if let Some(patch) = handle_empty_files(&old_lines, &new_lines) {
101            return patch;
102        }
103        // Find the line-level changes using Myers/LCS
104        let changes = self.myers_diff(&old_lines, &new_lines);
105        // Process the changes into chunks with context
106        let chunks =
107            process_changes_to_chunks(&changes, &old_lines, &new_lines, self.differ.context_lines);
108        // Create the final patch
109        create_patch(chunks)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::{PatchAlgorithm, Patcher, test_utils::load_fixture};
117
118    #[test]
119    fn test_simple_myers_diff() {
120        let old = "line1\nline2\nline3";
121        let new = "line1\nline2\nline3";
122        let differ = Differ::new(old, new);
123        let myers = MyersDiffer::new(&differ);
124        let patch = myers.generate();
125        assert!(
126            patch.chunks.is_empty(),
127            "Patch should be empty for identical files"
128        );
129    }
130
131    #[test]
132    fn test_myers_add_line() {
133        let old = "line1\nline2\nline3";
134        let new = "line1\nline2\nline3\nline4";
135        let differ = Differ::new(old, new);
136        let myers = MyersDiffer::new(&differ);
137        let patch = myers.generate();
138        assert_eq!(patch.chunks.len(), 1);
139        let result = Patcher::new(patch).apply(old, false).unwrap();
140        assert_eq!(result, new);
141    }
142
143    #[test]
144    fn test_myers_remove_line() {
145        let old = "line1\nline2\nline3";
146        let new = "line1\nline3";
147        let differ = Differ::new(old, new);
148        let myers = MyersDiffer::new(&differ);
149        let patch = myers.generate();
150        assert_eq!(patch.chunks.len(), 1);
151        let result = Patcher::new(patch).apply(old, false).unwrap();
152        assert_eq!(result, new);
153    }
154
155    #[test]
156    fn test_myers_empty_files() {
157        // Empty to non-empty
158        let old = "";
159        let new = "line1\nline2";
160        let differ = Differ::new(old, new);
161        let myers = MyersDiffer::new(&differ);
162        let patch = myers.generate();
163        assert_eq!(patch.chunks.len(), 1);
164        let result = Patcher::new(patch.clone()).apply(old, false).unwrap();
165        assert_eq!(result, new);
166
167        // Non-empty to empty
168        let old = "line1\nline2";
169        let new = "";
170        let differ = Differ::new(old, new);
171        let myers = MyersDiffer::new(&differ);
172        let patch = myers.generate();
173        assert_eq!(patch.chunks.len(), 1);
174        let result = Patcher::new(patch).apply(old, false).unwrap();
175        assert_eq!(result, new);
176    }
177
178    #[test]
179    fn test_myers_multiple_changes() {
180        let old = "line1\nline2\nline3\nline4\nline5";
181        let new = "line1\nmodified line\nline3\nline4\nnew line";
182        let differ = Differ::new(old, new);
183        let myers = MyersDiffer::new(&differ);
184        let patch = myers.generate();
185        let result = Patcher::new(patch).apply(old, false).unwrap();
186        assert_eq!(result, new);
187    }
188
189    #[test]
190    fn test_myers_complex_diff() {
191        let old = "A\nB\nC\nA\nB\nB\nA";
192        let new = "C\nB\nA\nB\nA\nC";
193        let differ = Differ::new(old, new);
194        let myers = MyersDiffer::new(&differ);
195        let patch = myers.generate();
196        let result = Patcher::new(patch).apply(old, false).unwrap();
197        assert_eq!(result, new);
198    }
199
200    // New tests using fixtures
201    #[test]
202    fn test_myers_fixture_simple() {
203        let old = load_fixture("simple_before.rs");
204        let new = load_fixture("simple_after.rs");
205        let differ = Differ::new(&old, &new); // Myers is default
206        let myers = MyersDiffer::new(&differ);
207        let patch = myers.generate();
208        let result = Patcher::new(patch).apply(&old, false).unwrap();
209        assert_eq!(result, new);
210    }
211
212    #[test]
213    fn test_myers_fixture_complex() {
214        let old = load_fixture("complex_before.rs");
215        let new = load_fixture("complex_after.rs");
216        let differ = Differ::new(&old, &new); // Myers is default
217        let myers = MyersDiffer::new(&differ);
218        let patch = myers.generate();
219        let result = Patcher::new(patch).apply(&old, false).unwrap();
220        assert_eq!(result, new);
221    }
222}