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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
67pub struct FileDiff {
68    #[serde(with = "strop_core::path_serde")]
69    pub path: PathBuf,
70    pub hunks: Vec<Hunk>,
71    pub added: usize,
72    pub deleted: usize,
73}
74
75/// One changed line, for gutter signs. Hunk headers include context
76/// lines, so signs track the +/- lines, not the header range.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Sign {
79    /// Buffer line was added or changed.
80    AddOrChange,
81    /// Buffer line sits right below a deletion (the line number may be
82    /// one past the buffer end for an EOF deletion — clamp on render).
83    DeleteAfter,
84}
85
86impl Hunk {
87    /// Signs this hunk produces, derived from its line origins.
88    pub fn signs(&self) -> Vec<(usize, Sign)> {
89        let mut out = Vec::new();
90        let mut nl = self.new_start;
91        for line in &self.lines {
92            match line.origin {
93                LineOrigin::Addition => {
94                    out.push((nl, Sign::AddOrChange));
95                    nl += 1;
96                }
97                LineOrigin::Deletion => out.push((nl, Sign::DeleteAfter)),
98                LineOrigin::Context => nl += 1,
99            }
100        }
101        out
102    }
103
104    /// The actual changed region (from add/del lines, not the header,
105    /// which includes context): new-side `new_first`/`new_count`
106    /// (1-based) and old-side `old_first`/`old_count`. For pure
107    /// deletions `new_first` is the new line *following* the gap.
108    pub fn changed_region(&self) -> (usize, usize, usize, usize) {
109        let mut nl = self.new_start;
110        let mut ol = self.old_start;
111        let mut new_lines = Vec::new();
112        let mut old_lines = Vec::new();
113        for line in &self.lines {
114            match line.origin {
115                LineOrigin::Addition => {
116                    new_lines.push(nl);
117                    nl += 1;
118                }
119                LineOrigin::Deletion => {
120                    old_lines.push(ol);
121                    ol += 1;
122                }
123                LineOrigin::Context => {
124                    nl += 1;
125                    ol += 1;
126                }
127            }
128        }
129        let new_first = new_lines.first().copied().unwrap_or(nl);
130        let old_first = old_lines.first().copied().unwrap_or(ol);
131        (new_first, new_lines.len(), old_first, old_lines.len())
132    }
133
134    /// Buffer lines covered (signs render on these); `total_lines`
135    /// clamps an EOF deletion onto the last line.
136    pub fn covers(&self, line_1based: usize, total_lines: usize) -> bool {
137        self.signs().iter().any(|&(l, kind)| match kind {
138            Sign::AddOrChange => l == line_1based,
139            Sign::DeleteAfter => l.min(total_lines) == line_1based,
140        })
141    }
142
143    /// The `@@ -a,b +c,d @@` header row as the diff surface shows it.
144    pub fn header(&self) -> String {
145        format!(
146            "@@ -{},{} +{},{} @@",
147            self.old_start, self.old_count, self.new_start, self.new_count
148        )
149    }
150
151    /// Assemble a hunk from its header numbers and typed lines; the
152    /// kind comes from the actual origins — header counts include
153    /// context lines, which would mislabel small-file hunks.
154    pub fn build(
155        old_start: usize,
156        old_count: usize,
157        new_start: usize,
158        new_count: usize,
159        lines: Vec<DiffLine>,
160    ) -> Self {
161        let has_add = lines.iter().any(|l| l.origin == LineOrigin::Addition);
162        let has_del = lines.iter().any(|l| l.origin == LineOrigin::Deletion);
163        let kind = match (has_add, has_del) {
164            (true, false) => HunkKind::Add,
165            (false, true) => HunkKind::Delete,
166            _ => HunkKind::Change,
167        };
168        Hunk {
169            kind,
170            new_start,
171            new_count,
172            old_start,
173            old_count,
174            lines,
175        }
176    }
177}