vtcode_core/tools/file_ops/
diff_preview.rs1use 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
9pub 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
18pub 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
33pub 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
48pub 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}