Skip to main content

strop_git/
diff.rs

1//! Diff data: hunks, line origins, signs — the typed model every
2//! git surface renders from (0010).
3
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum HunkKind {
8    Add,
9    Change,
10    Delete,
11}
12
13/// Where a diff line comes from — addition/deletion carry which side's
14/// line number applies (0010 §1: typed origins, never `+`-sniffing).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LineOrigin {
17    Context,
18    Addition,
19    Deletion,
20}
21
22/// One line of a hunk: content without prefix, plus the 1-based line
23/// number on each side that has one (absent side: `None`, never `0`).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct DiffLine {
26    pub origin: LineOrigin,
27    pub old_lineno: Option<usize>,
28    pub new_lineno: Option<usize>,
29    /// The line's bytes WITHOUT its terminator (0018: byte-precise —
30    /// a `\r` stays; non-UTF-8 stays bytes).
31    pub text: Vec<u8>,
32    /// Whether the source line ended in a newline — the missing-final-
33    /// newline marker is data, not a patch-text nuance (0018).
34    pub has_newline: bool,
35}
36
37impl DiffLine {
38    /// Display form (lossy at the render edge only).
39    pub fn text_str(&self) -> std::borrow::Cow<'_, str> {
40        String::from_utf8_lossy(&self.text)
41    }
42    /// The line's bytes WITH its terminator, exactly as stored.
43    pub fn bytes_with_terminator(&self) -> Vec<u8> {
44        let mut b = self.text.clone();
45        if self.has_newline {
46            b.push(b'\n');
47        }
48        b
49    }
50}
51
52/// One diff hunk between two versions of a file, in 1-based lines.
53#[derive(Debug, Clone)]
54pub struct Hunk {
55    pub kind: HunkKind,
56    /// First affected line in the new version (1-based). For pure
57    /// deletions this is the line *after* which content vanished.
58    pub new_start: usize,
59    pub new_count: usize,
60    pub old_start: usize,
61    pub old_count: usize,
62    pub lines: Vec<DiffLine>,
63}
64
65/// One file's diff at a commit (vs its parent): the delta view's data.
66#[derive(Debug, Clone)]
67pub struct FileDiff {
68    pub path: PathBuf,
69    pub hunks: Vec<Hunk>,
70    pub added: usize,
71    pub deleted: usize,
72}
73
74/// One changed line, for gutter signs. Hunk headers include context
75/// lines, so signs track the +/- lines, not the header range.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum Sign {
78    /// Buffer line was added or changed.
79    AddOrChange,
80    /// Buffer line sits right below a deletion (the line number may be
81    /// one past the buffer end for an EOF deletion — clamp on render).
82    DeleteAfter,
83}
84
85impl Hunk {
86    /// Signs this hunk produces, derived from its line origins.
87    pub fn signs(&self) -> Vec<(usize, Sign)> {
88        let mut out = Vec::new();
89        let mut nl = self.new_start;
90        for line in &self.lines {
91            match line.origin {
92                LineOrigin::Addition => {
93                    out.push((nl, Sign::AddOrChange));
94                    nl += 1;
95                }
96                LineOrigin::Deletion => out.push((nl, Sign::DeleteAfter)),
97                LineOrigin::Context => nl += 1,
98            }
99        }
100        out
101    }
102
103    /// The actual changed region (from add/del lines, not the header,
104    /// which includes context): new-side `new_first`/`new_count`
105    /// (1-based) and old-side `old_first`/`old_count`. For pure
106    /// deletions `new_first` is the new line *following* the gap.
107    pub fn changed_region(&self) -> (usize, usize, usize, usize) {
108        let mut nl = self.new_start;
109        let mut ol = self.old_start;
110        let mut new_lines = Vec::new();
111        let mut old_lines = Vec::new();
112        for line in &self.lines {
113            match line.origin {
114                LineOrigin::Addition => {
115                    new_lines.push(nl);
116                    nl += 1;
117                }
118                LineOrigin::Deletion => {
119                    old_lines.push(ol);
120                    ol += 1;
121                }
122                LineOrigin::Context => {
123                    nl += 1;
124                    ol += 1;
125                }
126            }
127        }
128        let new_first = new_lines.first().copied().unwrap_or(nl);
129        let old_first = old_lines.first().copied().unwrap_or(ol);
130        (new_first, new_lines.len(), old_first, old_lines.len())
131    }
132
133    /// Buffer lines covered (signs render on these); `total_lines`
134    /// clamps an EOF deletion onto the last line.
135    pub fn covers(&self, line_1based: usize, total_lines: usize) -> bool {
136        self.signs().iter().any(|&(l, kind)| match kind {
137            Sign::AddOrChange => l == line_1based,
138            Sign::DeleteAfter => l.min(total_lines) == line_1based,
139        })
140    }
141
142    /// The `@@ -a,b +c,d @@` header row as the diff surface shows it.
143    pub fn header(&self) -> String {
144        format!(
145            "@@ -{},{} +{},{} @@",
146            self.old_start, self.old_count, self.new_start, self.new_count
147        )
148    }
149
150    /// Assemble a hunk from its header numbers and typed lines; the
151    /// kind comes from the actual origins — header counts include
152    /// context lines, which would mislabel small-file hunks.
153    pub fn build(
154        old_start: usize,
155        old_count: usize,
156        new_start: usize,
157        new_count: usize,
158        lines: Vec<DiffLine>,
159    ) -> Self {
160        let has_add = lines.iter().any(|l| l.origin == LineOrigin::Addition);
161        let has_del = lines.iter().any(|l| l.origin == LineOrigin::Deletion);
162        let kind = match (has_add, has_del) {
163            (true, false) => HunkKind::Add,
164            (false, true) => HunkKind::Delete,
165            _ => HunkKind::Change,
166        };
167        Hunk {
168            kind,
169            new_start,
170            new_count,
171            old_start,
172            old_count,
173            lines,
174        }
175    }
176}