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