1use std::ops::Range;
10use std::time::Duration;
11
12use similar::{Algorithm, ChangeTag, DiffOp, TextDiff};
13
14use crate::display::Padding;
15use crate::text::Document;
16
17pub const DIFF_TIMEOUT: Duration = Duration::from_secs(2);
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum RowKind {
22 Equal,
23 Added,
25 Removed,
27 Changed,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct DiffRow {
33 pub kind: RowKind,
34 pub left: Option<usize>,
35 pub right: Option<usize>,
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub struct DiffStats {
40 pub added: usize,
41 pub removed: usize,
42 pub changed: usize,
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Eq)]
46pub struct LineDiff {
47 pub rows: Vec<DiffRow>,
48 pub hunks: Vec<Range<usize>>,
50 pub stats: DiffStats,
51}
52
53fn lines(doc: &Document) -> Vec<String> {
54 (0..doc.line_count()).map(|l| doc.line_text(l)).collect()
55}
56
57pub fn diff_lines(left: &Document, right: &Document) -> LineDiff {
61 let a = lines(left);
62 let b = lines(right);
63 let a_refs: Vec<&str> = a.iter().map(String::as_str).collect();
64 let b_refs: Vec<&str> = b.iter().map(String::as_str).collect();
65 let diff = TextDiff::configure()
66 .algorithm(Algorithm::Myers)
67 .timeout(DIFF_TIMEOUT)
68 .diff_slices(&a_refs, &b_refs);
69 let mut out = LineDiff::default();
70 let push = |out: &mut LineDiff, kind, left, right| {
71 if kind != RowKind::Equal {
72 let row = out.rows.len();
73 match out.hunks.last_mut() {
74 Some(h) if h.end == row => h.end += 1,
75 _ => out.hunks.push(row..row + 1),
76 }
77 match kind {
78 RowKind::Added => out.stats.added += 1,
79 RowKind::Removed => out.stats.removed += 1,
80 RowKind::Changed => out.stats.changed += 1,
81 RowKind::Equal => {}
82 }
83 }
84 out.rows.push(DiffRow { kind, left, right });
85 };
86 for op in diff.ops() {
87 match *op {
88 DiffOp::Equal {
89 old_index,
90 new_index,
91 len,
92 } => {
93 for i in 0..len {
94 push(
95 &mut out,
96 RowKind::Equal,
97 Some(old_index + i),
98 Some(new_index + i),
99 );
100 }
101 }
102 DiffOp::Delete {
103 old_index, old_len, ..
104 } => {
105 for i in 0..old_len {
106 push(&mut out, RowKind::Removed, Some(old_index + i), None);
107 }
108 }
109 DiffOp::Insert {
110 new_index, new_len, ..
111 } => {
112 for i in 0..new_len {
113 push(&mut out, RowKind::Added, None, Some(new_index + i));
114 }
115 }
116 DiffOp::Replace {
117 old_index,
118 old_len,
119 new_index,
120 new_len,
121 } => {
122 for i in 0..old_len.max(new_len) {
123 let l = (i < old_len).then_some(old_index + i);
124 let r = (i < new_len).then_some(new_index + i);
125 let kind = match (l, r) {
126 (Some(_), Some(_)) => RowKind::Changed,
127 (Some(_), None) => RowKind::Removed,
128 _ => RowKind::Added,
129 };
130 push(&mut out, kind, l, r);
131 }
132 }
133 }
134 }
135 out
136}
137
138pub fn diff_words(left: &str, right: &str) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
141 let diff = TextDiff::configure()
142 .timeout(DIFF_TIMEOUT)
143 .diff_words(left, right);
144 let (mut a, mut b) = (Vec::new(), Vec::new());
145 let (mut ia, mut ib) = (0, 0);
146 let add = |ranges: &mut Vec<Range<usize>>, start: usize, len: usize| match ranges.last_mut() {
147 Some(last) if last.end == start => last.end += len,
148 _ => ranges.push(start..start + len),
149 };
150 for change in diff.iter_all_changes() {
151 let len = change.value().len();
152 match change.tag() {
153 ChangeTag::Equal => {
154 ia += len;
155 ib += len;
156 }
157 ChangeTag::Delete => {
158 add(&mut a, ia, len);
159 ia += len;
160 }
161 ChangeTag::Insert => {
162 add(&mut b, ib, len);
163 ib += len;
164 }
165 }
166 }
167 (a, b)
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum Side {
172 Left,
173 Right,
174}
175
176impl DiffRow {
177 pub fn line(&self, side: Side) -> Option<usize> {
179 match side {
180 Side::Left => self.left,
181 Side::Right => self.right,
182 }
183 }
184}
185
186impl LineDiff {
187 pub fn line_rows(&self, side: Side) -> Vec<usize> {
189 self.rows
190 .iter()
191 .enumerate()
192 .filter(|(_, r)| r.line(side).is_some())
193 .map(|(i, _)| i)
194 .collect()
195 }
196
197 pub fn padding(&self, side: Side) -> Padding {
200 let mut p = Padding::default();
201 let mut blank = 0;
202 for r in &self.rows {
203 if r.line(side).is_some() {
204 p.before.push(blank);
205 blank = 0;
206 } else {
207 blank += 1;
208 }
209 }
210 p.after_last = blank;
211 p
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn kinds(d: &LineDiff) -> Vec<(RowKind, Option<usize>, Option<usize>)> {
220 d.rows.iter().map(|r| (r.kind, r.left, r.right)).collect()
221 }
222
223 #[test]
224 fn rows_align_with_fillers() {
225 let left = Document::new("a\nb\nc\nd");
226 let right = Document::new("a\nB\nc\nnew\nd");
227 let d = diff_lines(&left, &right);
228 use RowKind::*;
229 assert_eq!(
230 kinds(&d),
231 vec![
232 (Equal, Some(0), Some(0)),
233 (Changed, Some(1), Some(1)),
234 (Equal, Some(2), Some(2)),
235 (Added, None, Some(3)),
236 (Equal, Some(3), Some(4)),
237 ]
238 );
239 assert_eq!(d.hunks, vec![1..2, 3..4]);
240 assert_eq!(
241 d.stats,
242 DiffStats {
243 added: 1,
244 removed: 0,
245 changed: 1
246 }
247 );
248 }
249
250 #[test]
251 fn uneven_replacements_pair_then_spill() {
252 let left = Document::new("x\none\ntwo\nthree\ny");
253 let right = Document::new("x\nuno\ny");
254 let d = diff_lines(&left, &right);
255 use RowKind::*;
256 assert_eq!(
257 kinds(&d),
258 vec![
259 (Equal, Some(0), Some(0)),
260 (Changed, Some(1), Some(1)),
261 (Removed, Some(2), None),
262 (Removed, Some(3), None),
263 (Equal, Some(4), Some(2)),
264 ]
265 );
266 assert_eq!(d.hunks, vec![1..4]);
267 }
268
269 #[test]
270 fn identical_documents_have_no_hunks() {
271 let doc = Document::new("same\ntext\n");
272 let d = diff_lines(&doc, &doc.clone());
273 assert!(d.hunks.is_empty());
274 assert_eq!(d.rows.len(), 3);
275 }
276
277 #[test]
278 fn word_ranges() {
279 let (a, b) = diff_words("let total = price * qty;", "let total = cost * qty + tax;");
280 let left = "let total = price * qty;";
281 let right = "let total = cost * qty + tax;";
282 let words = |s: &str, r: &[Range<usize>]| -> Vec<String> {
283 r.iter().map(|r| s[r.clone()].to_owned()).collect()
284 };
285 assert_eq!(words(left, &a), vec!["price", "qty;"]);
286 assert_eq!(words(right, &b), vec!["cost", "qty + tax;"]);
287 }
288
289 #[test]
290 fn padding_lines_up_both_sides() {
291 let d = diff_lines(
292 &Document::new(
293 "a
294b",
295 ),
296 &Document::new(
297 "new
298a
299new
300b
301end",
302 ),
303 );
304 let left = d.padding(Side::Left);
305 assert_eq!(left.before, vec![1, 1]);
306 assert_eq!(left.after_last, 1);
307 assert_eq!(
308 d.padding(Side::Right),
309 Padding {
310 before: vec![0; 5],
311 after_last: 0
312 }
313 );
314 assert_eq!(d.line_rows(Side::Left), vec![1, 3]);
315 assert_eq!(d.line_rows(Side::Right), vec![0, 1, 2, 3, 4]);
316 }
317}