Skip to main content

vtcode_core/tools/file_ops/
diff_preview.rs

1//! Diff preview utilities for file operations.
2
3use crate::config::constants::diff;
4use crate::utils::diff::{DiffOptions, compute_diff_with_theme};
5use serde_json::{Value, json};
6use std::time::Instant;
7use vtcode_commons::diff_preview::count_diff_changes;
8
9/// Create a diff preview response when content exceeds the size limit.
10pub fn diff_preview_size_skip() -> Value {
11    json!({
12        "skipped": true,
13        "reason": "content_exceeds_preview_limit",
14        "max_bytes": diff::MAX_PREVIEW_BYTES
15    })
16}
17
18/// Create a diff preview response when inline diffs are suppressed due to too many changes.
19pub fn diff_preview_suppressed(additions: usize, deletions: usize, line_count: usize) -> Value {
20    json!({
21        "skipped": true,
22        "suppressed": true,
23        "reason": "too_many_changes",
24        "message": diff::SUPPRESSION_MESSAGE,
25        "summary": {
26            "additions": additions,
27            "deletions": deletions,
28            "total_lines": line_count
29        }
30    })
31}
32
33/// Create a diff preview response when an error prevents diff generation.
34pub fn diff_preview_error_skip(reason: &str, detail: Option<&str>) -> Value {
35    match detail {
36        Some(value) => json!({
37            "skipped": true,
38            "reason": reason,
39            "detail": value
40        }),
41        None => json!({
42            "skipped": true,
43            "reason": reason
44        }),
45    }
46}
47
48/// Build a unified diff preview between before and after content.
49pub fn build_diff_preview(path: &str, before: Option<&str>, after: &str) -> Value {
50    let started = Instant::now();
51    let previous = before.unwrap_or("");
52    let old_label = format!("a/{path}");
53    let new_label = format!("b/{path}");
54
55    let diff_bundle = compute_diff_with_theme(
56        previous,
57        after,
58        DiffOptions {
59            context_lines: diff::CONTEXT_RADIUS,
60            old_label: Some(old_label.as_str()),
61            new_label: Some(new_label.as_str()),
62            missing_newline_hint: true,
63        },
64    );
65
66    if diff_bundle.formatted.trim().is_empty() {
67        tracing::debug!(
68            target: "vtcode.tools.diff",
69            path,
70            before_bytes = previous.len(),
71            after_bytes = after.len(),
72            additions = 0,
73            deletions = 0,
74            line_count = 0,
75            truncated = false,
76            suppressed = false,
77            elapsed_ms = started.elapsed().as_millis(),
78            "diff preview generated"
79        );
80
81        return json!({
82            "content": "",
83            "truncated": false,
84            "omitted_line_count": 0,
85            "skipped": false,
86            "is_empty": true
87        });
88    }
89
90    let line_count = diff_bundle.formatted.lines().count();
91    let counts = count_diff_changes(&diff_bundle.hunks);
92    let additions = counts.additions;
93    let deletions = counts.deletions;
94    let total_changes = counts.total();
95
96    if total_changes > diff::MAX_SINGLE_FILE_CHANGES {
97        tracing::debug!(
98            target: "vtcode.tools.diff",
99            path,
100            before_bytes = previous.len(),
101            after_bytes = after.len(),
102            additions,
103            deletions,
104            line_count,
105            truncated = false,
106            suppressed = true,
107            elapsed_ms = started.elapsed().as_millis(),
108            "diff preview suppressed (too many changes)"
109        );
110
111        return diff_preview_suppressed(additions, deletions, line_count);
112    }
113
114    if line_count > diff::MAX_PREVIEW_LINES {
115        let lines: Vec<&str> = diff_bundle.formatted.lines().collect();
116        let head_count = diff::HEAD_LINE_COUNT.min(lines.len());
117        let tail_count = diff::TAIL_LINE_COUNT.min(lines.len().saturating_sub(head_count));
118        let omitted = lines.len().saturating_sub(head_count + tail_count);
119
120        let mut condensed = Vec::with_capacity(head_count + tail_count + 1);
121        condensed.extend(lines[..head_count].iter().copied());
122        if omitted > 0 {
123            condensed.push("");
124        }
125        if tail_count > 0 {
126            let tail_start = lines.len().saturating_sub(tail_count);
127            condensed.extend(lines[tail_start..].iter().copied());
128        }
129
130        let diff_output = if omitted > 0 {
131            let mut result = condensed[..head_count].join("\n");
132            result.push_str(&format!("\n... {omitted} lines omitted ...\n"));
133            result.push_str(&condensed[head_count + 1..].join("\n"));
134            result
135        } else {
136            condensed.join("\n")
137        };
138
139        let elapsed = started.elapsed().as_millis();
140
141        tracing::debug!(
142            target: "vtcode.tools.diff",
143            path,
144            before_bytes = previous.len(),
145            after_bytes = after.len(),
146            additions,
147            deletions,
148            line_count,
149            omitted_lines = omitted,
150            truncated = true,
151            suppressed = false,
152            elapsed_ms = elapsed,
153            "diff preview generated"
154        );
155
156        json!({
157            "content": diff_output,
158            "truncated": true,
159            "omitted_line_count": omitted,
160            "skipped": false
161        })
162    } else {
163        let elapsed = started.elapsed().as_millis();
164
165        tracing::debug!(
166            target: "vtcode.tools.diff",
167            path,
168            before_bytes = previous.len(),
169            after_bytes = after.len(),
170            additions,
171            deletions,
172            line_count,
173            truncated = false,
174            suppressed = false,
175            elapsed_ms = elapsed,
176            "diff preview generated"
177        );
178
179        json!({
180            "content": diff_bundle.formatted,
181            "truncated": false,
182            "omitted_line_count": 0,
183            "skipped": false
184        })
185    }
186}