Skip to main content

xi_rope/
diff.rs

1// Copyright 2018 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Computing deltas between two ropes.
16
17use 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
26/// A trait implemented by various diffing strategies.
27pub trait Diff<N: NodeInfo> {
28    fn compute_delta(base: &Node<N>, target: &Node<N>) -> Delta<N>;
29}
30
31/// The minimum length of non-whitespace characters in a line before
32/// we consider it for diffing purposes.
33const MIN_SIZE: usize = 32;
34
35/// A line-oriented, hash based diff algorithm.
36///
37/// This works by taking a hash of each line in either document that
38/// has a length, ignoring leading whitespace, above some threshold.
39///
40/// Lines in the target document are matched against lines in the
41/// base document. When a match is found, it is extended forwards
42/// and backwards as far as possible.
43///
44/// This runs in O(n+m) in the lengths of the two ropes, and produces
45/// results on a variety of workloads that are comparable in quality
46/// (measured in terms of serialized diff size) with the results from
47/// using a suffix array, while being an order of magnitude faster.
48pub 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        // before doing anything, scan top down and bottom up for like-ness.
55        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 our preliminary scan finds no differences we're done
64        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        // we now have an ordered list of matches and their positions.
93        // to ensure that our delta only copies non-decreasing base regions,
94        // we take the longest increasing subsequence.
95        // TODO: a possible optimization here would be to expand matches
96        // to adjacent lines first? this would be at best a small win though..
97
98        let longest_subseq =
99            if needs_subseq { longest_increasing_region_set(&matches) } else { matches };
100
101        // for each matching region, we extend it forwards and backwards.
102        // we keep track of how far forward we extend it each time, to avoid
103        // having a subsequent scan extend backwards over the same region.
104        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            // don't let last match expand past target_end
114            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
132/// Given two ropes and the offsets of two equal bytes, finds the largest
133/// identical substring shared between the two ropes which contains the offset.
134///
135/// The return value is a pair of offsets, each of which represents an absolute
136/// distance. That is to say, the position of the start and end boundaries
137/// relative to the input offset.
138fn 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
153/// Finds the longest increasing subset of copyable regions. This is essentially
154/// the longest increasing subsequence problem. This implementation is adapted
155/// from https://codereview.stackexchange.com/questions/187337/longest-increasing-subsequence-algorithm
156fn 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        // If the next item is greater than the last item of the current longest
162        // subsequence, push its index at the end of the result and continue.
163        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, // we ignore duplicates
172            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    // walk backwards from the last item in result to build the final sequence
184    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/// Represents copying `len` bytes from base to target.
200#[derive(Debug, Clone, Copy)]
201struct DiffOp {
202    target_idx: usize,
203    base_idx: usize,
204    len: usize,
205}
206
207/// Keeps track of copy ops during diff construction.
208#[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
248/// Creates a map of lines to offsets, ignoring trailing whitespace, and only for those lines
249/// where line.len() >= min_size. Offsets refer to the first non-whitespace byte in the line.
250fn 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}