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}