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