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 => 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 = 0u32;
96 let mut new_line_no = 0u32;
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 as u32;
102 new_line_no = new_start as u32;
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(5, 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.into_iter().map(|line| line.numbered_text(width)).collect()
176}
177
178fn display_line_from_diff_line(line: &crate::diff::DiffLine) -> DiffDisplayLine {
179 let text = line.text.trim_end_matches('\n').to_string();
180 match line.kind {
181 DiffLineKind::Context => DiffDisplayLine {
182 kind: DiffDisplayKind::Context,
183 line_number: line.new_line,
184 text,
185 },
186 DiffLineKind::Addition => DiffDisplayLine {
187 kind: DiffDisplayKind::Addition,
188 line_number: line.new_line,
189 text,
190 },
191 DiffLineKind::Deletion => DiffDisplayLine {
192 kind: DiffDisplayKind::Deletion,
193 line_number: line.old_line,
194 text,
195 },
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::diff::{DiffLine, DiffLineKind};
203
204 #[test]
205 fn counts_diff_changes_from_hunks() {
206 let hunks = vec![DiffHunk {
207 old_start: 1,
208 old_lines: 2,
209 new_start: 1,
210 new_lines: 2,
211 lines: vec![
212 DiffLine {
213 kind: DiffLineKind::Context,
214 old_line: Some(1),
215 new_line: Some(1),
216 text: "same\n".to_string(),
217 },
218 DiffLine {
219 kind: DiffLineKind::Deletion,
220 old_line: Some(2),
221 new_line: None,
222 text: "old\n".to_string(),
223 },
224 DiffLine {
225 kind: DiffLineKind::Addition,
226 old_line: None,
227 new_line: Some(2),
228 text: "new\n".to_string(),
229 },
230 ],
231 }];
232
233 let counts = count_diff_changes(&hunks);
234 assert_eq!(counts.additions, 1);
235 assert_eq!(counts.deletions, 1);
236 assert_eq!(counts.total(), 2);
237 }
238
239 #[test]
240 fn formats_numbered_unified_diff_with_start_only_headers() {
241 let diff = "\
242diff --git a/file.txt b/file.txt
243@@ -10,2 +10,2 @@
244-old
245+new
246 context
247";
248
249 let lines = format_numbered_unified_diff(diff);
250 assert_eq!(lines[0], "diff --git a/file.txt b/file.txt");
251 assert!(lines.iter().any(|line| line == "@@ -10 +10 @@"));
252 assert!(lines.iter().any(|line| line.starts_with("- 10 old")));
253 assert!(lines.iter().any(|line| line.starts_with("+ 10 new")));
254 assert!(lines.iter().any(|line| line.starts_with(" 11 context")));
255 }
256
257 #[test]
258 fn display_lines_from_hunks_preserves_semantics() {
259 let hunks = vec![DiffHunk {
260 old_start: 10,
261 old_lines: 2,
262 new_start: 10,
263 new_lines: 2,
264 lines: vec![
265 DiffLine {
266 kind: DiffLineKind::Deletion,
267 old_line: Some(10),
268 new_line: None,
269 text: "old\n".to_string(),
270 },
271 DiffLine {
272 kind: DiffLineKind::Addition,
273 old_line: None,
274 new_line: Some(10),
275 text: "new\n".to_string(),
276 },
277 DiffLine {
278 kind: DiffLineKind::Context,
279 old_line: Some(11),
280 new_line: Some(11),
281 text: "same\n".to_string(),
282 },
283 ],
284 }];
285
286 let lines = display_lines_from_hunks(&hunks);
287 assert_eq!(lines[0].kind, DiffDisplayKind::HunkHeader);
288 assert_eq!(lines[0].text, "@@ -10 +10 @@");
289 assert_eq!(lines[1].kind, DiffDisplayKind::Deletion);
290 assert_eq!(lines[1].line_number, Some(10));
291 assert_eq!(lines[1].text, "old");
292 assert_eq!(lines[2].kind, DiffDisplayKind::Addition);
293 assert_eq!(lines[2].line_number, Some(10));
294 assert_eq!(lines[3].kind, DiffDisplayKind::Context);
295 assert_eq!(lines[3].line_number, Some(11));
296 }
297
298 #[test]
299 fn diff_display_line_number_width_tracks_max_digits() {
300 let lines = vec![
301 DiffDisplayLine {
302 kind: DiffDisplayKind::Addition,
303 line_number: Some(99),
304 text: "let a = 1;".to_string(),
305 },
306 DiffDisplayLine {
307 kind: DiffDisplayKind::Context,
308 line_number: Some(10_420),
309 text: "let b = 2;".to_string(),
310 },
311 ];
312
313 assert_eq!(diff_display_line_number_width(&lines), 5);
314 }
315
316 #[test]
317 fn preserves_plain_text_when_not_diff() {
318 let lines = format_numbered_unified_diff("plain text output");
319 assert_eq!(lines, vec!["plain text output".to_string()]);
320 }
321}