1use std::borrow::Cow;
18use std::collections::HashMap;
19
20use crate::compare::RopeScanner;
21use crate::delta::{Delta, DeltaElement};
22use crate::interval::Interval;
23use crate::rope::{LinesMetric, Rope, RopeDelta, RopeInfo};
24use crate::tree::{Node, NodeInfo};
25
26pub trait Diff<N: NodeInfo> {
28 fn compute_delta(base: &Node<N>, target: &Node<N>) -> Delta<N>;
29}
30
31const MIN_SIZE: usize = 32;
34
35pub struct LineHashDiff;
49
50impl Diff<RopeInfo> for LineHashDiff {
51 fn compute_delta(base: &Rope, target: &Rope) -> RopeDelta {
52 let mut builder = DiffBuilder::default();
53
54 let mut scanner = RopeScanner::new(base, target);
56 let (start_offset, diff_end) = scanner.find_min_diff_range();
57 let target_end = target.len() - diff_end;
58
59 if start_offset > 0 {
60 builder.copy(0, 0, start_offset);
61 }
62
63 if start_offset == base.len() && target.len() == base.len() {
65 return builder.to_delta(base, target);
66 }
67
68 let line_hashes = make_line_hashes(&base, MIN_SIZE);
69
70 let line_count = target.measure::<LinesMetric>() + 1;
71 let mut matches = Vec::with_capacity(line_count);
72
73 let mut targ_line_offset = 0;
74 let mut prev_base = 0;
75
76 let mut needs_subseq = false;
77 for line in target.lines_raw(start_offset..target_end) {
78 let non_ws = non_ws_offset(&line);
79 if line.len() - non_ws >= MIN_SIZE {
80 if let Some(base_off) = line_hashes.get(&line[non_ws..]) {
81 let targ_off = targ_line_offset + non_ws;
82 matches.push((start_offset + targ_off, *base_off));
83 if *base_off < prev_base {
84 needs_subseq = true;
85 }
86 prev_base = *base_off;
87 }
88 }
89 targ_line_offset += line.len();
90 }
91
92 let longest_subseq =
99 if needs_subseq { longest_increasing_region_set(&matches) } else { matches };
100
101 let mut prev_end = start_offset;
105
106 for (targ_off, base_off) in longest_subseq {
107 if targ_off <= prev_end {
108 continue;
109 }
110 let (left_dist, mut right_dist) =
111 expand_match(base, target, base_off, targ_off, prev_end);
112
113 right_dist = right_dist.min(target_end - targ_off);
115
116 let targ_start = targ_off - left_dist;
117 let base_start = base_off - left_dist;
118 let len = left_dist + right_dist;
119 prev_end = targ_start + len;
120
121 builder.copy(base_start, targ_start, len);
122 }
123
124 if diff_end > 0 {
125 builder.copy(base.len() - diff_end, target.len() - diff_end, diff_end);
126 }
127
128 builder.to_delta(base, target)
129 }
130}
131
132fn expand_match(
139 base: &Rope,
140 target: &Rope,
141 base_off: usize,
142 targ_off: usize,
143 prev_match_targ_end: usize,
144) -> (usize, usize) {
145 let mut scanner = RopeScanner::new(base, target);
146 let max_left = targ_off - prev_match_targ_end;
147 let start = scanner.find_ne_char_back(base_off, targ_off, max_left);
148 debug_assert!(start <= max_left, "{} <= {}", start, max_left);
149 let end = scanner.find_ne_char(base_off, targ_off, None);
150 (start.min(max_left), end)
151}
152
153fn longest_increasing_region_set(items: &[(usize, usize)]) -> Vec<(usize, usize)> {
157 let mut result = vec![0];
158 let mut prev_chain = vec![0; items.len()];
159
160 for i in 1..items.len() {
161 let last_idx = *result.last().unwrap();
164 if items[last_idx].1 < items[i].1 {
165 prev_chain[i] = last_idx;
166 result.push(i);
167 continue;
168 }
169
170 let next_idx = match result.binary_search_by(|&j| items[j].1.cmp(&items[i].1)) {
171 Ok(_) => continue, Err(idx) => idx,
173 };
174
175 if items[i].1 < items[result[next_idx]].1 {
176 if next_idx > 0 {
177 prev_chain[i] = result[next_idx - 1];
178 }
179 result[next_idx] = i;
180 }
181 }
182
183 let mut u = result.len();
185 let mut v = *result.last().unwrap();
186 while u != 0 {
187 u -= 1;
188 result[u] = v;
189 v = prev_chain[v];
190 }
191 result.iter().map(|i| items[*i]).collect()
192}
193
194#[inline]
195fn non_ws_offset(s: &str) -> usize {
196 s.as_bytes().iter().take_while(|b| **b == b' ' || **b == b'\t').count()
197}
198
199#[derive(Debug, Clone, Copy)]
201struct DiffOp {
202 target_idx: usize,
203 base_idx: usize,
204 len: usize,
205}
206
207#[derive(Debug, Clone, Default)]
209pub struct DiffBuilder {
210 ops: Vec<DiffOp>,
211}
212
213impl DiffBuilder {
214 fn copy(&mut self, base: usize, target: usize, len: usize) {
215 if let Some(prev) = self.ops.last_mut() {
216 let prev_end = prev.target_idx + prev.len;
217 let base_end = prev.base_idx + prev.len;
218 assert!(prev_end <= target, "{} <= {} prev {:?}", prev_end, target, prev);
219 if prev_end == target && base_end == base {
220 prev.len += len;
221 return;
222 }
223 }
224 self.ops.push(DiffOp { target_idx: target, base_idx: base, len })
225 }
226
227 fn to_delta(self, base: &Rope, target: &Rope) -> RopeDelta {
228 let mut els = Vec::with_capacity(self.ops.len() * 2);
229 let mut targ_pos = 0;
230 for DiffOp { base_idx, target_idx, len } in self.ops {
231 if target_idx > targ_pos {
232 let iv = Interval::new(targ_pos, target_idx);
233 els.push(DeltaElement::Insert(target.subseq(iv)));
234 }
235 els.push(DeltaElement::Copy(base_idx, base_idx + len));
236 targ_pos = target_idx + len;
237 }
238
239 if targ_pos < target.len() {
240 let iv = Interval::new(targ_pos, target.len());
241 els.push(DeltaElement::Insert(target.subseq(iv)));
242 }
243
244 Delta { els, base_len: base.len() }
245 }
246}
247
248fn make_line_hashes<'a>(base: &'a Rope, min_size: usize) -> HashMap<Cow<'a, str>, usize> {
251 let mut offset = 0;
252 let mut line_hashes = HashMap::with_capacity(base.len() / 60);
253 for line in base.lines_raw(..) {
254 let non_ws = non_ws_offset(&line);
255 if line.len() - non_ws >= min_size {
256 let cow = match line {
257 Cow::Owned(ref s) => Cow::Owned(s[non_ws..].to_string()),
258 Cow::Borrowed(s) => Cow::Borrowed(&s[non_ws..]),
259 };
260 line_hashes.insert(cow, offset + non_ws);
261 }
262 offset += line.len();
263 }
264 line_hashes
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 static SMALL_ONE: &str = "This adds FixedSizeAdler32, that has a size set at construction, and keeps bytes in a cyclic buffer of that size to be removed when it fills up.
272
273Current logic (and implementing Write) might be too much, since bytes will probably always be fed one by one anyway. Otherwise a faster way of removing a sequence might be needed (one by one is inefficient).";
274
275 static SMALL_TWO: &str = "This adds some function, I guess?, that has a size set at construction, and keeps bytes in a cyclic buffer of that size to be ground up and injested when it fills up.
276
277Currently my sense of smell (and the pain of implementing Write) might be too much, since bytes will probably always be fed one by one anyway. Otherwise crying might be needed (one by one is inefficient).";
278
279 static INTERVAL_STR: &str = include_str!("../src/interval.rs");
280 static BREAKS_STR: &str = include_str!("../src/breaks.rs");
281
282 #[test]
283 fn diff_smoke_test() {
284 let one = SMALL_ONE.into();
285 let two = SMALL_TWO.into();
286
287 let delta = LineHashDiff::compute_delta(&one, &two);
288 println!("delta: {:?}", &delta);
289
290 let result = delta.apply(&one);
291 assert_eq!(result, two);
292
293 let delta = LineHashDiff::compute_delta(&one, &two);
294 println!("delta: {:?}", &delta);
295
296 let result = delta.apply(&one);
297 assert_eq!(result, two);
298 }
299
300 #[test]
301 fn test_larger_diff() {
302 let one = INTERVAL_STR.into();
303 let two = BREAKS_STR.into();
304
305 let delta = LineHashDiff::compute_delta(&one, &two);
306 let result = delta.apply(&one);
307 assert_eq!(String::from(result), String::from(two));
308 }
309}