Skip to main content

vtcode_diff/
format.rs

1//! Unified-diff text formatting and numbered rendering.
2
3use super::display::display_lines_from_unified_diff;
4use super::parse::trim_line_ending;
5use super::types::{DiffDisplayLine, DiffDocument, DiffHunk, DiffLineKind, DiffOptions};
6
7/// Formats a structured diff as plain unified text.
8///
9/// Labels are emitted when both [`DiffOptions::old_label`] and
10/// [`DiffOptions::new_label`] are present. Source line terminators are
11/// normalized to `\n` in the formatted output while missing-final-newline
12/// hints remain controlled by [`DiffOptions::missing_newline_hint`].
13#[must_use]
14pub fn format_unified_diff(old: &str, new: &str, options: DiffOptions<'_>) -> String {
15    let document = DiffDocument::between(old, new, options.clone());
16    format_unified_hunks(&document.hunks, &options)
17}
18
19/// Formats precomputed hunks as plain unified text.
20#[must_use]
21pub fn format_unified_hunks(hunks: &[DiffHunk], options: &DiffOptions<'_>) -> String {
22    if hunks.is_empty() {
23        return String::new();
24    }
25
26    let mut output = String::new();
27    if let (Some(old_label), Some(new_label)) = (options.old_label, options.new_label) {
28        output.push_str("--- ");
29        output.push_str(old_label);
30        output.push('\n');
31        output.push_str("+++ ");
32        output.push_str(new_label);
33        output.push('\n');
34    }
35
36    for hunk in hunks {
37        output.push_str("@@ -");
38        output.push_str(&format_unified_range(hunk.old_start, hunk.old_lines));
39        output.push_str(" +");
40        output.push_str(&format_unified_range(hunk.new_start, hunk.new_lines));
41        output.push_str(" @@\n");
42        for line in &hunk.lines {
43            let prefix = match line.kind {
44                DiffLineKind::Context => ' ',
45                DiffLineKind::Addition => '+',
46                DiffLineKind::Deletion => '-',
47            };
48            output.push(prefix);
49            output.push_str(trim_line_ending(&line.text));
50            output.push('\n');
51
52            let has_line_terminator = line.text.ends_with('\n') || line.text.ends_with('\r');
53            if options.missing_newline_hint && !has_line_terminator {
54                output.push_str(r"\ No newline at end of file");
55                output.push('\n');
56            }
57        }
58    }
59    output
60}
61
62fn format_unified_range(start: usize, count: usize) -> String {
63    if count == 0 {
64        return format!("{},0", start.saturating_sub(1));
65    }
66    if count == 1 {
67        return start.to_string();
68    }
69    format!("{start},{count}")
70}
71
72/// Formats a git-compatible `@@ -old +new @@` hunk header.
73///
74/// Range counts are preserved rather than collapsed to start-only form: a
75/// pure deletion such as `@@ -65,19 +65,0 @@` must not read as a one-line
76/// change. Counts of one are elided (`@@ -1 +1 @@`) to match `git diff`.
77#[must_use]
78pub fn format_hunk_header(old_start: usize, old_count: usize, new_start: usize, new_count: usize) -> String {
79    format!(
80        "@@ -{} +{} @@",
81        format_unified_range(old_start, old_count),
82        format_unified_range(new_start, new_count)
83    )
84}
85/// Formats a numbered unified diff without ANSI color.
86#[must_use]
87pub fn format_numbered_unified_diff(input: &str) -> Vec<String> {
88    let lines = display_lines_from_unified_diff(input);
89    let width = diff_display_line_number_width(&lines);
90    lines.iter().map(|line| line.numbered_text(width)).collect()
91}
92
93/// Computes the clamped line-number gutter width.
94#[must_use]
95pub fn diff_display_line_number_width(lines: &[DiffDisplayLine]) -> usize {
96    let maximum = lines
97        .iter()
98        .flat_map(|line| [line.old_line, line.new_line])
99        .flatten()
100        .max()
101        .unwrap_or_default();
102    decimal_digits(maximum).clamp(5, 6)
103}
104fn decimal_digits(mut number: u32) -> usize {
105    let mut digits = 1usize;
106    while number >= 10 {
107        number /= 10;
108        digits += 1;
109    }
110    digits
111}