Skip to main content

lattice_diff/
types.rs

1//! Data model for the diff subsystem.
2//!
3//! All types are pure (no actor / host references). The
4//! `HunkIndex` is what `D.2`'s `DiffSubsystem` will publish via
5//! `ArcSwap`; the `Hunk` and `LineRange` shapes are what every
6//! consumer of `D.3+` reads against.
7
8use smallvec::SmallVec;
9
10/// Half-open line range `[start, end)` in a document.
11///
12/// Line indices are 0-based. `end == start` means the range is
13/// empty (zero lines). The largest representable range covers
14/// `u32::MAX - 1` lines, which is sufficient for files up to
15/// ~4 billion lines.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub struct LineRange {
18    pub start: u32,
19    pub end: u32,
20}
21
22impl LineRange {
23    /// Construct a new line range. Debug-asserts `start <= end`.
24    pub fn new(start: u32, end: u32) -> Self {
25        debug_assert!(
26            start <= end,
27            "LineRange::new: start {start} must be <= end {end}"
28        );
29        Self { start, end }
30    }
31
32    /// `true` if the range covers zero lines.
33    pub fn is_empty(self) -> bool {
34        self.start == self.end
35    }
36
37    /// Number of lines covered.
38    pub fn len(self) -> u32 {
39        self.end - self.start
40    }
41}
42
43/// What kind of change a hunk represents.
44///
45/// Classification is relative to *one* "earlier" side. For
46/// two-way diffs the earlier side is `ranges[0]` (the A rope);
47/// for three-way diffs the earlier side is `ranges[0]` (the
48/// base rope). `Conflict` is only produced by three-way diff
49/// when both local and remote independently changed an
50/// overlapping base region.
51#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
52pub enum HunkKind {
53    /// Lines present only in the later side(s). Earlier side
54    /// range is empty.
55    Add,
56    /// Lines present only in the earlier side. Later side
57    /// range(s) is/are empty.
58    Remove,
59    /// Lines present in both, with differing content.
60    Change,
61    /// Three-way only: both local and remote modified the
62    /// same base region. Resolution is deferred to D.6 /
63    /// user.
64    Conflict,
65}
66
67/// Diff algorithm used by `imara-diff`.
68///
69/// All three are wired and behave identically with respect to
70/// hunk *kinds*; they differ in how they choose where hunk
71/// boundaries land for ambiguous edits.
72#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
73pub enum DiffAlgorithm {
74    /// Git's default since 2.7. Best general-purpose results
75    /// on code; preferred for Lattice.
76    #[default]
77    Histogram,
78    /// The classic Eugene Myers algorithm. Slightly weaker
79    /// results on rebraced code; included for completeness.
80    Myers,
81    /// Myers run with the `--minimal` post-process — finds
82    /// the smallest possible diff at extra CPU cost. Use when
83    /// you want the tightest output and don't mind a slower
84    /// recompute. Mapped to `imara_diff::Algorithm::MyersMinimal`.
85    MyersMinimal,
86}
87
88/// A single hunk: a contiguous region of change across the
89/// participating documents.
90///
91/// `ranges.len()` matches the document arity:
92///
93/// - Two-way: `[a_range, b_range]`
94/// - Three-way: `[base_range, local_range, remote_range]`
95///
96/// The `SmallVec` inline capacity of 3 covers both cases
97/// without allocation.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct Hunk {
100    pub kind: HunkKind,
101    pub ranges: SmallVec<[LineRange; 3]>,
102    /// DR.4 (2026-08-12): intra-line refinement for this hunk — which
103    /// BYTES of each changed line actually changed.
104    ///
105    /// **Per side, not per pair** (DR.5, 2026-08-12). `refine.removed`
106    /// is indexed by `line - ranges[0].start` and `refine.added` by
107    /// `line - ranges[1].start`; the two need not be the same length,
108    /// which is the whole point — the predecessor shape was pair-keyed
109    /// and so could not describe an *n*-removed / *m*-added hunk at
110    /// all, making the commonest "rewrite a line and add a comment
111    /// above it" hunk decline refinement outright.
112    ///
113    /// Empty for a hunk that declined (`Add` / `Remove`, identical
114    /// regions, a wholesale rewrite, or one past the size cap), which
115    /// renders exactly as it did before refinement existed.
116    ///
117    /// Carried ON the hunk rather than in a parallel map keyed by hunk
118    /// index, so refinement cannot desynchronise from the ranges it
119    /// describes — the same "one thing being shifted" property the
120    /// sign-map derivation relies on. Computed once at diff time, not
121    /// per render.
122    pub refine: crate::refine::RegionRefinement,
123}
124
125/// Published hunk list with the algorithm used and a
126/// revision tag.
127///
128/// `D.2`'s `DiffSubsystem` increments `revision` on each
129/// recompute and publishes the new `Arc<HunkIndex>` via
130/// `ArcSwap`. Consumers compare revisions to invalidate
131/// caches (`DiffMap` row-translation, scroll-bind row
132/// mapping, etc.).
133#[derive(Clone, Debug)]
134pub struct HunkIndex {
135    pub hunks: Vec<Hunk>,
136    pub algorithm: DiffAlgorithm,
137    pub revision: u64,
138}
139
140impl HunkIndex {
141    /// Construct an empty index with the given algorithm and
142    /// revision 0.
143    pub fn empty(algorithm: DiffAlgorithm) -> Self {
144        Self {
145            hunks: Vec::new(),
146            algorithm,
147            revision: 0,
148        }
149    }
150
151    pub fn is_empty(&self) -> bool {
152        self.hunks.is_empty()
153    }
154
155    pub fn len(&self) -> usize {
156        self.hunks.len()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn line_range_basics() {
166        let r = LineRange::new(3, 7);
167        assert_eq!(r.start, 3);
168        assert_eq!(r.end, 7);
169        assert_eq!(r.len(), 4);
170        assert!(!r.is_empty());
171
172        let empty = LineRange::new(5, 5);
173        assert!(empty.is_empty());
174        assert_eq!(empty.len(), 0);
175    }
176
177    #[test]
178    fn hunk_kind_default_for_diff_algorithm() {
179        assert_eq!(DiffAlgorithm::default(), DiffAlgorithm::Histogram);
180    }
181
182    #[test]
183    fn hunk_index_empty_helpers() {
184        let idx = HunkIndex::empty(DiffAlgorithm::Histogram);
185        assert!(idx.is_empty());
186        assert_eq!(idx.len(), 0);
187        assert_eq!(idx.algorithm, DiffAlgorithm::Histogram);
188        assert_eq!(idx.revision, 0);
189    }
190}