watchdiff_tui/diff/
algorithms.rs1use similar::{TextDiff, ChangeTag, Algorithm};
2use clap::ValueEnum;
3
4pub trait DiffAlgorithm: Send + Sync {
6 fn diff(&self, old: &str, new: &str) -> DiffResult;
8
9 fn name(&self) -> &'static str;
11
12 fn description(&self) -> &'static str;
14}
15
16#[derive(Debug, Clone)]
18pub struct DiffResult {
19 pub hunks: Vec<DiffHunk>,
20 pub stats: DiffStats,
21}
22
23#[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#[derive(Debug, Clone)]
35pub enum DiffOperation {
36 Equal(String),
37 Insert(String),
38 Delete(String),
39}
40
41#[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
60pub 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
81pub 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
102pub 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
123trait 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#[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}