1use crate::diff::{DiffHunk, DiffLineKind};
4use crate::diff_paths::{
5 format_start_only_hunk_header, is_diff_addition_line, is_diff_deletion_line, parse_hunk_starts,
6};
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
9pub struct DiffChangeCounts {
10 pub additions: usize,
11 pub deletions: usize,
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum DiffDisplayKind {
16 Metadata,
17 HunkHeader,
18 Context,
19 Addition,
20 Deletion,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct DiffDisplayLine {
25 pub kind: DiffDisplayKind,
26 pub line_number: Option<usize>,
27 pub text: String,
28}
29
30impl DiffDisplayLine {
31 pub fn numbered_text(&self, line_number_width: usize) -> String {
32 match self.kind {
33 DiffDisplayKind::Metadata | DiffDisplayKind::HunkHeader => self.text.clone(),
34 DiffDisplayKind::Addition => format!(
35 "+{:>line_number_width$} {}",
36 self.line_number.unwrap_or_default(),
37 self.text
38 ),
39 DiffDisplayKind::Deletion => format!(
40 "-{:>line_number_width$} {}",
41 self.line_number.unwrap_or_default(),
42 self.text
43 ),
44 DiffDisplayKind::Context => format!(
45 " {:>line_number_width$} {}",
46 self.line_number.unwrap_or_default(),
47 self.text
48 ),
49 }
50 }
51}
52
53impl DiffChangeCounts {
54 pub fn total(self) -> usize {
55 self.additions + self.deletions
56 }
57}
58
59pub fn count_diff_changes(hunks: &[DiffHunk]) -> DiffChangeCounts {
60 let mut counts = DiffChangeCounts::default();
61
62 for hunk in hunks {
63 for line in &hunk.lines {
64 match line.kind {
65 DiffLineKind::Addition => counts.additions += 1,
66 DiffLineKind::Deletion => counts.deletions += 1,
67 DiffLineKind::Context => {}
68 }
69 }
70 }
71
72 counts
73}
74
75pub fn display_lines_from_hunks(hunks: &[DiffHunk]) -> Vec<DiffDisplayLine> {
76 let mut lines = Vec::new();
77
78 for hunk in hunks {
79 lines.push(DiffDisplayLine {
80 kind: DiffDisplayKind::HunkHeader,
81 line_number: None,
82 text: format!("@@ -{} +{} @@", hunk.old_start, hunk.new_start),
83 });
84
85 for line in &hunk.lines {
86 lines.push(display_line_from_diff_line(line));
87 }
88 }
89
90 lines
91}
92
93pub fn display_lines_from_unified_diff(diff_content: &str) -> Vec<DiffDisplayLine> {
94 let mut lines = Vec::new();
95 let mut old_line_no = 0usize;
96 let mut new_line_no = 0usize;
97 let mut in_hunk = false;
98
99 for line in diff_content.lines() {
100 if let Some((old_start, new_start)) = parse_hunk_starts(line) {
101 old_line_no = old_start;
102 new_line_no = new_start;
103 in_hunk = true;
104 lines.push(DiffDisplayLine {
105 kind: DiffDisplayKind::HunkHeader,
106 line_number: None,
107 text: format_start_only_hunk_header(line)
108 .unwrap_or_else(|| format!("@@ -{old_start} +{new_start} @@")),
109 });
110 continue;
111 }
112
113 if !in_hunk {
114 lines.push(DiffDisplayLine {
115 kind: DiffDisplayKind::Metadata,
116 line_number: None,
117 text: line.to_string(),
118 });
119 continue;
120 }
121
122 if is_diff_addition_line(line) {
123 lines.push(DiffDisplayLine {
124 kind: DiffDisplayKind::Addition,
125 line_number: Some(new_line_no),
126 text: line[1..].to_string(),
127 });
128 new_line_no = new_line_no.saturating_add(1);
129 continue;
130 }
131
132 if is_diff_deletion_line(line) {
133 lines.push(DiffDisplayLine {
134 kind: DiffDisplayKind::Deletion,
135 line_number: Some(old_line_no),
136 text: line[1..].to_string(),
137 });
138 old_line_no = old_line_no.saturating_add(1);
139 continue;
140 }
141
142 if let Some(context_line) = line.strip_prefix(' ') {
143 lines.push(DiffDisplayLine {
144 kind: DiffDisplayKind::Context,
145 line_number: Some(new_line_no),
146 text: context_line.to_string(),
147 });
148 old_line_no = old_line_no.saturating_add(1);
149 new_line_no = new_line_no.saturating_add(1);
150 continue;
151 }
152
153 lines.push(DiffDisplayLine {
154 kind: DiffDisplayKind::Metadata,
155 line_number: None,
156 text: line.to_string(),
157 });
158 }
159
160 lines
161}
162
163pub fn diff_display_line_number_width(lines: &[DiffDisplayLine]) -> usize {
164 let max_digits = lines
165 .iter()
166 .filter_map(|line| line.line_number.map(|line_no| line_no.to_string().len()))
167 .max()
168 .unwrap_or(4);
169 max_digits.clamp(4, 6)
170}
171
172pub fn format_numbered_unified_diff(diff_content: &str) -> Vec<String> {
173 let display_lines = display_lines_from_unified_diff(diff_content);
174 let width = diff_display_line_number_width(&display_lines);
175 display_lines
176 .into_iter()
177 .map(|line| line.numbered_text(width))
178 .collect()
179}
180
181fn display_line_from_diff_line(line: &crate::diff::DiffLine) -> DiffDisplayLine {
182 let text = line.text.trim_end_matches('\n').to_string();
183 match line.kind {
184 DiffLineKind::Context => DiffDisplayLine {
185 kind: DiffDisplayKind::Context,
186 line_number: line.new_line,
187 text,
188 },
189 DiffLineKind::Addition => DiffDisplayLine {
190 kind: DiffDisplayKind::Addition,
191 line_number: line.new_line,
192 text,
193 },
194 DiffLineKind::Deletion => DiffDisplayLine {
195 kind: DiffDisplayKind::Deletion,
196 line_number: line.old_line,
197 text,
198 },
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::diff::{DiffLine, DiffLineKind};
206
207 #[test]
208 fn counts_diff_changes_from_hunks() {
209 let hunks = vec![DiffHunk {
210 old_start: 1,
211 old_lines: 2,
212 new_start: 1,
213 new_lines: 2,
214 lines: vec![
215 DiffLine {
216 kind: DiffLineKind::Context,
217 old_line: Some(1),
218 new_line: Some(1),
219 text: "same\n".to_string(),
220 },
221 DiffLine {
222 kind: DiffLineKind::Deletion,
223 old_line: Some(2),
224 new_line: None,
225 text: "old\n".to_string(),
226 },
227 DiffLine {
228 kind: DiffLineKind::Addition,
229 old_line: None,
230 new_line: Some(2),
231 text: "new\n".to_string(),
232 },
233 ],
234 }];
235
236 let counts = count_diff_changes(&hunks);
237 assert_eq!(counts.additions, 1);
238 assert_eq!(counts.deletions, 1);
239 assert_eq!(counts.total(), 2);
240 }
241
242 #[test]
243 fn formats_numbered_unified_diff_with_start_only_headers() {
244 let diff = "\
245diff --git a/file.txt b/file.txt
246@@ -10,2 +10,2 @@
247-old
248+new
249 context
250";
251
252 let lines = format_numbered_unified_diff(diff);
253 assert_eq!(lines[0], "diff --git a/file.txt b/file.txt");
254 assert!(lines.iter().any(|line| line == "@@ -10 +10 @@"));
255 assert!(lines.iter().any(|line| line.starts_with("- 10 old")));
256 assert!(lines.iter().any(|line| line.starts_with("+ 10 new")));
257 assert!(lines.iter().any(|line| line.starts_with(" 11 context")));
258 }
259
260 #[test]
261 fn display_lines_from_hunks_preserves_semantics() {
262 let hunks = vec![DiffHunk {
263 old_start: 10,
264 old_lines: 2,
265 new_start: 10,
266 new_lines: 2,
267 lines: vec![
268 DiffLine {
269 kind: DiffLineKind::Deletion,
270 old_line: Some(10),
271 new_line: None,
272 text: "old\n".to_string(),
273 },
274 DiffLine {
275 kind: DiffLineKind::Addition,
276 old_line: None,
277 new_line: Some(10),
278 text: "new\n".to_string(),
279 },
280 DiffLine {
281 kind: DiffLineKind::Context,
282 old_line: Some(11),
283 new_line: Some(11),
284 text: "same\n".to_string(),
285 },
286 ],
287 }];
288
289 let lines = display_lines_from_hunks(&hunks);
290 assert_eq!(lines[0].kind, DiffDisplayKind::HunkHeader);
291 assert_eq!(lines[0].text, "@@ -10 +10 @@");
292 assert_eq!(lines[1].kind, DiffDisplayKind::Deletion);
293 assert_eq!(lines[1].line_number, Some(10));
294 assert_eq!(lines[1].text, "old");
295 assert_eq!(lines[2].kind, DiffDisplayKind::Addition);
296 assert_eq!(lines[2].line_number, Some(10));
297 assert_eq!(lines[3].kind, DiffDisplayKind::Context);
298 assert_eq!(lines[3].line_number, Some(11));
299 }
300
301 #[test]
302 fn diff_display_line_number_width_tracks_max_digits() {
303 let lines = vec![
304 DiffDisplayLine {
305 kind: DiffDisplayKind::Addition,
306 line_number: Some(99),
307 text: "let a = 1;".to_string(),
308 },
309 DiffDisplayLine {
310 kind: DiffDisplayKind::Context,
311 line_number: Some(10_420),
312 text: "let b = 2;".to_string(),
313 },
314 ];
315
316 assert_eq!(diff_display_line_number_width(&lines), 5);
317 }
318
319 #[test]
320 fn preserves_plain_text_when_not_diff() {
321 let lines = format_numbered_unified_diff("plain text output");
322 assert_eq!(lines, vec!["plain text output".to_string()]);
323 }
324}