Skip to main content

vtcode_diff/
display.rs

1//! Semantic display-line extraction, side-by-side pairing, and line counts.
2
3use std::time::Duration;
4
5use super::format::format_hunk_header;
6use super::intraline::{annotate_word_level_diffs, annotate_word_level_diffs_with_timeout};
7use super::parse::{parse_hunk_range, parse_hunk_starts, parse_omitted_line_count, trim_line_ending};
8use super::types::{
9    DiffChangeCounts, DiffDisplayKind, DiffDisplayLine, DiffHunk, DiffLineKind, SideBySideRow, default_inline_timeout,
10};
11
12/// Counts additions and deletions in structured hunks.
13#[must_use]
14pub fn count_diff_changes(hunks: &[DiffHunk]) -> DiffChangeCounts {
15    let mut counts = DiffChangeCounts::default();
16    for line in hunks.iter().flat_map(|hunk| &hunk.lines) {
17        match line.kind {
18            DiffLineKind::Addition => counts.additions += 1,
19            DiffLineKind::Deletion => counts.deletions += 1,
20            DiffLineKind::Context => {}
21        }
22    }
23    counts
24}
25/// Converts hunks to semantic display lines and bounded intraline ranges.
26#[must_use]
27pub fn display_lines_from_hunks(hunks: &[DiffHunk]) -> Vec<DiffDisplayLine> {
28    display_lines_from_hunks_with_timeout(hunks, default_inline_timeout())
29}
30
31pub(crate) fn display_lines_from_hunks_with_timeout(
32    hunks: &[DiffHunk],
33    inline_timeout: Duration,
34) -> Vec<DiffDisplayLine> {
35    let mut output = Vec::new();
36    for hunk in hunks {
37        output.push(DiffDisplayLine::body(
38            DiffDisplayKind::HunkHeader,
39            None,
40            None,
41            format_hunk_header(hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines),
42        ));
43        output.extend(hunk.lines.iter().map(|line| {
44            let kind = match line.kind {
45                DiffLineKind::Context => DiffDisplayKind::Context,
46                DiffLineKind::Addition => DiffDisplayKind::Addition,
47                DiffLineKind::Deletion => DiffDisplayKind::Deletion,
48            };
49            DiffDisplayLine::body(kind, line.old_line, line.new_line, trim_line_ending(&line.text).to_owned())
50        }));
51    }
52    annotate_word_level_diffs_with_timeout(&mut output, inline_timeout);
53    output
54}
55
56/// Parses unified text into display lines, retaining metadata lines.
57#[must_use]
58pub fn display_lines_from_unified_diff(input: &str) -> Vec<DiffDisplayLine> {
59    let mut output = Vec::new();
60    let mut old_line = 0u32;
61    let mut new_line = 0u32;
62    let mut in_hunk = false;
63    let mut remaining_old = 0usize;
64    let mut remaining_new = 0usize;
65    let mut omission_in_hunk = false;
66    let mut hunk_old_start = 0u32;
67    let mut hunk_new_start = 0u32;
68    let mut hunk_old_count = 0usize;
69    let mut hunk_new_count = 0usize;
70    let mut omitted_tail = Vec::new();
71    for raw in input.lines() {
72        if let Some((old_start, new_start)) = parse_hunk_starts(raw) {
73            assign_omitted_tail_line_numbers(
74                &mut output,
75                &omitted_tail,
76                hunk_old_start,
77                hunk_old_count,
78                hunk_new_start,
79                hunk_new_count,
80            );
81            omitted_tail.clear();
82            old_line = old_start;
83            new_line = new_start;
84            if let Some((_, old_count, _, new_count)) = parse_hunk_range(raw) {
85                remaining_old = old_count;
86                remaining_new = new_count;
87                hunk_old_count = old_count;
88                hunk_new_count = new_count;
89            }
90            hunk_old_start = old_start;
91            hunk_new_start = new_start;
92            in_hunk = true;
93            omission_in_hunk = false;
94            // Preserve the authored header verbatim so range counts survive.
95            // Collapsing to `@@ -65 +65 @@` made a pure deletion read as a
96            // one-line change.
97            output.push(DiffDisplayLine::body(DiffDisplayKind::HunkHeader, None, None, raw.to_owned()));
98        } else if in_hunk && (omission_in_hunk || remaining_new > 0) && raw.starts_with('+') {
99            let output_index = output.len();
100            output.push(DiffDisplayLine::body(
101                DiffDisplayKind::Addition,
102                None,
103                (!omission_in_hunk).then_some(new_line),
104                raw[1..].to_owned(),
105            ));
106            if omission_in_hunk {
107                omitted_tail.push(output_index);
108            }
109            new_line = new_line.saturating_add(1);
110            remaining_new = remaining_new.saturating_sub(1);
111        } else if in_hunk && (omission_in_hunk || remaining_old > 0) && raw.starts_with('-') {
112            let output_index = output.len();
113            output.push(DiffDisplayLine::body(
114                DiffDisplayKind::Deletion,
115                (!omission_in_hunk).then_some(old_line),
116                None,
117                raw[1..].to_owned(),
118            ));
119            if omission_in_hunk {
120                omitted_tail.push(output_index);
121            }
122            old_line = old_line.saturating_add(1);
123            remaining_old = remaining_old.saturating_sub(1);
124        } else if in_hunk && (omission_in_hunk || (remaining_old > 0 && remaining_new > 0)) && raw.starts_with(' ') {
125            let output_index = output.len();
126            output.push(DiffDisplayLine::body(
127                DiffDisplayKind::Context,
128                (!omission_in_hunk).then_some(old_line),
129                (!omission_in_hunk).then_some(new_line),
130                raw[1..].to_owned(),
131            ));
132            if omission_in_hunk {
133                omitted_tail.push(output_index);
134            }
135            old_line = old_line.saturating_add(1);
136            new_line = new_line.saturating_add(1);
137            remaining_old = remaining_old.saturating_sub(1);
138            remaining_new = remaining_new.saturating_sub(1);
139        } else if in_hunk && let Some(omitted) = parse_omitted_line_count(raw) {
140            output.push(DiffDisplayLine::body(DiffDisplayKind::Metadata, None, None, raw.to_owned()));
141            omission_in_hunk = true;
142            let omitted = omitted.min(remaining_old).min(remaining_new);
143            old_line = old_line.saturating_add(u32::try_from(omitted).unwrap_or(u32::MAX));
144            new_line = new_line.saturating_add(u32::try_from(omitted).unwrap_or(u32::MAX));
145            remaining_old = remaining_old.saturating_sub(omitted);
146            remaining_new = remaining_new.saturating_sub(omitted);
147        } else {
148            output.push(DiffDisplayLine::body(DiffDisplayKind::Metadata, None, None, raw.to_owned()));
149        }
150    }
151    assign_omitted_tail_line_numbers(
152        &mut output,
153        &omitted_tail,
154        hunk_old_start,
155        hunk_old_count,
156        hunk_new_start,
157        hunk_new_count,
158    );
159    annotate_word_level_diffs(&mut output);
160    output
161}
162
163fn assign_omitted_tail_line_numbers(
164    lines: &mut [DiffDisplayLine],
165    tail: &[usize],
166    old_start: u32,
167    old_count: usize,
168    new_start: u32,
169    new_count: usize,
170) {
171    if tail.is_empty() {
172        return;
173    }
174    let old_tail_count = tail
175        .iter()
176        .filter(|&&index| lines[index].kind != DiffDisplayKind::Addition)
177        .count();
178    let new_tail_count = tail
179        .iter()
180        .filter(|&&index| lines[index].kind != DiffDisplayKind::Deletion)
181        .count();
182    let mut old_line = old_start
183        .saturating_add(u32::try_from(old_count).unwrap_or(u32::MAX))
184        .saturating_sub(u32::try_from(old_tail_count).unwrap_or(u32::MAX));
185    let mut new_line = new_start
186        .saturating_add(u32::try_from(new_count).unwrap_or(u32::MAX))
187        .saturating_sub(u32::try_from(new_tail_count).unwrap_or(u32::MAX));
188    for &index in tail {
189        match lines[index].kind {
190            DiffDisplayKind::Addition => {
191                lines[index].new_line = Some(new_line);
192                new_line = new_line.saturating_add(1);
193            }
194            DiffDisplayKind::Deletion => {
195                lines[index].old_line = Some(old_line);
196                old_line = old_line.saturating_add(1);
197            }
198            DiffDisplayKind::Context => {
199                lines[index].old_line = Some(old_line);
200                lines[index].new_line = Some(new_line);
201                old_line = old_line.saturating_add(1);
202                new_line = new_line.saturating_add(1);
203            }
204            DiffDisplayKind::Metadata | DiffDisplayKind::HunkHeader => {}
205        }
206    }
207}
208/// Pairs deletions and additions for side-by-side presentation.
209#[must_use]
210pub fn side_by_side_rows(lines: &[DiffDisplayLine]) -> Vec<SideBySideRow> {
211    let mut rows = Vec::new();
212    for_each_side_by_side_pair(lines, |left, right| {
213        rows.push(SideBySideRow { left: left.cloned(), right: right.cloned() });
214    });
215    rows
216}
217
218pub(crate) fn for_each_side_by_side_pair<'a, F>(lines: &'a [DiffDisplayLine], mut visit: F)
219where
220    F: FnMut(Option<&'a DiffDisplayLine>, Option<&'a DiffDisplayLine>),
221{
222    let mut index = 0usize;
223    while index < lines.len() {
224        match lines[index].kind {
225            DiffDisplayKind::HunkHeader | DiffDisplayKind::Metadata => {
226                visit(Some(&lines[index]), None);
227                index += 1;
228            }
229            DiffDisplayKind::Context => {
230                let line = &lines[index];
231                visit(Some(line), Some(line));
232                index += 1;
233            }
234            DiffDisplayKind::Deletion => {
235                let delete_start = index;
236                while index < lines.len() && lines[index].kind == DiffDisplayKind::Deletion {
237                    index += 1;
238                }
239                let insert_start = index;
240                while index < lines.len() && lines[index].kind == DiffDisplayKind::Addition {
241                    index += 1;
242                }
243                let delete_count = insert_start - delete_start;
244                let insert_count = index - insert_start;
245                for offset in 0..delete_count.max(insert_count) {
246                    visit(
247                        (offset < delete_count).then(|| &lines[delete_start + offset]),
248                        (offset < insert_count).then(|| &lines[insert_start + offset]),
249                    );
250                }
251            }
252            DiffDisplayKind::Addition => {
253                visit(None, Some(&lines[index]));
254                index += 1;
255            }
256        }
257    }
258}