1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub enum LineOrigin {
17 Context,
18 Addition,
19 Deletion,
20}
21
22#[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 pub text: Vec<u8>,
32 pub has_newline: bool,
35}
36
37impl DiffLine {
38 pub fn text_str(&self) -> std::borrow::Cow<'_, str> {
40 String::from_utf8_lossy(&self.text)
41 }
42 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
54pub struct Hunk {
55 pub kind: HunkKind,
56 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#[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
75impl FileDiff {
76 pub(crate) fn from_hunks(path: PathBuf, hunks: Vec<Hunk>) -> Self {
80 let added = hunks
81 .iter()
82 .flat_map(|h| &h.lines)
83 .filter(|l| l.origin == LineOrigin::Addition)
84 .count();
85 let deleted = hunks
86 .iter()
87 .flat_map(|h| &h.lines)
88 .filter(|l| l.origin == LineOrigin::Deletion)
89 .count();
90 Self {
91 path,
92 hunks,
93 added,
94 deleted,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum Sign {
103 AddOrChange,
105 DeleteAfter,
108}
109
110impl Hunk {
111 pub fn signs(&self) -> Vec<(usize, Sign)> {
113 let mut out = Vec::new();
114 let mut nl = self.new_start;
115 for line in &self.lines {
116 match line.origin {
117 LineOrigin::Addition => {
118 out.push((nl, Sign::AddOrChange));
119 nl += 1;
120 }
121 LineOrigin::Deletion => out.push((nl, Sign::DeleteAfter)),
122 LineOrigin::Context => nl += 1,
123 }
124 }
125 out
126 }
127
128 pub fn changed_region(&self) -> (usize, usize, usize, usize) {
133 let mut nl = self.new_start;
134 let mut ol = self.old_start;
135 let mut new_lines = Vec::new();
136 let mut old_lines = Vec::new();
137 for line in &self.lines {
138 match line.origin {
139 LineOrigin::Addition => {
140 new_lines.push(nl);
141 nl += 1;
142 }
143 LineOrigin::Deletion => {
144 old_lines.push(ol);
145 ol += 1;
146 }
147 LineOrigin::Context => {
148 nl += 1;
149 ol += 1;
150 }
151 }
152 }
153 let new_first = new_lines.first().copied().unwrap_or(nl);
154 let old_first = old_lines.first().copied().unwrap_or(ol);
155 (new_first, new_lines.len(), old_first, old_lines.len())
156 }
157
158 pub fn covers(&self, line_1based: usize, total_lines: usize) -> bool {
161 self.signs().iter().any(|&(l, kind)| match kind {
162 Sign::AddOrChange => l == line_1based,
163 Sign::DeleteAfter => l.min(total_lines) == line_1based,
164 })
165 }
166
167 pub fn header(&self) -> String {
169 format!(
170 "@@ -{},{} +{},{} @@",
171 self.old_start, self.old_count, self.new_start, self.new_count
172 )
173 }
174
175 pub fn build(
179 old_start: usize,
180 old_count: usize,
181 new_start: usize,
182 new_count: usize,
183 lines: Vec<DiffLine>,
184 ) -> Self {
185 let has_add = lines.iter().any(|l| l.origin == LineOrigin::Addition);
186 let has_del = lines.iter().any(|l| l.origin == LineOrigin::Deletion);
187 let kind = match (has_add, has_del) {
188 (true, false) => HunkKind::Add,
189 (false, true) => HunkKind::Delete,
190 _ => HunkKind::Change,
191 };
192 Hunk {
193 kind,
194 new_start,
195 new_count,
196 old_start,
197 old_count,
198 lines,
199 }
200 }
201}