1use similar::{ChangeTag, TextDiff};
4
5use super::layout::layout_document;
6use super::parse::{is_unified_metadata_line, parse_hunk_range, parse_omitted_line_count, trim_line_ending};
7use super::types::{
8 DiffBundle, DiffDocument, DiffHunk, DiffLine, DiffLineKind, DiffOptions, DiffRow, DiffStats, LayoutOptions,
9 ParseDiffError, default_inline_timeout,
10};
11
12impl DiffDocument {
13 #[must_use]
15 pub fn between(old: &str, new: &str, options: DiffOptions<'_>) -> Self {
16 let old_lines = split_lines_with_terminator(old);
17 let new_lines = split_lines_with_terminator(new);
18 let old_line_refs: Vec<&str> = old_lines.iter().map(String::as_str).collect();
19 let new_line_refs: Vec<&str> = new_lines.iter().map(String::as_str).collect();
20 let mut config = TextDiff::configure();
21 let _ = config.algorithm(options.algorithm.similar()).timeout(options.timeout);
22 let diff = config.diff_slices(&old_line_refs, &new_line_refs);
23 let mut hunks = Vec::new();
24
25 for group in diff.grouped_ops(options.context_lines) {
26 let Some(_first) = group.first() else {
27 continue;
28 };
29 let mut lines = Vec::new();
30 for operation in &group {
31 for change in diff.iter_changes(operation) {
32 let (kind, old_line, new_line) = match change.tag() {
33 ChangeTag::Equal => (
34 DiffLineKind::Context,
35 change.old_index().and_then(one_based_u32),
36 change.new_index().and_then(one_based_u32),
37 ),
38 ChangeTag::Delete => (DiffLineKind::Deletion, change.old_index().and_then(one_based_u32), None),
39 ChangeTag::Insert => (DiffLineKind::Addition, None, change.new_index().and_then(one_based_u32)),
40 };
41 lines.push(DiffLine {
42 kind,
43 old_line,
44 new_line,
45 text: change.value().to_owned(),
46 });
47 }
48 }
49 let old_lines = lines.iter().filter(|line| line.kind != DiffLineKind::Addition).count();
50 let new_lines = lines.iter().filter(|line| line.kind != DiffLineKind::Deletion).count();
51 let old_start = hunk_start_from_lines(&lines, true);
52 let new_start = hunk_start_from_lines(&lines, false);
53 hunks.push(DiffHunk { old_start, old_lines, new_start, new_lines, lines });
54 }
55 let mut document = Self::from_hunks(hunks);
56 document.inline_timeout = options.inline_timeout;
57 document
58 }
59
60 #[must_use]
62 pub fn from_hunks(hunks: Vec<DiffHunk>) -> Self {
63 let mut stats = DiffStats { hunks: hunks.len(), ..DiffStats::default() };
64 for line in hunks.iter().flat_map(|hunk| &hunk.lines) {
65 match line.kind {
66 DiffLineKind::Context => stats.context += 1,
67 DiffLineKind::Addition => stats.additions += 1,
68 DiffLineKind::Deletion => stats.deletions += 1,
69 }
70 }
71 Self {
72 hunks,
73 stats,
74 inline_timeout: default_inline_timeout(),
75 }
76 }
77
78 pub fn from_unified(input: &str) -> Result<Self, ParseDiffError> {
83 let mut hunks = Vec::new();
84 let mut current: Option<DiffHunk> = None;
85 let mut old_line = 0u32;
86 let mut new_line = 0u32;
87 let mut expected_old_lines = 0usize;
88 let mut expected_new_lines = 0usize;
89 let mut omitted_in_current_hunk = false;
90 let mut omitted_rows = 0usize;
91 let mut omitted_tail = Vec::new();
92
93 for raw_with_ending in split_line_slices(input) {
94 let raw = trim_line_ending(raw_with_ending);
95 if raw.starts_with("@@") {
96 if let Some(mut hunk) = current.take() {
97 assign_omitted_hunk_tail_line_numbers(
98 &mut hunk,
99 &omitted_tail,
100 expected_old_lines,
101 expected_new_lines,
102 );
103 if !omitted_in_current_hunk {
104 validate_hunk_counts(&hunk, expected_old_lines, expected_new_lines)?;
105 }
106 hunks.push(hunk);
107 }
108 omitted_tail.clear();
109 let (old_start, old_count, new_start, new_count) =
110 parse_hunk_range(raw).ok_or_else(|| ParseDiffError::new("invalid hunk header"))?;
111 old_line = old_start;
112 new_line = new_start;
113 expected_old_lines = old_count;
114 expected_new_lines = new_count;
115 omitted_in_current_hunk = false;
116 current = Some(DiffHunk {
117 old_start: old_start as usize,
118 old_lines: 0,
119 new_start: new_start as usize,
120 new_lines: 0,
121 lines: Vec::new(),
122 });
123 continue;
124 }
125 if raw.starts_with("diff ")
126 || raw.starts_with("index ")
127 || raw == r"\ No newline at end of file"
128 || raw.is_empty()
129 {
130 continue;
131 }
132 let hunk_complete = current
136 .as_ref()
137 .is_some_and(|hunk| hunk.old_lines >= expected_old_lines && hunk.new_lines >= expected_new_lines);
138 if (current.is_none() || hunk_complete) && is_unified_metadata_line(raw) {
139 continue;
140 }
141 if hunk_complete && (raw.starts_with("--- ") || raw.starts_with("+++ ")) {
142 continue;
143 }
144 let Some(hunk) = current.as_mut() else {
145 return Err(ParseDiffError::new("diff body appears before a hunk header"));
146 };
147 let is_body_line = matches!(raw.as_bytes().first().copied(), Some(b'-' | b'+' | b' '));
148 if !is_body_line && let Some(omitted) = parse_omitted_line_count(raw) {
149 omitted_in_current_hunk = true;
150 let marker_rows = omitted;
151 let advance = marker_rows
152 .min(expected_old_lines.saturating_sub(hunk.old_lines))
153 .min(expected_new_lines.saturating_sub(hunk.new_lines));
154 old_line = old_line.saturating_add(u32::try_from(advance).unwrap_or(u32::MAX));
155 new_line = new_line.saturating_add(u32::try_from(advance).unwrap_or(u32::MAX));
156 hunk.old_lines = hunk.old_lines.saturating_add(advance);
157 hunk.new_lines = hunk.new_lines.saturating_add(advance);
158 omitted_rows = omitted_rows.saturating_add(marker_rows);
159 continue;
160 }
161 let (kind, old_number, new_number) = match raw.as_bytes().first().copied() {
162 Some(b'-') => {
163 if hunk.old_lines >= expected_old_lines {
164 return Err(ParseDiffError::new("too many old-side lines for hunk header"));
165 }
166 let number = old_line;
167 old_line = old_line.saturating_add(1);
168 hunk.old_lines += 1;
169 (DiffLineKind::Deletion, Some(number), None)
170 }
171 Some(b'+') => {
172 if hunk.new_lines >= expected_new_lines {
173 return Err(ParseDiffError::new("too many new-side lines for hunk header"));
174 }
175 let number = new_line;
176 new_line = new_line.saturating_add(1);
177 hunk.new_lines += 1;
178 (DiffLineKind::Addition, None, Some(number))
179 }
180 Some(b' ') => {
181 if hunk.old_lines >= expected_old_lines || hunk.new_lines >= expected_new_lines {
182 return Err(ParseDiffError::new("too many context lines for hunk header"));
183 }
184 let old_number = old_line;
185 let new_number = new_line;
186 old_line = old_line.saturating_add(1);
187 new_line = new_line.saturating_add(1);
188 hunk.old_lines += 1;
189 hunk.new_lines += 1;
190 (DiffLineKind::Context, Some(old_number), Some(new_number))
191 }
192 _ => return Err(ParseDiffError::new("invalid unified diff body line")),
193 };
194 hunk.lines.push(DiffLine {
195 kind,
196 old_line: old_number,
197 new_line: new_number,
198 text: raw_with_ending[1..].to_owned(),
199 });
200 if omitted_in_current_hunk {
201 omitted_tail.push(hunk.lines.len() - 1);
202 }
203 }
204 if let Some(mut hunk) = current {
205 assign_omitted_hunk_tail_line_numbers(&mut hunk, &omitted_tail, expected_old_lines, expected_new_lines);
206 if !omitted_in_current_hunk {
207 validate_hunk_counts(&hunk, expected_old_lines, expected_new_lines)?;
208 }
209 hunks.push(hunk);
210 }
211 let mut document = Self::from_hunks(hunks);
212 document.stats.omitted_rows = omitted_rows;
213 Ok(document)
214 }
215
216 #[must_use]
218 pub fn layout(&self, options: LayoutOptions) -> Vec<DiffRow> {
219 layout_document(self, options)
220 }
221}
222fn one_based_u32(index: usize) -> Option<u32> {
223 u32::try_from(index).ok()?.checked_add(1)
226}
227
228fn hunk_start_from_lines(lines: &[DiffLine], old_side: bool) -> usize {
229 let line_number = lines
231 .iter()
232 .find_map(|line| if old_side { line.old_line } else { line.new_line });
233 if let Some(line_number) = line_number {
234 return usize::try_from(line_number).unwrap_or(usize::MAX).max(1);
235 }
236
237 let counterpart = lines
238 .iter()
239 .find_map(|line| if old_side { line.new_line } else { line.old_line });
240 usize::try_from(counterpart.unwrap_or(1)).unwrap_or(usize::MAX).max(1)
241}
242
243fn validate_hunk_counts(
244 hunk: &DiffHunk,
245 expected_old_lines: usize,
246 expected_new_lines: usize,
247) -> Result<(), ParseDiffError> {
248 if hunk.counts_match(expected_old_lines, expected_new_lines) {
250 Ok(())
251 } else {
252 Err(ParseDiffError::new("hunk line counts do not match header"))
253 }
254}
255
256fn assign_omitted_hunk_tail_line_numbers(
257 hunk: &mut DiffHunk,
258 tail: &[usize],
259 expected_old_lines: usize,
260 expected_new_lines: usize,
261) {
262 if tail.is_empty() {
266 return;
267 }
268 debug_assert!(tail.iter().all(|&index| index < hunk.lines.len()));
269 let old_tail_count = tail
270 .iter()
271 .filter(|&&index| hunk.lines[index].kind != DiffLineKind::Addition)
272 .count();
273 let new_tail_count = tail
274 .iter()
275 .filter(|&&index| hunk.lines[index].kind != DiffLineKind::Deletion)
276 .count();
277 let mut old_line = u32::try_from(hunk.old_start)
278 .unwrap_or(u32::MAX)
279 .saturating_add(u32::try_from(expected_old_lines).unwrap_or(u32::MAX))
280 .saturating_sub(u32::try_from(old_tail_count).unwrap_or(u32::MAX));
281 let mut new_line = u32::try_from(hunk.new_start)
282 .unwrap_or(u32::MAX)
283 .saturating_add(u32::try_from(expected_new_lines).unwrap_or(u32::MAX))
284 .saturating_sub(u32::try_from(new_tail_count).unwrap_or(u32::MAX));
285 for &index in tail {
286 match hunk.lines[index].kind {
287 DiffLineKind::Addition => {
288 hunk.lines[index].new_line = Some(new_line);
289 new_line = new_line.saturating_add(1);
290 }
291 DiffLineKind::Deletion => {
292 hunk.lines[index].old_line = Some(old_line);
293 old_line = old_line.saturating_add(1);
294 }
295 DiffLineKind::Context => {
296 hunk.lines[index].old_line = Some(old_line);
297 hunk.lines[index].new_line = Some(new_line);
298 old_line = old_line.saturating_add(1);
299 new_line = new_line.saturating_add(1);
300 }
301 }
302 }
303}
304pub fn compute_diff<F>(old: &str, new: &str, options: DiffOptions<'_>, formatter: F) -> DiffBundle
312where
313 F: FnOnce(&[DiffHunk], &DiffOptions<'_>) -> String,
314{
315 if old == new {
316 return DiffBundle {
317 hunks: Vec::new(),
318 formatted: String::new(),
319 is_empty: true,
320 };
321 }
322 let document = DiffDocument::between(old, new, options.clone());
323 let formatted = formatter(&document.hunks, &options);
324 DiffBundle { hunks: document.hunks, formatted, is_empty: false }
325}
326pub(crate) fn split_lines_with_terminator(text: &str) -> Vec<String> {
327 split_line_slices(text).into_iter().map(str::to_owned).collect()
328}
329
330pub(crate) fn split_line_slices(text: &str) -> Vec<&str> {
340 let mut lines = Vec::new();
341 let mut start = 0usize;
342 let bytes = text.as_bytes();
343 let mut index = 0usize;
344 while index < bytes.len() {
347 if bytes[index] != b'\n' && bytes[index] != b'\r' {
348 index += 1;
349 continue;
350 }
351 let end = if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') {
352 index + 2
353 } else {
354 index + 1
355 };
356 debug_assert!(text.is_char_boundary(start) && text.is_char_boundary(end));
357 lines.push(&text[start..end]);
358 start = end;
359 index = end;
360 }
361 if start < text.len() {
362 debug_assert!(text.is_char_boundary(start));
363 lines.push(&text[start..]);
364 }
365 lines
366}