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