1use std::ops::Range;
2use std::path::Path;
3
4use pfx_editor_doc::TextSplice;
5use similar::{DiffTag, TextDiff};
6
7pub const CONTEXT: usize = 3;
8pub const REVERT: &str = "revert hunk";
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Mark {
12 Add,
13 Del,
14 Change,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct Hunk {
19 pub mark: Mark,
20 pub old: Range<usize>,
21 pub new: Range<usize>,
22 pub old_lines: Range<usize>,
23 pub new_lines: Range<usize>,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum Row {
28 Head(usize),
29 Context {
30 line: usize,
31 text: String,
32 },
33 Fold(usize),
34 Del {
35 hunk: usize,
36 text: String,
37 },
38 Add {
39 hunk: usize,
40 line: usize,
41 text: String,
42 },
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Eq)]
46pub struct Diff {
47 hunks: Vec<Hunk>,
48 rows: Vec<Row>,
49}
50
51fn offsets(lines: &[&str]) -> Vec<usize> {
52 let mut at = 0;
53 let mut starts = vec![0];
54 for line in lines {
55 at += line.len();
56 starts.push(at);
57 }
58 starts
59}
60
61fn bare(line: &str) -> String {
62 line.trim_end_matches(['\n', '\r']).to_string()
63}
64
65fn context<'a>(news: &'a [&str], lines: Range<usize>) -> impl Iterator<Item = Row> + 'a {
66 lines.map(|line| Row::Context {
67 line,
68 text: bare(news[line]),
69 })
70}
71
72impl Hunk {
73 pub fn revert(&self, file: impl AsRef<Path>, base: &str, text: &str) -> TextSplice {
74 let old = &base[self.old.clone()];
75 let end = self.new.start + old.len();
76 TextSplice::new(
77 file.as_ref(),
78 self.new.clone(),
79 &text[self.new.clone()],
80 old,
81 )
82 .labelled(REVERT)
83 .with_carets(self.new.clone(), end..end)
84 }
85}
86
87impl Diff {
88 pub fn between(base: &str, text: &str) -> Diff {
89 let diff = TextDiff::from_lines(base, text);
90 let (olds, news) = (diff.old_slices(), diff.new_slices());
91 let (old_at, new_at) = (offsets(olds), offsets(news));
92 let ops = diff.ops();
93 let mut hunks: Vec<Hunk> = Vec::new();
94 let mut rows = Vec::new();
95 let mut index = 0;
96 while index < ops.len() {
97 let (tag, old, new) = ops[index].as_tag_tuple();
98 if tag == DiffTag::Equal {
99 let before = !hunks.is_empty();
100 let after = index + 1 < ops.len();
101 let (head, tail) = (
102 if before { CONTEXT } else { 0 },
103 if after { CONTEXT } else { 0 },
104 );
105 if head + tail >= new.len() {
106 rows.extend(context(news, new));
107 } else {
108 rows.extend(context(news, new.start..new.start + head));
109 rows.push(Row::Fold(new.len() - head - tail));
110 rows.extend(context(news, new.end - tail..new.end));
111 }
112 index += 1;
113 continue;
114 }
115 let (mut old_lines, mut new_lines) = (old, new);
116 index += 1;
117 while index < ops.len() && ops[index].tag() != DiffTag::Equal {
118 let (_, old, new) = ops[index].as_tag_tuple();
119 old_lines.end = old.end;
120 new_lines.end = new.end;
121 index += 1;
122 }
123 let mark = if old_lines.is_empty() {
124 Mark::Add
125 } else if new_lines.is_empty() {
126 Mark::Del
127 } else {
128 Mark::Change
129 };
130 let at = hunks.len();
131 rows.push(Row::Head(at));
132 for line in old_lines.clone() {
133 rows.push(Row::Del {
134 hunk: at,
135 text: bare(olds[line]),
136 });
137 }
138 for line in new_lines.clone() {
139 rows.push(Row::Add {
140 hunk: at,
141 line,
142 text: bare(news[line]),
143 });
144 }
145 hunks.push(Hunk {
146 mark,
147 old: old_at[old_lines.start]..old_at[old_lines.end],
148 new: new_at[new_lines.start]..new_at[new_lines.end],
149 old_lines,
150 new_lines,
151 });
152 }
153 Diff { hunks, rows }
154 }
155
156 pub fn hunks(&self) -> &[Hunk] {
157 &self.hunks
158 }
159
160 pub fn rows(&self) -> &[Row] {
161 &self.rows
162 }
163
164 pub fn is_empty(&self) -> bool {
165 self.hunks.is_empty()
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 const BASE: &str = "a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6\ng = 7\nh = 8\ni = 9\nj = 10\n";
174
175 #[test]
176 fn equal_texts_have_no_hunks() {
177 let diff = Diff::between(BASE, BASE);
178 assert!(diff.is_empty());
179 assert!(diff.rows().iter().all(|row| !matches!(row, Row::Head(_))));
180 }
181
182 #[test]
183 fn a_changed_line_is_one_change_hunk_with_context() {
184 let text = BASE.replace("e = 5", "e = 50");
185 let diff = Diff::between(BASE, &text);
186 let [hunk] = diff.hunks() else {
187 panic!("{:?}", diff.hunks())
188 };
189 assert_eq!(hunk.mark, Mark::Change);
190 assert_eq!(&BASE[hunk.old.clone()], "e = 5\n");
191 assert_eq!(&text[hunk.new.clone()], "e = 50\n");
192 assert_eq!(hunk.old_lines, 4..5);
193 let rows = diff.rows();
194 assert!(rows.contains(&Row::Del {
195 hunk: 0,
196 text: "e = 5".into()
197 }));
198 assert!(rows.contains(&Row::Add {
199 hunk: 0,
200 line: 4,
201 text: "e = 50".into()
202 }));
203 assert!(rows.contains(&Row::Fold(1)));
204 assert!(rows.contains(&Row::Context {
205 line: 7,
206 text: "h = 8".into()
207 }));
208 assert!(!rows.contains(&Row::Context {
209 line: 8,
210 text: "i = 9".into()
211 }));
212 }
213
214 #[test]
215 fn added_and_deleted_lines_are_marked() {
216 let text = format!("new = 0\n{}", BASE.replace("j = 10\n", ""));
217 let diff = Diff::between(BASE, &text);
218 let marks: Vec<_> = diff.hunks().iter().map(|hunk| hunk.mark).collect();
219 assert_eq!(marks, vec![Mark::Add, Mark::Del]);
220 }
221
222 #[test]
223 fn the_same_texts_give_the_same_diff() {
224 let text = BASE.replace("c = 3", "c = 30").replace("i = 9", "i = 90");
225 assert_eq!(Diff::between(BASE, &text), Diff::between(BASE, &text));
226 assert_eq!(Diff::between(BASE, &text).hunks().len(), 2);
227 }
228
229 #[test]
230 fn a_revert_puts_the_base_text_back_over_the_hunk() {
231 let text = BASE
232 .replace("c = 3", "c = 30\nc2 = 31")
233 .replace("i = 9", "i = 90");
234 let diff = Diff::between(BASE, &text);
235 let splice = diff.hunks()[0].revert("x.toml", BASE, &text);
236 assert_eq!(splice.label, REVERT);
237 let mut back = text.clone();
238 back.replace_range(splice.range.clone(), &splice.new);
239 assert_eq!(back, BASE.replace("i = 9", "i = 90"));
240 }
241
242 #[test]
243 fn a_missing_final_newline_is_a_change() {
244 let diff = Diff::between("a = 1\n", "a = 1");
245 assert_eq!(diff.hunks().len(), 1);
246 assert_eq!(diff.hunks()[0].mark, Mark::Change);
247 }
248}