watchdiff_tui/diff/
formatter.rs1use std::path::Path;
2use super::algorithms::{DiffResult, DiffOperation};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DiffFormat {
7 Unified,
8 SideBySide,
9 Context,
10 GitPatch,
11}
12
13pub struct DiffFormatter;
15
16impl DiffFormatter {
17 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 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 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 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; 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 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 output.push(format!("diff --git a/{} b/{}", old_path.display(), new_path.display()));
110 output.push(format!("index 0000000..1111111 100644")); output.push(Self::format_unified(result, old_path, new_path));
114
115 output.join("\n")
116 }
117
118 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 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), }
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}