1use std::collections::VecDeque;
4
5use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
6
7use super::display::{display_lines_from_hunks_with_timeout, for_each_side_by_side_pair};
8use super::format::diff_display_line_number_width;
9use super::types::{
10 DiffCell, DiffDisplayKind, DiffDisplayLine, DiffDocument, DiffLayout, DiffRow, DiffRowKind, DiffSegment,
11 LayoutOptions,
12};
13
14pub(crate) fn layout_document(document: &DiffDocument, options: LayoutOptions) -> Vec<DiffRow> {
15 let display = display_lines_from_hunks_with_timeout(&document.hunks, document.inline_timeout);
16 layout_display_lines(&display, options)
17}
18
19#[must_use]
24pub fn layout_display_lines(display: &[DiffDisplayLine], options: LayoutOptions) -> Vec<DiffRow> {
25 let use_side_by_side = options.layout == DiffLayout::SideBySide && options.width >= options.min_side_by_side_width;
26 let mut rows = RowCollector::new(options.max_rows);
27 if use_side_by_side {
28 layout_side_by_side(display, options, &mut rows);
29 } else {
30 layout_unified(display, options, &mut rows);
31 }
32 rows.finish()
33}
34
35#[must_use]
45pub fn bounded_display_lines(lines: &[DiffDisplayLine], max_rows: usize) -> Vec<DiffDisplayLine> {
46 if max_rows == 0 {
47 return Vec::new();
48 }
49 if lines.len() <= max_rows {
50 return lines.to_vec();
51 }
52
53 let retained = max_rows.saturating_sub(1);
54 let head_count = retained.saturating_add(1) / 2;
55 let tail_count = retained / 2;
56 let omitted = lines.len().saturating_sub(head_count + tail_count);
57
58 let mut bounded = Vec::with_capacity(max_rows);
59 bounded.extend_from_slice(&lines[..head_count]);
60 bounded.push(DiffDisplayLine::body(
61 DiffDisplayKind::Metadata,
62 None,
63 None,
64 format!("... {omitted} lines omitted ..."),
65 ));
66 if tail_count > 0 {
67 bounded.extend_from_slice(&lines[lines.len() - tail_count..]);
68 }
69 bounded
70}
71
72fn layout_unified(lines: &[DiffDisplayLine], options: LayoutOptions, rows: &mut RowCollector) {
73 let gutter_width = diff_display_line_number_width(lines).saturating_add(4);
74 let content_width = options.width.saturating_sub(gutter_width).max(1);
75 let mut hunk_index = None;
76 for line in lines {
77 if line.kind == DiffDisplayKind::Metadata {
78 rows.push(metadata_row(&line.text, hunk_index));
79 continue;
80 }
81 if line.kind == DiffDisplayKind::HunkHeader {
82 hunk_index = Some(hunk_index.map_or(0, |index| index + 1));
83 rows.push(header_row(&line.text, hunk_index));
84 continue;
85 }
86 let marker = marker_for_kind(line.kind);
87 for (continuation, segments) in wrap_segments(&line.text, &line.changed, content_width, options.wrap)
88 .into_iter()
89 .enumerate()
90 {
91 rows.push(DiffRow {
92 kind: row_kind(line.kind),
93 marker,
94 hunk_index,
95 continuation: continuation > 0,
96 left: Some(DiffCell {
97 old_line: (continuation == 0).then_some(line.old_line).flatten(),
98 new_line: (continuation == 0).then_some(line.new_line).flatten(),
99 marker,
100 segments,
101 }),
102 right: None,
103 });
104 }
105 }
106}
107
108fn layout_side_by_side(lines: &[DiffDisplayLine], options: LayoutOptions, rows: &mut RowCollector) {
109 let pane_width = options.width.saturating_sub(1) / 2;
110 let content_width = pane_width.saturating_sub(6).max(1);
111 let mut hunk_index = None;
112 for_each_side_by_side_pair(lines, |left, right| {
113 let is_full_width = right.is_none()
114 && left.is_some_and(|line| matches!(line.kind, DiffDisplayKind::HunkHeader | DiffDisplayKind::Metadata));
115 if is_full_width {
116 let text = left.map_or("", |line| line.text.as_str());
117 if left.is_some_and(|line| line.kind == DiffDisplayKind::HunkHeader) {
118 hunk_index = Some(hunk_index.map_or(0, |index| index + 1));
119 rows.push(header_row(text, hunk_index));
120 } else {
121 rows.push(metadata_row(text, hunk_index));
122 }
123 return;
124 }
125 let left_parts = left.map_or_else(
126 || vec![Vec::new()],
127 |line| wrap_segments(&line.text, &line.changed, content_width, options.wrap),
128 );
129 let right_parts = right.map_or_else(
130 || vec![Vec::new()],
131 |line| wrap_segments(&line.text, &line.changed, content_width, options.wrap),
132 );
133 let count = left_parts.len().max(right_parts.len());
134 for offset in 0..count {
135 let left_cell = left.and_then(|line| {
136 left_parts.get(offset).map(|segments| DiffCell {
137 old_line: (offset == 0).then_some(line.old_line).flatten(),
138 new_line: (offset == 0).then_some(line.new_line).flatten(),
139 marker: marker_for_kind(line.kind),
140 segments: segments.clone(),
141 })
142 });
143 let right_cell = right.and_then(|line| {
144 right_parts.get(offset).map(|segments| DiffCell {
145 old_line: (offset == 0).then_some(line.old_line).flatten(),
146 new_line: (offset == 0).then_some(line.new_line).flatten(),
147 marker: marker_for_kind(line.kind),
148 segments: segments.clone(),
149 })
150 });
151 let kind = right.or(left).map_or(DiffRowKind::Context, |line| row_kind(line.kind));
152 rows.push(DiffRow {
153 kind,
154 marker: right_cell.as_ref().or(left_cell.as_ref()).map_or(' ', |cell| cell.marker),
155 hunk_index,
156 continuation: offset > 0,
157 left: left_cell,
158 right: right_cell,
159 });
160 }
161 });
162}
163
164fn header_row(text: &str, hunk_index: Option<usize>) -> DiffRow {
165 DiffRow {
166 kind: DiffRowKind::HunkHeader,
167 marker: '@',
168 hunk_index,
169 continuation: false,
170 left: Some(DiffCell {
171 old_line: None,
172 new_line: None,
173 marker: '@',
174 segments: vec![DiffSegment { text: text.to_owned(), emphasized: false }],
175 }),
176 right: None,
177 }
178}
179
180fn metadata_row(text: &str, hunk_index: Option<usize>) -> DiffRow {
181 DiffRow {
182 kind: DiffRowKind::Metadata,
183 marker: ' ',
184 hunk_index,
185 continuation: false,
186 left: Some(DiffCell {
187 old_line: None,
188 new_line: None,
189 marker: ' ',
190 segments: vec![DiffSegment { text: text.to_owned(), emphasized: false }],
191 }),
192 right: None,
193 }
194}
195
196struct RowCollector {
197 head: Vec<DiffRow>,
198 tail: VecDeque<DiffRow>,
199 max_rows: usize,
200 total_rows: usize,
201 overflowed: bool,
202}
203
204impl RowCollector {
205 fn new(max_rows: usize) -> Self {
206 Self {
207 head: Vec::new(),
208 tail: VecDeque::new(),
209 max_rows,
210 total_rows: 0,
211 overflowed: false,
212 }
213 }
214
215 fn push(&mut self, row: DiffRow) {
216 self.total_rows = self.total_rows.saturating_add(1);
217 if self.max_rows == 0 {
218 return;
219 }
220 if !self.overflowed {
221 self.head.push(row);
222 if self.head.len() <= self.max_rows {
223 return;
224 }
225
226 let retained = self.max_rows.saturating_sub(1);
227 let head_count = retained.saturating_add(1) / 2;
228 let tail_count = retained / 2;
229 if tail_count > 0 {
230 let tail_start = self.head.len() - tail_count;
231 self.tail = self.head.split_off(tail_start).into();
232 }
233 self.head.truncate(head_count);
234 self.overflowed = true;
235 return;
236 }
237
238 let tail_count = self.max_rows.saturating_sub(1) / 2;
239 if tail_count > 0 {
240 if self.tail.len() == tail_count {
241 let _ = self.tail.pop_front();
242 }
243 self.tail.push_back(row);
244 }
245 }
246
247 fn finish(mut self) -> Vec<DiffRow> {
248 if !self.overflowed {
249 return self.head;
250 }
251 let omitted = self.total_rows.saturating_sub(self.head.len()).saturating_sub(self.tail.len());
252 self.head.push(omission_row(omitted));
253 self.head.extend(self.tail);
254 self.head
255 }
256}
257
258fn omission_row(omitted: usize) -> DiffRow {
259 DiffRow {
260 kind: DiffRowKind::Omission,
261 marker: '…',
262 hunk_index: None,
263 continuation: false,
264 left: Some(DiffCell {
265 old_line: None,
266 new_line: None,
267 marker: '…',
268 segments: vec![DiffSegment {
269 text: format!("{omitted} rows omitted"),
270 emphasized: false,
271 }],
272 }),
273 right: None,
274 }
275}
276
277fn wrap_segments(text: &str, changed: &[(usize, usize)], width: usize, wrap: bool) -> Vec<Vec<DiffSegment>> {
278 if !wrap || UnicodeWidthStr::width(text) <= width {
282 return vec![segment_slice(text, changed, 0, text.len())];
283 }
284 let mut rows = Vec::new();
285 let mut byte_start = 0usize;
286 let mut display_width = 0usize;
287 for (byte, character) in text.char_indices() {
288 let char_width = UnicodeWidthChar::width(character).unwrap_or_default();
289 if display_width > 0 && display_width.saturating_add(char_width) > width {
290 rows.push(segment_slice(text, changed, byte_start, byte));
291 byte_start = byte;
292 display_width = 0;
293 }
294 display_width = display_width.saturating_add(char_width);
295 }
296 rows.push(segment_slice(text, changed, byte_start, text.len()));
297 rows
298}
299
300fn segment_slice(text: &str, changed: &[(usize, usize)], start: usize, end: usize) -> Vec<DiffSegment> {
301 if start == end {
302 return vec![DiffSegment { text: String::new(), emphasized: false }];
303 }
304 let mut boundaries = vec![start, end];
305 for &(range_start, range_end) in changed {
306 if range_start < end && range_end > start {
307 let bounded_start = range_start.max(start).min(end);
308 let bounded_end = range_end.max(start).min(end);
309 if text.is_char_boundary(bounded_start) && text.is_char_boundary(bounded_end) {
310 boundaries.push(bounded_start);
311 boundaries.push(bounded_end);
312 }
313 }
314 }
315 boundaries.sort_unstable();
316 boundaries.dedup();
317 boundaries
318 .windows(2)
319 .filter_map(|pair| {
320 let segment_start = pair[0];
321 let segment_end = pair[1];
322 (segment_start < segment_end).then(|| DiffSegment {
323 text: text[segment_start..segment_end].to_owned(),
324 emphasized: changed
325 .iter()
326 .any(|&(range_start, range_end)| segment_start >= range_start && segment_end <= range_end),
327 })
328 })
329 .collect()
330}
331
332fn marker_for_kind(kind: DiffDisplayKind) -> char {
333 match kind {
334 DiffDisplayKind::Addition => '+',
335 DiffDisplayKind::Deletion => '-',
336 DiffDisplayKind::HunkHeader => '@',
337 DiffDisplayKind::Metadata | DiffDisplayKind::Context => ' ',
338 }
339}
340
341fn row_kind(kind: DiffDisplayKind) -> DiffRowKind {
342 match kind {
343 DiffDisplayKind::Addition => DiffRowKind::Addition,
344 DiffDisplayKind::Deletion => DiffRowKind::Deletion,
345 DiffDisplayKind::HunkHeader => DiffRowKind::HunkHeader,
346 DiffDisplayKind::Metadata => DiffRowKind::Metadata,
347 DiffDisplayKind::Context => DiffRowKind::Context,
348 }
349}