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, 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
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Sign {
79 AddOrChange,
81 DeleteAfter,
84}
85
86impl Hunk {
87 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 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 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 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 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}