Skip to main content

diffs_rs/
projection.rs

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}