1use anstyle::Reset;
4use hashbrown::HashMap;
5use serde::Serialize;
6use std::cmp::min;
7use std::fmt::Write;
8
9use crate::ui::theme;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Chunk<'a> {
14 Equal(&'a str),
15 Delete(&'a str),
16 Insert(&'a str),
17}
18
19pub fn compute_diff_chunks<'a>(old: &'a str, new: &'a str) -> Vec<Chunk<'a>> {
21 if old.is_empty() && new.is_empty() {
22 return Vec::with_capacity(0);
23 }
24 if old.is_empty() {
25 return vec![Chunk::Insert(new)];
26 }
27 if new.is_empty() {
28 return vec![Chunk::Delete(old)];
29 }
30
31 let mut prefix_len = 0;
33 for (o, n) in old.chars().zip(new.chars()) {
34 if o == n {
35 prefix_len += o.len_utf8();
36 } else {
37 break;
38 }
39 }
40
41 let mut suffix_len = 0;
42 let old_rest = &old[prefix_len..];
43 let new_rest = &new[prefix_len..];
44
45 let old_chars: Vec<char> = old_rest.chars().collect();
46 let new_chars: Vec<char> = new_rest.chars().collect();
47
48 for (o, n) in old_chars.iter().rev().zip(new_chars.iter().rev()) {
49 if o == n {
50 suffix_len += o.len_utf8();
51 } else {
52 break;
53 }
54 }
55
56 let old_middle_end = old_rest.len() - suffix_len;
57 let new_middle_end = new_rest.len() - suffix_len;
58
59 let old_middle = &old_rest[..old_middle_end];
60 let new_middle = &new_rest[..new_middle_end];
61
62 let mut result = Vec::with_capacity(old_middle.len() + new_middle.len());
63
64 if prefix_len > 0 {
66 result.push(Chunk::Equal(&old[..prefix_len]));
67 }
68
69 if !old_middle.is_empty() || !new_middle.is_empty() {
71 let old_chars: Vec<char> = old_middle.chars().collect();
72 let new_chars: Vec<char> = new_middle.chars().collect();
73 let old_byte_starts: Vec<usize> = old_middle.char_indices().map(|(idx, _)| idx).collect();
74 let new_byte_starts: Vec<usize> = new_middle.char_indices().map(|(idx, _)| idx).collect();
75 let edits = myers_diff(&old_chars, &new_chars);
76
77 let mut old_pos = 0;
78 let mut new_pos = 0;
79
80 for edit in edits {
81 match edit {
82 Edit::Equal => {
83 old_pos += 1;
84 new_pos += 1;
85 }
86 Edit::Delete => {
87 let Some(ch) = old_chars.get(old_pos).copied() else {
88 break;
89 };
90 let Some(byte_start) = old_byte_starts.get(old_pos).copied() else {
91 break;
92 };
93 let byte_end = byte_start + ch.len_utf8();
94 result.push(Chunk::Delete(&old_middle[byte_start..byte_end]));
95 old_pos += 1;
96 }
97 Edit::Insert => {
98 let Some(ch) = new_chars.get(new_pos).copied() else {
99 break;
100 };
101 let Some(byte_start) = new_byte_starts.get(new_pos).copied() else {
102 break;
103 };
104 let byte_end = byte_start + ch.len_utf8();
105 result.push(Chunk::Insert(&new_middle[byte_start..byte_end]));
106 new_pos += 1;
107 }
108 }
109 }
110 }
111
112 if suffix_len > 0 {
114 result.push(Chunk::Equal(&old[old.len() - suffix_len..]));
115 }
116
117 result
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121enum Edit {
122 Equal,
123 Delete,
124 Insert,
125}
126
127fn myers_diff(old: &[char], new: &[char]) -> Vec<Edit> {
128 let n = old.len();
129 let m = new.len();
130
131 if n == 0 {
132 return vec![Edit::Insert; m];
133 }
134 if m == 0 {
135 return vec![Edit::Delete; n];
136 }
137
138 let max_d = n + m;
139 let mut v = vec![0; 2 * max_d + 1];
140 let mut v_index = vec![0usize; (max_d + 1) * (2 * max_d + 1)];
141 let row_len = 2 * max_d + 1;
142
143 v[max_d] = 0;
144
145 for d in 0..=max_d {
146 for k in (-(d as i32)..=(d as i32)).step_by(2) {
147 let k_idx = (k + max_d as i32) as usize;
148
149 let x = if k == -(d as i32) || (k != d as i32 && v[k_idx - 1] < v[k_idx + 1]) {
150 v[k_idx + 1]
151 } else {
152 v[k_idx - 1] + 1
153 };
154
155 let mut x = x;
156 let mut y = (x as i32 - k) as usize;
157
158 while x < n && y < m && old[x] == new[y] {
159 x += 1;
160 y += 1;
161 }
162
163 v[k_idx] = x;
164 v_index[d * row_len + k_idx] = x;
165
166 if x >= n && y >= m {
167 return backtrack_myers(old, new, &v_index, d, k, max_d);
168 }
169 }
170 }
171
172 vec![]
173}
174
175fn backtrack_myers(
176 old: &[char],
177 new: &[char],
178 v_index: &[usize],
179 d: usize,
180 mut k: i32,
181 max_d: usize,
182) -> Vec<Edit> {
183 let mut edits = Vec::with_capacity(old.len() + new.len());
184 let mut x = old.len();
185 let mut y = new.len();
186 let row_len = 2 * max_d + 1;
187
188 for cur_d in (0..=d).rev() {
189 if cur_d == 0 {
190 while x > 0 && y > 0 {
191 edits.push(Edit::Equal);
192 x -= 1;
193 y -= 1;
194 }
195 break;
196 }
197
198 let k_idx = (k + max_d as i32) as usize;
199
200 let prev_k = if k == -(cur_d as i32)
201 || (k != cur_d as i32
202 && v_index[(cur_d - 1) * row_len + k_idx - 1]
203 < v_index[(cur_d - 1) * row_len + k_idx + 1])
204 {
205 k + 1
206 } else {
207 k - 1
208 };
209
210 let prev_k_idx = (prev_k + max_d as i32) as usize;
211 let prev_x_val = v_index[(cur_d - 1) * row_len + prev_k_idx];
212 let prev_y = (prev_x_val as i32 - prev_k) as usize;
213
214 let (move_x, move_y) = if prev_k == k + 1 {
215 (prev_x_val, prev_y + 1)
216 } else {
217 (prev_x_val + 1, prev_y)
218 };
219
220 while x > move_x && y > move_y {
221 edits.push(Edit::Equal);
222 x -= 1;
223 y -= 1;
224 }
225
226 if prev_k == k + 1 {
227 edits.push(Edit::Insert);
228 y -= 1;
229 } else {
230 edits.push(Edit::Delete);
231 x -= 1;
232 }
233
234 k = prev_k;
235 }
236
237 edits.reverse();
238 edits
239}
240
241#[derive(Debug, Clone)]
243pub struct DiffOptions<'a> {
244 pub context_lines: usize,
245 pub old_label: Option<&'a str>,
246 pub new_label: Option<&'a str>,
247 pub missing_newline_hint: bool,
248}
249
250impl Default for DiffOptions<'_> {
251 fn default() -> Self {
252 Self {
253 context_lines: 3,
254 old_label: None,
255 new_label: None,
256 missing_newline_hint: true,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize)]
263pub struct DiffBundle {
264 pub hunks: Vec<DiffHunk>,
265 pub formatted: String,
266 pub is_empty: bool,
267}
268
269#[derive(Debug, Clone, Serialize)]
271pub struct DiffHunk {
272 pub old_start: usize,
273 pub old_lines: usize,
274 pub new_start: usize,
275 pub new_lines: usize,
276 pub lines: Vec<DiffLine>,
277}
278
279#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
281#[serde(rename_all = "snake_case")]
282pub enum DiffLineKind {
283 Context,
284 Addition,
285 Deletion,
286}
287
288#[derive(Debug, Clone, Serialize)]
290pub struct DiffLine {
291 pub kind: DiffLineKind,
292 pub old_line: Option<usize>,
293 pub new_line: Option<usize>,
294 pub text: String,
295}
296
297pub fn compute_diff<F>(old: &str, new: &str, options: DiffOptions<'_>, formatter: F) -> DiffBundle
299where
300 F: FnOnce(&[DiffHunk], &DiffOptions<'_>) -> String,
301{
302 let old_lines_owned = split_lines_with_terminator(old);
303 let new_lines_owned = split_lines_with_terminator(new);
304
305 let old_refs: Vec<&str> = old_lines_owned.iter().map(|s| s.as_str()).collect();
306 let new_refs: Vec<&str> = new_lines_owned.iter().map(|s| s.as_str()).collect();
307
308 let records = collect_line_records(&old_refs, &new_refs);
309 let has_changes = records
310 .iter()
311 .any(|record| matches!(record.kind, DiffLineKind::Addition | DiffLineKind::Deletion));
312
313 let hunks = if has_changes {
314 build_hunks(&records, options.context_lines)
315 } else {
316 Vec::new()
317 };
318
319 let formatted = if hunks.is_empty() {
320 String::new()
321 } else {
322 formatter(&hunks, &options)
323 };
324
325 DiffBundle {
326 hunks,
327 formatted,
328 is_empty: !has_changes,
329 }
330}
331
332pub fn format_unified_diff(old: &str, new: &str, options: DiffOptions<'_>) -> String {
334 let mut options = options;
335 options.missing_newline_hint = false;
336 let bundle = compute_diff(old, new, options, format_colored_diff);
337 crate::utils::ansi_parser::strip_ansi(&bundle.formatted)
338}
339
340pub fn compute_diff_with_theme(old: &str, new: &str, options: DiffOptions<'_>) -> DiffBundle {
342 compute_diff(old, new, options, format_colored_diff)
343}
344
345pub fn format_colored_diff(hunks: &[DiffHunk], options: &DiffOptions<'_>) -> String {
347 if hunks.is_empty() {
348 return String::new();
349 }
350
351 let active_styles = theme::active_styles();
352 let header_style = active_styles.status;
353 let hunk_header_style = active_styles.status;
354 let addition_style = active_styles.secondary;
355 let deletion_style = active_styles.error;
356 let context_style = active_styles.output;
357
358 let mut output = String::new();
359
360 if let (Some(old_label), Some(new_label)) = (options.old_label, options.new_label) {
361 let _ = write!(
362 output,
363 "{}--- {old_label}\n{}",
364 header_style.render(),
365 Reset.render()
366 );
367
368 let _ = write!(
369 output,
370 "{}+++ {new_label}\n{}",
371 header_style.render(),
372 Reset.render()
373 );
374 }
375
376 for hunk in hunks {
377 let _ = write!(
378 output,
379 "{}@@ -{},{} +{},{} @@\n{}",
380 hunk_header_style.render(),
381 hunk.old_start,
382 hunk.old_lines,
383 hunk.new_start,
384 hunk.new_lines,
385 Reset.render()
386 );
387
388 for line in &hunk.lines {
389 let (style, prefix) = match line.kind {
390 DiffLineKind::Addition => (&addition_style, '+'),
391 DiffLineKind::Deletion => (&deletion_style, '-'),
392 DiffLineKind::Context => (&context_style, ' '),
393 };
394
395 let mut display = String::with_capacity(line.text.len() + 2);
396 display.push(prefix);
397 display.push_str(&line.text);
398
399 let has_newline = display.ends_with('\n');
400 let display_content = if has_newline {
401 &display[..display.len() - 1]
402 } else {
403 &display
404 };
405
406 let _ = write!(
407 output,
408 "{}{} {}",
409 style.render(),
410 display_content,
411 Reset.render()
412 );
413 output.push('\n');
414
415 if options.missing_newline_hint && !line.text.ends_with('\n') {
416 let eof_hint = r"\ No newline at end of file";
417 let _ = write!(
418 output,
419 "{}{} {}",
420 context_style.render(),
421 eof_hint,
422 Reset.render()
423 );
424 output.push('\n');
425 }
426 }
427 }
428
429 output
430}
431
432fn split_lines_with_terminator(text: &str) -> Vec<String> {
433 if text.is_empty() {
434 return Vec::with_capacity(0);
435 }
436
437 let mut lines: Vec<String> = text
438 .split_inclusive('\n')
439 .map(|line| line.to_string())
440 .collect();
441
442 if lines.is_empty() {
443 lines.push(text.to_string());
444 }
445
446 lines
447}
448
449fn collect_line_records<'a>(
450 old_lines: &'a [&'a str],
451 new_lines: &'a [&'a str],
452) -> Vec<LineRecord<'a>> {
453 let (old_encoded, new_encoded) = encode_line_sequences(old_lines, new_lines);
454 let mut records = Vec::with_capacity(old_lines.len() + new_lines.len());
455 let mut old_index = 0usize;
456 let mut new_index = 0usize;
457
458 for chunk in compute_diff_chunks(old_encoded.as_str(), new_encoded.as_str()) {
459 match chunk {
460 Chunk::Equal(text) => {
461 for _ in text.chars() {
462 let old_line = old_index + 1;
463 let new_line = new_index + 1;
464 let line = old_lines[old_index];
465 records.push(LineRecord {
466 kind: DiffLineKind::Context,
467 old_line: Some(old_line),
468 new_line: Some(new_line),
469 text: line,
470 anchor_old: old_line,
471 anchor_new: new_line,
472 });
473 old_index += 1;
474 new_index += 1;
475 }
476 }
477 Chunk::Delete(text) => {
478 for _ in text.chars() {
479 let old_line = old_index + 1;
480 let anchor_new = new_index + 1;
481 let line = old_lines[old_index];
482 records.push(LineRecord {
483 kind: DiffLineKind::Deletion,
484 old_line: Some(old_line),
485 new_line: None,
486 text: line,
487 anchor_old: old_line,
488 anchor_new,
489 });
490 old_index += 1;
491 }
492 }
493 Chunk::Insert(text) => {
494 for _ in text.chars() {
495 let new_line = new_index + 1;
496 let anchor_old = old_index + 1;
497 let line = new_lines[new_index];
498 records.push(LineRecord {
499 kind: DiffLineKind::Addition,
500 old_line: None,
501 new_line: Some(new_line),
502 text: line,
503 anchor_old,
504 anchor_new: new_line,
505 });
506 new_index += 1;
507 }
508 }
509 }
510 }
511
512 records
513}
514
515fn encode_line_sequences<'a>(
516 old_lines: &'a [&'a str],
517 new_lines: &'a [&'a str],
518) -> (String, String) {
519 let mut token_map: HashMap<&'a str, char> = HashMap::new();
520 let mut next_codepoint: u32 = 0;
521
522 let old_encoded = encode_line_list(old_lines, &mut token_map, &mut next_codepoint);
523 let new_encoded = encode_line_list(new_lines, &mut token_map, &mut next_codepoint);
524
525 (old_encoded, new_encoded)
526}
527
528fn encode_line_list<'a>(
529 lines: &'a [&'a str],
530 map: &mut HashMap<&'a str, char>,
531 next_codepoint: &mut u32,
532) -> String {
533 let mut encoded = String::with_capacity(lines.len());
534 for &line in lines {
535 let token = if let Some(&value) = map.get(line) {
536 value
537 } else {
538 let Some(ch) = next_token_char(next_codepoint) else {
539 break;
540 };
541 map.insert(line, ch);
542 ch
543 };
544 encoded.push(token);
545 }
546 encoded
547}
548
549fn next_token_char(counter: &mut u32) -> Option<char> {
550 while *counter <= 0x10FFFF {
551 let candidate = *counter;
552 *counter += 1;
553 if (0xD800..=0xDFFF).contains(&candidate) {
554 continue;
555 }
556 if let Some(ch) = char::from_u32(candidate) {
557 return Some(ch);
558 }
559 }
560 None
561}
562
563#[derive(Debug)]
564struct LineRecord<'a> {
565 kind: DiffLineKind,
566 old_line: Option<usize>,
567 new_line: Option<usize>,
568 text: &'a str,
569 anchor_old: usize,
570 anchor_new: usize,
571}
572
573fn build_hunks(records: &[LineRecord<'_>], context: usize) -> Vec<DiffHunk> {
574 if records.is_empty() {
575 return Vec::new();
576 }
577
578 let ranges = compute_hunk_ranges(records, context);
579 let mut hunks = Vec::with_capacity(ranges.len());
580
581 for (start, end) in ranges {
582 let slice = &records[start..=end];
583
584 let old_start = slice
585 .iter()
586 .filter_map(|r| r.old_line)
587 .min()
588 .or_else(|| slice.iter().map(|r| r.anchor_old).min())
589 .unwrap_or(1);
590
591 let new_start = slice
592 .iter()
593 .filter_map(|r| r.new_line)
594 .min()
595 .or_else(|| slice.iter().map(|r| r.anchor_new).min())
596 .unwrap_or(1);
597
598 let old_lines = slice
599 .iter()
600 .filter(|r| matches!(r.kind, DiffLineKind::Context | DiffLineKind::Deletion))
601 .count();
602 let new_lines = slice
603 .iter()
604 .filter(|r| matches!(r.kind, DiffLineKind::Context | DiffLineKind::Addition))
605 .count();
606
607 let lines = slice
608 .iter()
609 .map(|record| DiffLine {
610 kind: record.kind.clone(),
611 old_line: record.old_line,
612 new_line: record.new_line,
613 text: record.text.to_string(),
614 })
615 .collect();
616
617 hunks.push(DiffHunk {
618 old_start,
619 old_lines,
620 new_start,
621 new_lines,
622 lines,
623 });
624 }
625
626 hunks
627}
628
629fn compute_hunk_ranges(records: &[LineRecord<'_>], context: usize) -> Vec<(usize, usize)> {
630 let mut ranges = Vec::with_capacity(4);
631 let mut current_start: Option<usize> = None;
632 let mut current_end: usize = 0;
633
634 for (idx, record) in records.iter().enumerate() {
635 if record.kind != DiffLineKind::Context {
636 let start = idx.saturating_sub(context);
637 let end = min(idx + context, records.len().saturating_sub(1));
638
639 if let Some(existing_start) = current_start {
640 if start < existing_start {
641 current_start = Some(start);
642 }
643 if end > current_end {
644 current_end = end;
645 }
646 } else {
647 current_start = Some(start);
648 current_end = end;
649 }
650 } else if let Some(start) = current_start
651 && idx > current_end
652 {
653 ranges.push((start, current_end));
654 current_start = None;
655 }
656 }
657
658 if let Some(start) = current_start {
659 ranges.push((start, current_end));
660 }
661
662 ranges
663}