Skip to main content

diffs_rs/
view.rs

1use std::{collections::BTreeMap, num::NonZeroU32, ops::Range};
2
3use anyhow::{Context, Result};
4
5use crate::{
6    model::{Row, RowKind},
7    source::{CheckedPair, Gap},
8    storage::{Document, FileIndex},
9};
10
11mod reading;
12pub(crate) use reading::SourceOrder;
13
14/// A stable source destination, independent of layout and context expansion.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[cfg_attr(feature = "delivery-trace", derive(serde::Serialize))]
17#[cfg_attr(feature = "delivery-trace", serde(untagged))]
18pub enum LineId {
19    Patch(usize),
20    Context {
21        file: usize,
22        old: NonZeroU32,
23        new: NonZeroU32,
24    },
25}
26
27impl From<usize> for LineId {
28    fn from(id: usize) -> Self {
29        Self::Patch(id)
30    }
31}
32
33impl LineId {
34    /// Checks positive coordinates; source correspondence is checked separately.
35    pub fn context(file: usize, old: u32, new: u32) -> Option<Self> {
36        Some(Self::Context {
37            file,
38            old: NonZeroU32::new(old)?,
39            new: NonZeroU32::new(new)?,
40        })
41    }
42
43    pub(crate) fn patch(self) -> Option<usize> {
44        match self {
45            Self::Patch(id) => Some(id),
46            Self::Context { .. } => None,
47        }
48    }
49
50    pub(crate) fn belongs_to(self, file: &crate::storage::FileIndex) -> bool {
51        match self {
52            Self::Patch(id) => (file.source_base..file.source_end()).contains(&id),
53            Self::Context { file: id, .. } => id == file.id,
54        }
55    }
56}
57
58#[derive(Clone, Copy)]
59enum Part {
60    Patch,
61    Hidden(usize),
62    Context { gap: usize, offset: u32 },
63    NoNewline,
64}
65
66struct Segment {
67    start: usize,
68    patch: usize,
69    part: Part,
70}
71
72#[derive(Default)]
73struct Rows {
74    segments: Vec<Segment>,
75    len: usize,
76    hunks: Vec<usize>,
77}
78
79impl Rows {
80    fn push(&mut self, patch: usize, len: usize, part: Part) -> Result<()> {
81        if len != 0 {
82            self.segments.push(Segment {
83                start: self.len,
84                patch,
85                part,
86            });
87            self.len = self
88                .len
89                .checked_add(len)
90                .context("Expanded row count overflow")?;
91        }
92        Ok(())
93    }
94
95    fn segment(&self, row: usize) -> Option<&Segment> {
96        if row >= self.len {
97            return None;
98        }
99        self.segments.get(
100            self.segments
101                .partition_point(|segment| segment.start <= row)
102                .checked_sub(1)?,
103        )
104    }
105
106    fn patch_row(&self, row: usize) -> usize {
107        let index = self
108            .segments
109            .partition_point(|segment| segment.patch <= row)
110            .saturating_sub(1);
111        let segment = &self.segments[index];
112        segment.start
113            + if matches!(segment.part, Part::Patch) {
114                row - segment.patch
115            } else {
116                0
117            }
118    }
119}
120
121struct Expansion {
122    source: Gap,
123    revealed: u32,
124}
125
126impl Expansion {
127    fn leading(&self) -> bool {
128        self.source.before == Some(0)
129    }
130    fn exposed(&self) -> Range<u32> {
131        if self.leading() {
132            self.source.len - self.revealed..self.source.len
133        } else {
134            0..self.revealed
135        }
136    }
137    fn line(&self, file: usize, offset: u32) -> LineId {
138        LineId::context(
139            file,
140            self.source.old + offset + 1,
141            self.source.new + offset + 1,
142        )
143        .expect("Validated source line is positive")
144    }
145}
146
147struct FileView {
148    gaps: Vec<Expansion>,
149    rows: [Rows; 2],
150}
151
152impl FileView {
153    fn insertion(file: &FileIndex, gap: &Gap, split: bool) -> (usize, usize) {
154        let end = gap
155            .before
156            .map_or_else(|| file.rows(split).len(), |h| file.hunk_row(h, split));
157        let replaces = end.checked_sub(1).is_some_and(|row| {
158            file.rows(split)
159                .get(row)
160                .is_some_and(|row| row.kind == RowKind::Gap)
161        });
162        (end - usize::from(replaces), end)
163    }
164
165    fn build(&self, file: &FileIndex) -> Result<[Rows; 2]> {
166        let mut layouts = [Rows::default(), Rows::default()];
167        for (layout, split) in [false, true].into_iter().enumerate() {
168            let rows = &mut layouts[layout];
169            let mut next = 0;
170            for (index, gap) in self.gaps.iter().enumerate() {
171                let (start, end) = Self::insertion(file, &gap.source, split);
172                rows.push(
173                    next,
174                    start.checked_sub(next).context("Context gaps overlap")?,
175                    Part::Patch,
176                )?;
177                let hidden = gap.source.len - gap.revealed;
178                if gap.leading() && hidden != 0 {
179                    rows.push(start, 1, Part::Hidden(index))?;
180                }
181                rows.push(
182                    start,
183                    gap.revealed as usize,
184                    Part::Context {
185                        gap: index,
186                        offset: gap.exposed().start,
187                    },
188                )?;
189                if !gap.leading() && hidden != 0 {
190                    rows.push(start, 1, Part::Hidden(index))?;
191                }
192                if gap.source.no_newline && hidden == 0 {
193                    rows.push(start, 1, Part::NoNewline)?;
194                }
195                next = end;
196            }
197            rows.push(next, file.rows(split).len() - next, Part::Patch)?;
198            rows.hunks = (0..file.hunks.len())
199                .map(|h| rows.patch_row(file.hunk_row(h, split)))
200                .collect();
201        }
202        Ok(layouts)
203    }
204}
205
206#[derive(Default)]
207pub(crate) struct View {
208    files: BTreeMap<usize, FileView>,
209    extra: Vec<[usize; 2]>,
210    source_revision: reading::Revision,
211}
212
213impl View {
214    #[cfg(feature = "owner-census")]
215    pub(crate) fn census(&self) -> serde_json::Value {
216        let mut ranges = 0;
217        let mut exposed = 0u64;
218        let mut segments = 0;
219        let mut allocations = self.extra.capacity() * size_of::<[usize; 2]>();
220        for file in self.files.values() {
221            ranges += file.gaps.len();
222            exposed += file
223                .gaps
224                .iter()
225                .map(|gap| u64::from(gap.revealed))
226                .sum::<u64>();
227            allocations += file.gaps.capacity() * size_of::<Expansion>();
228            for rows in &file.rows {
229                segments += rows.segments.len();
230                allocations += rows.segments.capacity() * size_of::<Segment>()
231                    + rows.hunks.capacity() * size_of::<usize>();
232            }
233        }
234        serde_json::json!({
235            "files": self.files.len(), "gap_ranges": ranges, "exposed_lines": exposed,
236            "layout_segments": segments, "vector_capacity_bytes": allocations,
237            "inline_file_values_bytes": self.files.len() * size_of::<FileView>(),
238            "scope": "View vectors and occupied file values only; excludes map-node overhead, source descriptors, caches, and allocator overhead",
239        })
240    }
241
242    pub(crate) fn row(&self, doc: &Document, row: usize, split: bool) -> Option<Row<LineId>> {
243        let file = self.file_at(doc, row, split)?;
244        let local = row - self.file_row(doc, file.id, split);
245        let Some(view) = self.files.get(&file.id) else {
246            return file
247                .rows(split)
248                .get(local)
249                .map(|row| row.map(LineId::Patch));
250        };
251        let segment = view.rows[usize::from(split)].segment(local)?;
252        let kind = match segment.part {
253            Part::Patch => {
254                return file
255                    .rows(split)
256                    .get(segment.patch + local - segment.start)
257                    .map(|row| row.map(LineId::Patch));
258            }
259            Part::Hidden(_) => RowKind::Gap,
260            Part::Context { gap, offset } => {
261                let id = view.gaps[gap].line(file.id, offset + (local - segment.start) as u32);
262                RowKind::Code {
263                    left: Some(id),
264                    right: Some(id),
265                }
266            }
267            Part::NoNewline => RowKind::NoNewline {
268                left: true,
269                right: true,
270            },
271        };
272        Some(Row {
273            file: file.id,
274            kind,
275        })
276    }
277
278    pub(crate) fn len(&self, doc: &Document, split: bool) -> usize {
279        doc.row_len(split)
280            + self
281                .extra
282                .last()
283                .map_or(0, |extra| extra[usize::from(split)])
284    }
285
286    pub(crate) fn source_row(&self, doc: &Document, id: LineId, split: bool) -> Option<usize> {
287        match id {
288            LineId::Patch(id) => Some(self.patch_row(doc, doc.row_for_source(id, split), split)),
289            LineId::Context { file, old, new } => {
290                let (old, new) = (old.get(), new.get());
291                let view = self.files.get(&file)?;
292                let gap = view
293                    .gaps
294                    .partition_point(|gap| gap.source.old < old)
295                    .checked_sub(1)?;
296                let source = &view.gaps[gap];
297                let offset = old - source.source.old - 1;
298                if !source.exposed().contains(&offset)
299                    || new.checked_sub(source.source.new) != Some(offset + 1)
300                {
301                    return None;
302                }
303                let (slot, _) = FileView::insertion(&doc.files[file], &source.source, split);
304                let segments = &view.rows[usize::from(split)].segments;
305                let first = segments.partition_point(|segment| segment.patch < slot);
306                let segment = segments[first..].iter().take_while(|segment| segment.patch == slot).find(|segment|
307                    matches!(segment.part, Part::Context { gap: index, .. } if index == gap))?;
308                Some(
309                    self.file_row(doc, file, split)
310                        + segment.start
311                        + (offset - source.exposed().start) as usize,
312                )
313            }
314        }
315    }
316
317    pub(crate) fn file_row(&self, doc: &Document, file: usize, split: bool) -> usize {
318        doc.files[file].row_base(split)
319            + file
320                .checked_sub(1)
321                .map_or(0, |index| self.extra_at(index, split))
322    }
323
324    pub(crate) fn boundary(
325        &self,
326        doc: &Document,
327        row: usize,
328        split: bool,
329        forward: bool,
330    ) -> Option<usize> {
331        if self.files.is_empty() {
332            return doc.boundary(row, split, forward, false);
333        }
334        let first = self.file_at(doc, row, split)?.id;
335        let mut index = first;
336        loop {
337            let file = &doc.files[index];
338            let base = self.file_row(doc, index, split);
339            let found = if let Some(view) = self.files.get(&index) {
340                let hunks = &view.rows[usize::from(split)].hunks;
341                if index != first {
342                    if forward {
343                        hunks.first().copied()
344                    } else {
345                        hunks.last().copied()
346                    }
347                } else if forward {
348                    hunks
349                        .get(hunks.partition_point(|h| base + *h <= row))
350                        .copied()
351                } else {
352                    hunks
353                        .partition_point(|h| base + *h < row)
354                        .checked_sub(1)
355                        .map(|i| hunks[i])
356                }
357            } else if index == first {
358                file.rows(split).boundary(row - base, forward, false)
359            } else {
360                file.rows(split).first_boundary(forward, false)
361            };
362            if let Some(local) = found {
363                return Some(base + local);
364            }
365            index = if forward {
366                index.checked_add(1).filter(|i| *i < doc.files.len())?
367            } else {
368                index.checked_sub(1)?
369            };
370        }
371    }
372
373    fn extra_at(&self, file: usize, split: bool) -> usize {
374        self.extra
375            .get(file)
376            .or_else(|| self.extra.last())
377            .map_or(0, |extra| extra[usize::from(split)])
378    }
379
380    fn file_at<'a>(&self, doc: &'a Document, row: usize, split: bool) -> Option<&'a FileIndex> {
381        doc.files
382            .get(
383                doc.files.partition_point(|file| {
384                    file.row_end(split) + self.extra_at(file.id, split) <= row
385                }),
386            )
387            .map(|f| f.as_ref())
388    }
389
390    pub(crate) fn patch_row(&self, doc: &Document, row: usize, split: bool) -> usize {
391        let Some(file) = doc
392            .files
393            .get(doc.files.partition_point(|f| f.row_end(split) <= row))
394        else {
395            return row;
396        };
397        let local = row - file.row_base(split);
398        self.file_row(doc, file.id, split)
399            + self
400                .files
401                .get(&file.id)
402                .map_or(local, |view| view.rows[usize::from(split)].patch_row(local))
403    }
404
405    pub(crate) fn original_row(&self, doc: &Document, row: usize, split: bool) -> usize {
406        let Some(file) = self.file_at(doc, row, split) else {
407            return row;
408        };
409        let local = row - self.file_row(doc, file.id, split);
410        let original = self
411            .files
412            .get(&file.id)
413            .and_then(|view| view.rows[usize::from(split)].segment(local))
414            .map_or(local, |segment| {
415                segment.patch
416                    + if matches!(segment.part, Part::Patch) {
417                        local - segment.start
418                    } else {
419                        0
420                    }
421            });
422        file.row_base(split) + original.min(file.rows(split).len().saturating_sub(1))
423    }
424
425    pub(crate) fn set(
426        &mut self,
427        doc: &Document,
428        file: usize,
429        pair: Option<&CheckedPair>,
430    ) -> Result<()> {
431        let view = pair
432            .filter(|pair| !pair.gaps.is_empty())
433            .map(|pair| {
434                let mut view = FileView {
435                    gaps: pair
436                        .gaps
437                        .iter()
438                        .map(|gap| Expansion {
439                            source: *gap,
440                            revealed: 0,
441                        })
442                        .collect(),
443                    rows: [Rows::default(), Rows::default()],
444                };
445                view.rows = view.build(&doc.files[file])?;
446                Ok::<_, anyhow::Error>(view)
447            })
448            .transpose()?;
449        self.install(doc, file, view)?;
450        self.source_revision.invalidate();
451        Ok(())
452    }
453
454    fn install(&mut self, doc: &Document, file: usize, view: Option<FileView>) -> Result<()> {
455        let old: [usize; 2] = std::array::from_fn(|layout| {
456            self.files.get(&file).map_or(0, |view| {
457                view.rows[layout].len - doc.files[file].rows(layout == 1).len()
458            })
459        });
460        let new: [usize; 2] = std::array::from_fn(|layout| {
461            view.as_ref().map_or(0, |view| {
462                view.rows[layout].len - doc.files[file].rows(layout == 1).len()
463            })
464        });
465        if self.extra.is_empty() && view.is_none() {
466            return Ok(());
467        }
468        self.extra.resize(
469            doc.files.len(),
470            self.extra.last().copied().unwrap_or([0; 2]),
471        );
472        for layout in 0..2 {
473            doc.row_len(layout == 1)
474                .checked_add(self.extra.last().map_or(0, |extra| extra[layout]) - old[layout])
475                .and_then(|n| n.checked_add(new[layout]))
476                .context("Expanded document row count overflow")?;
477        }
478        for extra in &mut self.extra[file..] {
479            for layout in 0..2 {
480                extra[layout] = extra[layout] - old[layout] + new[layout];
481            }
482        }
483        if let Some(view) = view {
484            self.files.insert(file, view);
485        } else {
486            self.files.remove(&file);
487        }
488        Ok(())
489    }
490
491    pub(crate) fn gap(
492        &self,
493        doc: &Document,
494        row: usize,
495        split: bool,
496    ) -> Option<(usize, usize, u32)> {
497        let file = self.file_at(doc, row, split)?;
498        let view = self.files.get(&file.id)?;
499        let segment =
500            view.rows[usize::from(split)].segment(row - self.file_row(doc, file.id, split))?;
501        let Part::Hidden(gap) = segment.part else {
502            return None;
503        };
504        Some((
505            file.id,
506            gap,
507            view.gaps[gap].source.len - view.gaps[gap].revealed,
508        ))
509    }
510
511    pub(crate) fn expand(&mut self, doc: &Document, file: usize, gap: usize) -> Result<LineId> {
512        let view = self
513            .files
514            .get_mut(&file)
515            .context("Context gap is unavailable")?;
516        let item = view
517            .gaps
518            .get_mut(gap)
519            .context("Context gap is outside the file")?;
520        let before = item.revealed;
521        item.revealed += 20.min(item.source.len - item.revealed);
522        let offset = if item.leading() {
523            item.exposed().start
524        } else {
525            item.revealed - 1
526        };
527        let selected = item.line(file, offset);
528        let built = view.build(&doc.files[file]);
529        let rows = match built {
530            Ok(rows) => rows,
531            Err(error) => {
532                view.gaps[gap].revealed = before;
533                return Err(error);
534            }
535        };
536        let old: [usize; 2] = std::array::from_fn(|layout| view.rows[layout].len);
537        let growth: [usize; 2] = std::array::from_fn(|layout| rows[layout].len - old[layout]);
538        for layout in 0..2 {
539            if doc
540                .row_len(layout == 1)
541                .checked_add(self.extra.last().map_or(0, |e| e[layout]))
542                .and_then(|n| n.checked_add(growth[layout]))
543                .is_none()
544            {
545                view.gaps[gap].revealed = before;
546                anyhow::bail!("Expanded document row count overflow");
547            }
548        }
549        view.rows = rows;
550        self.extra.resize(
551            doc.files.len(),
552            self.extra.last().copied().unwrap_or([0; 2]),
553        );
554        for extra in &mut self.extra[file..] {
555            for layout in 0..2 {
556                extra[layout] += growth[layout];
557            }
558        }
559        self.source_revision.invalidate();
560        Ok(selected)
561    }
562}