watchdiff_tui/diff/
formatter.rs

1use std::path::Path;
2use super::algorithms::{DiffResult, DiffOperation};
3
4/// Different output formats for diffs
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DiffFormat {
7    Unified,
8    SideBySide,
9    Context,
10    GitPatch,
11}
12
13/// Formats diff results into various text representations
14pub struct DiffFormatter;
15
16impl DiffFormatter {
17    /// Format a diff result as unified diff
18    pub fn format_unified<P: AsRef<Path>>(result: &DiffResult, old_path: P, new_path: P) -> String {
19        let old_path = old_path.as_ref();
20        let new_path = new_path.as_ref();
21        
22        let mut output = Vec::new();
23        output.push(format!("--- {}", old_path.display()));
24        output.push(format!("+++ {}", new_path.display()));
25        
26        for hunk in &result.hunks {
27            // Add hunk header
28            output.push(format!(
29                "@@ -{},{} +{},{} @@",
30                hunk.old_start + 1,
31                hunk.old_len,
32                hunk.new_start + 1,
33                hunk.new_len
34            ));
35            
36            // Add operations
37            for op in &hunk.operations {
38                match op {
39                    DiffOperation::Equal(line) => {
40                        output.push(format!(" {}", line.trim_end()));
41                    }
42                    DiffOperation::Insert(line) => {
43                        output.push(format!("+{}", line.trim_end()));
44                    }
45                    DiffOperation::Delete(line) => {
46                        output.push(format!("-{}", line.trim_end()));
47                    }
48                }
49            }
50        }
51        
52        output.join("\n")
53    }
54    
55    /// Format a diff result as side-by-side comparison
56    pub fn format_side_by_side<P: AsRef<Path>>(
57        result: &DiffResult, 
58        old_path: P, 
59        new_path: P, 
60        width: usize
61    ) -> String {
62        let old_path = old_path.as_ref();
63        let new_path = new_path.as_ref();
64        
65        let mut output = Vec::new();
66        let half_width = (width - 3) / 2; // Account for separator " | "
67        
68        output.push(format!(
69            "{:<width$} | {}", 
70            format!("--- {}", old_path.display()), 
71            format!("+++ {}", new_path.display()),
72            width = half_width
73        ));
74        output.push("-".repeat(width));
75        
76        for hunk in &result.hunks {
77            for op in &hunk.operations {
78                match op {
79                    DiffOperation::Equal(line) => {
80                        let content = format!("  {}", line.trim_end());
81                        let truncated = Self::truncate_line(&content, half_width);
82                        output.push(format!("{:<width$} | {}", truncated, truncated, width = half_width));
83                    }
84                    DiffOperation::Delete(line) => {
85                        let content = format!("- {}", line.trim_end());
86                        let truncated = Self::truncate_line(&content, half_width);
87                        output.push(format!("{:<width$} | {}", truncated, " ".repeat(half_width), width = half_width));
88                    }
89                    DiffOperation::Insert(line) => {
90                        let content = format!("+ {}", line.trim_end());
91                        let truncated = Self::truncate_line(&content, half_width);
92                        output.push(format!("{:<width$} | {}", " ".repeat(half_width), truncated, width = half_width));
93                    }
94                }
95            }
96        }
97        
98        output.join("\n")
99    }
100    
101    /// Format as Git patch format
102    pub fn format_git_patch<P: AsRef<Path>>(result: &DiffResult, old_path: P, new_path: P) -> String {
103        let old_path = old_path.as_ref();
104        let new_path = new_path.as_ref();
105        
106        let mut output = Vec::new();
107        
108        // Git patch header
109        output.push(format!("diff --git a/{} b/{}", old_path.display(), new_path.display()));
110        output.push(format!("index 0000000..1111111 100644")); // Placeholder hashes
111        
112        // Standard unified diff content
113        output.push(Self::format_unified(result, old_path, new_path));
114        
115        output.join("\n")
116    }
117    
118    /// Format diff statistics as a summary
119    pub fn format_stats(result: &DiffResult) -> String {
120        let stats = &result.stats;
121        
122        if stats.total_changes() == 0 {
123            return "No changes".to_string();
124        }
125        
126        let mut parts = Vec::new();
127        
128        if stats.lines_added > 0 {
129            parts.push(format!("{} insertion{}", 
130                stats.lines_added,
131                if stats.lines_added == 1 { "" } else { "s" }
132            ));
133        }
134        
135        if stats.lines_removed > 0 {
136            parts.push(format!("{} deletion{}", 
137                stats.lines_removed,
138                if stats.lines_removed == 1 { "" } else { "s" }
139            ));
140        }
141        
142        if stats.hunks > 0 {
143            parts.push(format!("{} hunk{}", 
144                stats.hunks,
145                if stats.hunks == 1 { "" } else { "s" }
146            ));
147        }
148        
149        parts.join(", ")
150    }
151    
152    /// Format with the specified format type
153    pub fn format<P: AsRef<Path>>(
154        result: &DiffResult,
155        format: DiffFormat, 
156        old_path: P,
157        new_path: P,
158        width: Option<usize>
159    ) -> String {
160        match format {
161            DiffFormat::Unified => Self::format_unified(result, old_path, new_path),
162            DiffFormat::SideBySide => {
163                let w = width.unwrap_or(80);
164                Self::format_side_by_side(result, old_path, new_path, w)
165            }
166            DiffFormat::GitPatch => Self::format_git_patch(result, old_path, new_path),
167            DiffFormat::Context => Self::format_unified(result, old_path, new_path), // Same as unified for now
168        }
169    }
170    
171    fn truncate_line(line: &str, max_width: usize) -> String {
172        if line.len() > max_width {
173            if max_width > 3 {
174                format!("{}...", &line[..max_width - 3])
175            } else {
176                line[..max_width].to_string()
177            }
178        } else {
179            line.to_string()
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::diff::algorithms::{MyersAlgorithm, DiffAlgorithm};
188
189    fn create_test_diff() -> DiffResult {
190        let myers = MyersAlgorithm;
191        myers.diff("line1\nline2\nline3", "line1\nmodified\nline3")
192    }
193
194    #[test]
195    fn test_format_unified() {
196        let result = create_test_diff();
197        let formatted = DiffFormatter::format_unified(&result, "old.txt", "new.txt");
198        
199        assert!(formatted.contains("--- old.txt"));
200        assert!(formatted.contains("+++ new.txt"));
201        assert!(formatted.contains("-line2"));
202        assert!(formatted.contains("+modified"));
203    }
204
205    #[test]
206    fn test_format_stats() {
207        let result = create_test_diff();
208        let stats = DiffFormatter::format_stats(&result);
209        
210        assert!(stats.contains("1 insertion"));
211        assert!(stats.contains("1 deletion"));
212    }
213
214    #[test]
215    fn test_format_git_patch() {
216        let result = create_test_diff();
217        let formatted = DiffFormatter::format_git_patch(&result, "file.txt", "file.txt");
218        
219        assert!(formatted.contains("diff --git"));
220        assert!(formatted.contains("index 0000000..1111111"));
221    }
222}