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