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