1use std::{num::NonZeroU32, ops::Range};
2
3use crate::model::{
4 Document, FileDiff, Kind, Row, RowKind, SourceLine, alignment_needed, alignment_offset,
5};
6
7mod storage;
8use storage::{TableBuilder, Tables, compact_fits};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11enum BlockKind {
12 Header,
13 Hunk(usize),
14 Gap,
15 Metadata,
16 Context { source_start: usize },
17 Replacement(usize),
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21struct Block {
22 start: [usize; 2],
23 kind: BlockKind,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27struct Replacement {
28 old_start: usize,
29 new_start: usize,
30 end: usize,
31 old_pad: usize,
32 new_pad: usize,
33}
34
35impl Replacement {
36 fn old_range(&self) -> Range<usize> {
37 self.old_start..self.new_start
38 }
39
40 fn new_range(&self) -> Range<usize> {
41 self.new_start..self.end
42 }
43}
44
45const _: () = assert!(size_of::<Replacement>() == 5 * size_of::<usize>());
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48struct SourceRun {
49 start: usize,
50 block: usize,
51 ordinal: usize,
52 old: Option<NonZeroU32>,
53 new: Option<NonZeroU32>,
54 kind: Kind,
55}
56
57#[derive(Debug)]
58struct Marker {
59 logical: usize,
60 display: usize,
61 left: bool,
62 right: bool,
63}
64
65#[derive(Debug, Default)]
66pub struct Projection {
67 tables: Tables,
68 markers: [Box<[Marker]>; 2],
69 hunks: Box<[[usize; 2]]>,
70 len: [usize; 2],
71 source_len: usize,
72}
73
74impl Projection {
75 pub fn new(parsed: &Document, meta: &FileDiff) -> Self {
76 let base = meta.hunks.first().map_or(0, |h| h.lines.start);
77 let end = meta.hunks.last().map_or(base, |h| h.lines.end);
78 Self::build(
79 parsed,
80 meta,
81 base,
82 end,
83 compact_fits(end - base, meta.hunks.len()),
84 )
85 }
86
87 fn build(parsed: &Document, meta: &FileDiff, base: usize, end: usize, compact: bool) -> Self {
88 let mut builder = Builder {
89 tables: TableBuilder::new(compact),
90 len: [0; 2],
91 };
92 builder.structural(BlockKind::Header);
93 if meta.hunks.is_empty() {
94 builder.structural(BlockKind::Metadata);
95 }
96 builder.file(parsed, meta, base);
97 let mut projection = Self {
98 tables: builder.tables.finish(),
99 len: builder.len,
100 source_len: end - base,
101 ..Self::default()
102 };
103 for (layout, split) in [false, true].into_iter().enumerate() {
104 let mut markers: Vec<Marker> = parsed.lines[base..end]
105 .iter()
106 .enumerate()
107 .filter(|(_, line)| line.no_newline)
108 .map(|(id, line)| Marker {
109 logical: projection.logical_source_row(id, split),
110 display: 0,
111 left: line.old.is_some(),
112 right: line.new.is_some(),
113 })
114 .collect();
115 markers.sort_unstable_by_key(|m| m.logical);
116 markers.dedup_by(|next, previous| {
117 if next.logical != previous.logical {
118 return false;
119 }
120 previous.left |= next.left;
121 previous.right |= next.right;
122 true
123 });
124 for (n, marker) in markers.iter_mut().enumerate() {
125 marker.display = marker.logical + n + 1;
126 }
127 projection.markers[layout] = markers.into_boxed_slice();
128 }
129 projection.hunks = (0..projection.tables.block_count())
130 .map(|n| projection.tables.block(n))
131 .filter(|b| matches!(b.kind, BlockKind::Hunk(_)))
132 .map(|b| {
133 [
134 projection.display_row(b.start[0], false),
135 projection.display_row(b.start[1], true),
136 ]
137 })
138 .collect();
139 projection
140 }
141
142 fn run(&self, id: usize) -> SourceRun {
143 self.tables.run_at(id)
144 }
145
146 fn run_len(&self, index: usize) -> usize {
147 self.tables.run_end(index, self.source_len) - self.tables.run(index).start
148 }
149
150 fn side_len(&self, side: &Range<usize>) -> usize {
151 if side.is_empty() {
152 0
153 } else {
154 let r = self.tables.side_run(side.end - 1);
155 self.tables.run(r).ordinal + self.run_len(r)
156 }
157 }
158
159 fn side_source(&self, side: &Range<usize>, ordinal: usize) -> Option<usize> {
160 let r = self.tables.side_run_at(side.clone(), ordinal)?;
161 let run = self.tables.run(r);
162 let delta = ordinal - run.ordinal;
163 (delta < self.run_len(r)).then_some(run.start + delta)
164 }
165
166 pub fn info(&self, id: usize) -> (Kind, Option<u32>, Option<u32>) {
167 let run = self.run(id);
168 let delta = (id - run.start) as u32;
169 (
170 run.kind,
171 run.old.map(|n| n.get() + delta),
172 run.new.map(|n| n.get() + delta),
173 )
174 }
175
176 pub(crate) fn source_intervals(
177 &self,
178 old: bool,
179 ) -> impl Iterator<Item = (Range<usize>, Option<u32>)> + '_ {
180 (0..self.tables.run_count()).map(move |index| {
181 let run = self.tables.run(index);
182 let line = if old { run.old } else { run.new };
183 (
184 run.start..run.start + self.run_len(index),
185 line.map(NonZeroU32::get),
186 )
187 })
188 }
189
190 fn logical_source_row(&self, id: usize, split: bool) -> usize {
191 let run = self.run(id);
192 let block = self.tables.block(run.block);
193 let ordinal = run.ordinal + id - run.start;
194 block.start[usize::from(split)]
195 + match &block.kind {
196 BlockKind::Context { source_start } => id - source_start,
197 BlockKind::Replacement(r) => {
198 let r = self.tables.replacement(*r);
199 match (split, run.kind) {
200 (true, Kind::Deletion) => r.old_pad + ordinal,
201 (true, _) => r.new_pad + ordinal,
202 (false, Kind::Deletion) => ordinal,
203 (false, _) => self.side_len(&r.old_range()) + ordinal,
204 }
205 }
206 _ => unreachable!("A source run must belong to a source block"),
207 }
208 }
209
210 fn display_row(&self, logical: usize, split: bool) -> usize {
211 logical + self.markers[usize::from(split)].partition_point(|m| m.logical < logical)
212 }
213
214 pub fn row_for_source(&self, id: usize, split: bool) -> usize {
215 self.display_row(self.logical_source_row(id, split), split)
216 }
217
218 pub(crate) fn hunk_row(&self, index: usize, split: bool) -> usize {
219 self.hunks[index][usize::from(split)]
220 }
221
222 pub fn rows(&self, split: bool, file: usize, source_base: usize) -> Rows<'_> {
223 Rows {
224 projection: self,
225 split,
226 file,
227 source_base,
228 }
229 }
230
231 pub fn source_run_bytes(&self) -> usize {
232 self.tables.source_run_bytes()
233 }
234 pub fn block_bytes(&self) -> usize {
235 self.tables.block_bytes()
236 }
237 pub fn side_index_bytes(&self) -> usize {
238 self.tables.side_index_bytes()
239 }
240 pub fn marker_bytes(&self) -> usize {
241 self.markers
242 .iter()
243 .map(|m| m.len() * size_of::<Marker>())
244 .sum()
245 }
246 pub fn boundary_bytes(&self) -> usize {
247 self.hunks.len() * size_of::<[usize; 2]>()
248 }
249 pub fn bytes(&self) -> usize {
250 self.source_run_bytes()
251 + self.block_bytes()
252 + self.side_index_bytes()
253 + self.marker_bytes()
254 + self.boundary_bytes()
255 }
256}
257
258pub struct Rows<'a> {
259 projection: &'a Projection,
260 split: bool,
261 file: usize,
262 source_base: usize,
263}
264
265impl Rows<'_> {
266 pub fn len(&self) -> usize {
267 let layout = usize::from(self.split);
268 self.projection.len[layout] + self.projection.markers[layout].len()
269 }
270 pub fn is_empty(&self) -> bool {
271 self.len() == 0
272 }
273
274 pub fn get(&self, index: usize) -> Option<Row> {
275 if index >= self.len() {
276 return None;
277 }
278 let p = self.projection;
279 let layout = usize::from(self.split);
280 let markers = &p.markers[layout];
281 let before = markers.partition_point(|m| m.display <= index);
282 if let Some(marker) = before.checked_sub(1).map(|i| &markers[i])
283 && marker.display == index
284 {
285 return Some(Row {
286 file: self.file,
287 kind: RowKind::NoNewline {
288 left: marker.left,
289 right: marker.right,
290 },
291 });
292 }
293 let logical = index - before;
294 let block = p.tables.block_at(layout, logical);
295 let ordinal = logical - block.start[layout];
296 let kind = match &block.kind {
297 BlockKind::Header => RowKind::Header,
298 BlockKind::Hunk(h) => RowKind::Hunk(*h),
299 BlockKind::Gap => RowKind::Gap,
300 BlockKind::Metadata => RowKind::Metadata,
301 BlockKind::Context { source_start } => RowKind::Code {
302 left: Some(self.source_base + source_start + ordinal),
303 right: Some(self.source_base + source_start + ordinal),
304 },
305 BlockKind::Replacement(r) => {
306 let r = p.tables.replacement(*r);
307 let (left, right) = if self.split {
308 (
309 ordinal
310 .checked_sub(r.old_pad)
311 .and_then(|n| p.side_source(&r.old_range(), n)),
312 ordinal
313 .checked_sub(r.new_pad)
314 .and_then(|n| p.side_source(&r.new_range(), n)),
315 )
316 } else {
317 let old_len = p.side_len(&r.old_range());
318 if ordinal < old_len {
319 (p.side_source(&r.old_range(), ordinal), None)
320 } else {
321 (None, p.side_source(&r.new_range(), ordinal - old_len))
322 }
323 };
324 RowKind::Code {
325 left: left.map(|n| n + self.source_base),
326 right: right.map(|n| n + self.source_base),
327 }
328 }
329 };
330 Some(Row {
331 file: self.file,
332 kind,
333 })
334 }
335
336 pub fn boundary(&self, index: usize, forward: bool, file: bool) -> Option<usize> {
337 if file {
338 return (!forward && index > 0).then_some(0);
339 }
340 let h = &self.projection.hunks;
341 let layout = usize::from(self.split);
342 if forward {
343 h.get(h.partition_point(|b| b[layout] <= index))
344 .map(|b| b[layout])
345 } else {
346 h.partition_point(|b| b[layout] < index)
347 .checked_sub(1)
348 .map(|i| h[i][layout])
349 }
350 }
351
352 pub fn first_boundary(&self, forward: bool, file: bool) -> Option<usize> {
353 if file {
354 return Some(0);
355 }
356 let h = &self.projection.hunks;
357 (if forward { h.first() } else { h.last() }).map(|b| b[usize::from(self.split)])
358 }
359}
360
361struct Builder {
362 tables: TableBuilder,
363 len: [usize; 2],
364}
365
366impl Builder {
367 fn structural(&mut self, kind: BlockKind) {
368 self.tables.push_block(Block {
369 start: self.len,
370 kind,
371 });
372 self.len[0] += 1;
373 self.len[1] += 1;
374 }
375
376 fn source_runs(
377 &mut self,
378 lines: &[SourceLine],
379 base: usize,
380 range: Range<usize>,
381 ) -> Range<usize> {
382 let first = self.tables.run_count();
383 let mut old_ordinal = 0;
384 let mut new_ordinal = 0;
385 for i in range {
386 let line = &lines[i];
387 let local = i - base;
388 if self
389 .tables
390 .run_count()
391 .checked_sub(1)
392 .map(|n| self.tables.run(n))
393 .is_none_or(|r| {
394 let delta = u32::try_from(local - r.start).ok();
395 r.block != self.tables.block_count()
396 || r.kind != line.kind
397 || r.old.and_then(|n| n.get().checked_add(delta?)) != line.old
398 || r.new.and_then(|n| n.get().checked_add(delta?)) != line.new
399 })
400 {
401 self.tables.push_run(SourceRun {
402 start: local,
403 block: self.tables.block_count(),
404 ordinal: if line.kind == Kind::Deletion {
405 old_ordinal
406 } else {
407 new_ordinal
408 },
409 old: line
410 .old
411 .map(|n| NonZeroU32::new(n).expect("Validated source line is positive")),
412 new: line
413 .new
414 .map(|n| NonZeroU32::new(n).expect("Validated source line is positive")),
415 kind: line.kind,
416 });
417 }
418 old_ordinal += usize::from(line.old.is_some());
419 new_ordinal += usize::from(line.new.is_some());
420 }
421 first..self.tables.run_count()
422 }
423
424 fn file(&mut self, parsed: &Document, file: &FileDiff, base: usize) {
425 let (mut old_end, mut new_end) = (1, 1);
426 for (h, hunk) in file.hunks.iter().enumerate() {
427 if hunk.old_start > old_end || hunk.new_start > new_end {
428 self.structural(BlockKind::Gap);
429 }
430 self.structural(BlockKind::Hunk(h));
431 let mut i = hunk.lines.start;
432 while i < hunk.lines.end {
433 let start = i;
434 let context = parsed.lines[i].kind == Kind::Context;
435 while i < hunk.lines.end && (parsed.lines[i].kind == Kind::Context) == context {
436 i += 1;
437 }
438 let runs = self.source_runs(&parsed.lines, base, start..i);
439 let (kind, split_len, unified_len) = if context {
440 (
441 BlockKind::Context {
442 source_start: start - base,
443 },
444 i - start,
445 i - start,
446 )
447 } else {
448 let old_len = parsed.lines[start..i]
449 .iter()
450 .filter(|line| line.kind == Kind::Deletion)
451 .count();
452 let new_len = i - start - old_len;
453 let offset = if alignment_needed(old_len, new_len) {
454 let old: Vec<_> = (start..i)
455 .filter(|&id| parsed.lines[id].kind == Kind::Deletion)
456 .collect();
457 let new: Vec<_> = (start..i)
458 .filter(|&id| parsed.lines[id].kind == Kind::Addition)
459 .collect();
460 alignment_offset(&parsed.lines, &old, &new)
461 } else {
462 0
463 };
464 let old_pad = if new_len > old_len { offset } else { 0 };
465 let new_pad = if old_len > new_len { offset } else { 0 };
466 let side_start = self.tables.side_count();
467 for r in runs.clone() {
468 if self.tables.run(r).kind == Kind::Deletion {
469 self.tables.push_side(r);
470 }
471 }
472 let middle = self.tables.side_count();
473 for r in runs {
474 if self.tables.run(r).kind == Kind::Addition {
475 self.tables.push_side(r);
476 }
477 }
478 let replacement = self.tables.replacement_count();
479 self.tables.push_replacement(Replacement {
480 old_start: side_start,
481 new_start: middle,
482 end: self.tables.side_count(),
483 old_pad,
484 new_pad,
485 });
486 (
487 BlockKind::Replacement(replacement),
488 (old_len + old_pad).max(new_len + new_pad),
489 old_len + new_len,
490 )
491 };
492 self.tables.push_block(Block {
493 start: self.len,
494 kind,
495 });
496 self.len[0] += unified_len;
497 self.len[1] += split_len;
498 }
499 for line in &parsed.lines[hunk.lines.clone()] {
500 if let Some(n) = line.old {
501 old_end = n.saturating_add(1);
502 }
503 if let Some(n) = line.new {
504 new_end = n.saturating_add(1);
505 }
506 }
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::Projection;
514 use crate::model::{Document, RowKind};
515
516 #[test]
517 fn both_storage_forms_preserve_parsed_views_and_wide_document_bases() {
518 let patch = concat!(
519 "diff --git a/review b/review\n--- a/review\n+++ b/review\n",
520 "@@ -2,4 +2,5 @@\n head\n-let retries = 3;\n+// inserted\n+let retries = 5;\n-let delay = 10;\n+let delay = 20;\n tail\n",
521 "@@ -20,5 +21,3 @@\n start\n-obsolete_one();\n-obsolete_two();\n-let retries = 3;\n+let retries = 5;\n end\n",
522 "@@ -60,0 +62,2 @@\n+first addition\n+second addition\n",
523 "@@ -80,2 +84,0 @@\n-first deletion\n-second deletion\n",
524 "@@ -100,2 +102,2 @@\n-old one\n-old two\n+new one\n+new two\n",
525 "@@ -120,2 +121,3 @@\n shared\n-old end\n\\ No newline at end of file\n+inserted\n+new end\n\\ No newline at end of file\n",
526 ).as_bytes();
527 let parsed = Document::parse(patch).unwrap();
528 let file = &parsed.files[0];
529 let compact = Projection::new(&parsed, file);
530 let wide = Projection::build(&parsed, file, 0, parsed.lines.len(), false);
531 assert!(compact.bytes() < wide.bytes());
532 assert_eq!(compact.marker_bytes(), wide.marker_bytes());
533 assert_eq!(compact.boundary_bytes(), wide.boundary_bytes());
534 let base = usize::try_from(u64::from(u32::MAX) + 17).unwrap_or(17);
535 for projection in [&compact, &wide] {
536 assert_eq!(projection.tables.replacement(0).old_pad, 1);
537 assert_eq!(projection.tables.replacement(1).new_pad, 2);
538 assert!(projection.tables.replacement(2).old_range().is_empty());
539 assert!(projection.tables.replacement(3).new_range().is_empty());
540 for (id, line) in parsed.lines.iter().enumerate() {
541 assert_eq!(projection.info(id), (line.kind, line.old, line.new));
542 }
543 for split in [false, true] {
544 let rows = projection.rows(split, 7, base);
545 let (expected, inverse) = if split {
546 (&parsed.split, &parsed.split_locations)
547 } else {
548 (&parsed.unified, &parsed.unified_locations)
549 };
550 assert_eq!(rows.len(), expected.len());
551 assert!(rows.get(expected.len()).is_none());
552 for (n, expected) in expected.iter().enumerate() {
553 let actual = rows.get(n).unwrap();
554 let kind = match expected.kind {
555 RowKind::Code { left, right } => RowKind::Code {
556 left: left.map(|id| base + id),
557 right: right.map(|id| base + id),
558 },
559 kind => kind,
560 };
561 assert_eq!(actual.file, 7);
562 assert_eq!(actual.kind, kind);
563 }
564 for (id, &row) in inverse.iter().enumerate() {
565 assert_eq!(projection.row_for_source(id, split), row);
566 }
567 for file_boundary in [false, true] {
568 let boundaries: Vec<_> = expected
569 .iter()
570 .enumerate()
571 .filter_map(|(n, row)| {
572 (if file_boundary {
573 matches!(row.kind, RowKind::Header)
574 } else {
575 matches!(row.kind, RowKind::Hunk(_))
576 })
577 .then_some(n)
578 })
579 .collect();
580 for forward in [false, true] {
581 assert_eq!(
582 rows.first_boundary(forward, file_boundary),
583 if forward {
584 boundaries.first().copied()
585 } else {
586 boundaries.last().copied()
587 }
588 );
589 for n in 0..rows.len() {
590 let expected = if forward {
591 boundaries.iter().copied().find(|&b| b > n)
592 } else {
593 boundaries.iter().rev().copied().find(|&b| b < n)
594 };
595 assert_eq!(rows.boundary(n, forward, file_boundary), expected);
596 }
597 }
598 }
599 }
600 }
601 }
602
603 #[test]
604 fn source_runs_preserve_line_number_endpoints_and_absent_sides() {
605 let patch = b"diff --git a/numbers b/numbers\n--- a/numbers\n+++ b/numbers\n@@ -1,4 +1,4 @@\n a\n-b\n-c\n+d\n+e\n f\n";
606 for first in [1, u32::MAX - 3] {
607 let mut parsed = Document::parse_file(patch).unwrap();
608 for line in &mut parsed.lines {
609 line.old = line.old.map(|n| first + (n - 1));
610 line.new = line.new.map(|n| first + (n - 1));
611 }
612 let file = &mut parsed.files[0];
613 file.hunks[0].old_start = first;
614 file.hunks[0].new_start = first;
615 let projection = Projection::new(&parsed, &parsed.files[0]);
616 for (id, line) in parsed.lines.iter().enumerate() {
617 assert_eq!(projection.info(id), (line.kind, line.old, line.new));
618 }
619 for (split, expected) in [
620 (
621 true,
622 &[
623 (Some(0), Some(0)),
624 (Some(1), Some(3)),
625 (Some(2), Some(4)),
626 (Some(5), Some(5)),
627 ][..],
628 ),
629 (
630 false,
631 &[
632 (Some(0), Some(0)),
633 (Some(1), None),
634 (Some(2), None),
635 (None, Some(3)),
636 (None, Some(4)),
637 (Some(5), Some(5)),
638 ][..],
639 ),
640 ] {
641 let rows = projection.rows(split, 0, 0);
642 let start = 2 + usize::from(first > 1);
643 assert_eq!(rows.len(), start + expected.len());
644 for (offset, &(left, right)) in expected.iter().enumerate() {
645 let row = start + offset;
646 assert_eq!(rows.get(row).unwrap().kind, RowKind::Code { left, right });
647 for id in left.into_iter().chain(right) {
648 assert_eq!(projection.row_for_source(id, split), row);
649 }
650 }
651 }
652 }
653 }
654}