watchdiff_tui/diff/
algorithms.rs

1use similar::{TextDiff, ChangeTag, Algorithm};
2use clap::ValueEnum;
3
4/// Trait defining a diff algorithm interface
5pub trait DiffAlgorithm: Send + Sync {
6    /// Generate a diff between old and new content
7    fn diff(&self, old: &str, new: &str) -> DiffResult;
8    
9    /// Get the algorithm name
10    fn name(&self) -> &'static str;
11    
12    /// Get algorithm description
13    fn description(&self) -> &'static str;
14}
15
16/// Result of a diff operation
17#[derive(Debug, Clone)]
18pub struct DiffResult {
19    pub hunks: Vec<DiffHunk>,
20    pub stats: DiffStats,
21}
22
23/// A single hunk (contiguous block of changes)
24#[derive(Debug, Clone)]
25pub struct DiffHunk {
26    pub old_start: usize,
27    pub old_len: usize,
28    pub new_start: usize,
29    pub new_len: usize,
30    pub operations: Vec<DiffOperation>,
31}
32
33/// Individual diff operation
34#[derive(Debug, Clone)]
35pub enum DiffOperation {
36    Equal(String),
37    Insert(String),
38    Delete(String),
39}
40
41/// Statistics about the diff
42#[derive(Debug, Clone, Default)]
43pub struct DiffStats {
44    pub lines_added: usize,
45    pub lines_removed: usize,
46    pub lines_modified: usize,
47    pub hunks: usize,
48}
49
50impl DiffStats {
51    pub fn total_changes(&self) -> usize {
52        self.lines_added + self.lines_removed
53    }
54    
55    pub fn net_change(&self) -> isize {
56        self.lines_added as isize - self.lines_removed as isize
57    }
58}
59
60/// Myers diff algorithm implementation
61pub struct MyersAlgorithm;
62
63impl DiffAlgorithm for MyersAlgorithm {
64    fn diff(&self, old: &str, new: &str) -> DiffResult {
65        let diff = TextDiff::configure()
66            .algorithm(Algorithm::Myers)
67            .diff_lines(old, new);
68        
69        self.convert_to_result(&diff)
70    }
71    
72    fn name(&self) -> &'static str {
73        "Myers"
74    }
75    
76    fn description(&self) -> &'static str {
77        "Myers' O(ND) diff algorithm - fast and widely used"
78    }
79}
80
81/// Patience diff algorithm implementation  
82pub struct PatienceAlgorithm;
83
84impl DiffAlgorithm for PatienceAlgorithm {
85    fn diff(&self, old: &str, new: &str) -> DiffResult {
86        let diff = TextDiff::configure()
87            .algorithm(Algorithm::Patience)
88            .diff_lines(old, new);
89            
90        self.convert_to_result(&diff)
91    }
92    
93    fn name(&self) -> &'static str {
94        "Patience"
95    }
96    
97    fn description(&self) -> &'static str {
98        "Patience diff - better for refactored code with moved blocks"
99    }
100}
101
102/// LCS (Longest Common Subsequence) diff algorithm
103pub struct LcsAlgorithm;
104
105impl DiffAlgorithm for LcsAlgorithm {
106    fn diff(&self, old: &str, new: &str) -> DiffResult {
107        let diff = TextDiff::configure()
108            .algorithm(Algorithm::Lcs)
109            .diff_lines(old, new);
110            
111        self.convert_to_result(&diff)
112    }
113    
114    fn name(&self) -> &'static str {
115        "LCS"  
116    }
117    
118    fn description(&self) -> &'static str {
119        "Longest Common Subsequence - produces minimal diffs"
120    }
121}
122
123// Shared implementation for converting similar::TextDiff to our DiffResult
124trait DiffConverter {
125    fn convert_to_result(&self, diff: &TextDiff<str>) -> DiffResult {
126        let mut hunks = Vec::new();
127        let mut stats = DiffStats::default();
128        
129        for (_idx, group) in diff.grouped_ops(3).iter().enumerate() {
130            let mut operations = Vec::new();
131            
132            let old_start = group[0].old_range().start;
133            let new_start = group[0].new_range().start;
134            let old_len = group.iter().map(|op| op.old_range().len()).sum();
135            let new_len = group.iter().map(|op| op.new_range().len()).sum();
136            
137            for op in group {
138                for change in diff.iter_changes(op) {
139                    let content = change.value().to_string();
140                    
141                    match change.tag() {
142                        ChangeTag::Equal => {
143                            operations.push(DiffOperation::Equal(content));
144                        }
145                        ChangeTag::Insert => {
146                            operations.push(DiffOperation::Insert(content));
147                            stats.lines_added += 1;
148                        }
149                        ChangeTag::Delete => {
150                            operations.push(DiffOperation::Delete(content));
151                            stats.lines_removed += 1;
152                        }
153                    }
154                }
155            }
156            
157            hunks.push(DiffHunk {
158                old_start,
159                old_len, 
160                new_start,
161                new_len,
162                operations,
163            });
164        }
165        
166        stats.hunks = hunks.len();
167        stats.lines_modified = stats.lines_added.min(stats.lines_removed);
168        
169        DiffResult { hunks, stats }
170    }
171}
172
173impl DiffConverter for MyersAlgorithm {}
174impl DiffConverter for PatienceAlgorithm {}
175impl DiffConverter for LcsAlgorithm {}
176
177/// Available diff algorithms
178#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
179pub enum DiffAlgorithmType {
180    Myers,
181    Patience, 
182    Lcs,
183}
184
185impl DiffAlgorithmType {
186    pub fn all() -> &'static [DiffAlgorithmType] {
187        &[Self::Myers, Self::Patience, Self::Lcs]
188    }
189    
190    pub fn create(&self) -> Box<dyn DiffAlgorithm> {
191        match self {
192            Self::Myers => Box::new(MyersAlgorithm),
193            Self::Patience => Box::new(PatienceAlgorithm),
194            Self::Lcs => Box::new(LcsAlgorithm),
195        }
196    }
197    
198    pub fn name(&self) -> &'static str {
199        match self {
200            Self::Myers => "Myers",
201            Self::Patience => "Patience",
202            Self::Lcs => "LCS",
203        }
204    }
205}
206
207impl std::fmt::Display for DiffAlgorithmType {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        write!(f, "{}", self.name())
210    }
211}
212
213impl Default for DiffAlgorithmType {
214    fn default() -> Self {
215        Self::Myers
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_myers_diff() {
225        let myers = MyersAlgorithm;
226        let old = "line1\nline2\nline3";
227        let new = "line1\nmodified\nline3";
228        
229        let result = myers.diff(old, new);
230        
231        assert_eq!(result.stats.lines_added, 1);
232        assert_eq!(result.stats.lines_removed, 1);
233        assert!(!result.hunks.is_empty());
234    }
235    
236    #[test]
237    fn test_patience_diff() {
238        let patience = PatienceAlgorithm;
239        let old = "a\nb\nc\nd";
240        let new = "a\nc\nb\nd";
241        
242        let result = patience.diff(old, new);
243        assert!(!result.hunks.is_empty());
244    }
245    
246    #[test]
247    fn test_diff_stats() {
248        let stats = DiffStats {
249            lines_added: 5,
250            lines_removed: 3,
251            lines_modified: 0,
252            hunks: 2,
253        };
254        
255        assert_eq!(stats.total_changes(), 8);
256        assert_eq!(stats.net_change(), 2);
257    }
258}