// Copyright 2021 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. #![allow(missing_docs)] use std::cmp::{max, min, Ordering};use std::collections::{BTreeMap, HashMap};use std::fmt::{Debug, Formatter};use std::ops::Range;use std::slice; use itertools::Itertools; pub fn find_line_ranges(text: &[u8]) -> Vec<Range<usize>> { let mut ranges = vec![]; let mut start = 0; loop { match text[start..].iter().position(|b| *b == b'\n') { None => { break; } Some(i) => { ranges.push(start..start + i + 1); start += i + 1; } } } if start < text.len() { ranges.push(start..text.len()); } ranges} fn is_word_byte(b: u8) -> bool { // TODO: Make this configurable (probably higher up in the call stack) matches!( b, // Count 0x80..0xff as word bytes so multi-byte UTF-8 chars are // treated as a single unit. b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'\x80'..=b'\xff' )} pub fn find_word_ranges(text: &[u8]) -> Vec<Range<usize>> { let mut word_ranges = vec![]; let mut word_start_pos = 0; let mut in_word = false; for (i, b) in text.iter().enumerate() { if in_word && !is_word_byte(*b) { in_word = false; word_ranges.push(word_start_pos..i); word_start_pos = i; } else if !in_word && is_word_byte(*b) { in_word = true; word_start_pos = i; } } if in_word && word_start_pos < text.len() { word_ranges.push(word_start_pos..text.len()); } word_ranges} pub fn find_nonword_ranges(text: &[u8]) -> Vec<Range<usize>> { let mut ranges = vec![]; for (i, b) in text.iter().enumerate() { if !is_word_byte(*b) { ranges.push(i..i + 1); } } ranges} struct Histogram<'a> { word_to_positions: HashMap<&'a [u8], Vec<usize>>, count_to_words: BTreeMap<usize, Vec<&'a [u8]>>,} impl Histogram<'_> { fn calculate<'a>( text: &'a [u8], ranges: &[Range<usize>], max_occurrences: usize, ) -> Histogram<'a> { let mut word_to_positions: HashMap<&[u8], Vec<usize>> = HashMap::new(); for (i, range) in ranges.iter().enumerate() { let positions = word_to_positions.entry(&text[range.clone()]).or_default(); // Allow one more than max_occurrences, so we can later skip those with more // than max_occurrences if positions.len() <= max_occurrences { positions.push(i); } } let mut count_to_words: BTreeMap<usize, Vec<&[u8]>> = BTreeMap::new(); for (word, ranges) in &word_to_positions { count_to_words.entry(ranges.len()).or_default().push(word); } Histogram { word_to_positions, count_to_words, } }} /// Finds the LCS given a array where the value of `input[i]` indicates that/// the position of element `i` in the right array is at position `input[i]` in/// the left array.////// For example (some have multiple valid outputs):////// [0,1,2] => [(0,0),(1,1),(2,2)]/// [2,1,0] => [(0,2)]/// [0,1,4,2,3,5,6] => [(0,0),(1,1),(2,3),(3,4),(5,5),(6,6)]/// [0,1,4,3,2,5,6] => [(0,0),(1,1),(4,2),(5,5),(6,6)]fn find_lcs(input: &[usize]) -> Vec<(usize, usize)> { if input.is_empty() { return vec![]; } let mut chain = vec![(0, 0, 0); input.len()]; let mut global_longest = 0; let mut global_longest_right_pos = 0; for (right_pos, &left_pos) in input.iter().enumerate() { let mut longest_from_here = 1; let mut previous_right_pos = usize::MAX; for i in (0..right_pos).rev() { let (previous_len, previous_left_pos, _) = chain[i]; if previous_left_pos < left_pos { let len = previous_len + 1; if len > longest_from_here { longest_from_here = len; previous_right_pos = i; if len > global_longest { global_longest = len; global_longest_right_pos = right_pos; // If this is the longest chain globally so far, we cannot find a // longer one by using a previous value, so break early. break; } } } } chain[right_pos] = (longest_from_here, left_pos, previous_right_pos); } let mut result = vec![]; let mut right_pos = global_longest_right_pos; loop { let (_, left_pos, previous_right_pos) = chain[right_pos]; result.push((left_pos, right_pos)); if previous_right_pos == usize::MAX { break; } right_pos = previous_right_pos; } result.reverse(); result} /// Finds unchanged ranges among the ones given as arguments. The data between/// those ranges is ignored.pub(crate) fn unchanged_ranges( left: &[u8], right: &[u8], left_ranges: &[Range<usize>], right_ranges: &[Range<usize>],) -> Vec<(Range<usize>, Range<usize>)> { if left_ranges.is_empty() || right_ranges.is_empty() { return vec![]; } let max_occurrences = 100; let mut left_histogram = Histogram::calculate(left, left_ranges, max_occurrences); if *left_histogram.count_to_words.keys().next().unwrap() > max_occurrences { // If there are very many occurrences of all words, then we just give up. return vec![]; } let mut right_histogram = Histogram::calculate(right, right_ranges, max_occurrences); // Look for words with few occurrences in `left` (could equally well have picked // `right`?). If any of them also occur in `right`, then we add the words to // the LCS. let mut uncommon_shared_words = vec![]; while !left_histogram.count_to_words.is_empty() && uncommon_shared_words.is_empty() { let left_words = left_histogram .count_to_words .first_entry() .map(|x| x.remove()) .unwrap(); for left_word in left_words { if right_histogram.word_to_positions.contains_key(left_word) { uncommon_shared_words.push(left_word); } } } if uncommon_shared_words.is_empty() { return vec![]; } // Let's say our inputs are "a b a b" and "a b c c b a b". We will have found // the least common words to be "a" and "b". We now assume that each // occurrence of each word lines up in the left and right input. We do that // by numbering the shared occurrences, effectively instead comparing "a1 b1 // a2 b2" and "a1 b1 c c b2 a2 b". We then walk the common words in the // right input in order (["a1", "b1", "b2", "a2"]), and record the index of // that word in the left input ([0,1,3,2]). We then find the LCS and split // points based on that ([0,1,3] or [0,1,2] are both valid). // [(index into left_ranges, word, occurrence #)] let mut left_positions = vec![]; let mut right_positions = vec![]; for uncommon_shared_word in uncommon_shared_words { let left_occurrences = left_histogram .word_to_positions .get_mut(uncommon_shared_word) .unwrap(); let right_occurrences = right_histogram .word_to_positions .get_mut(uncommon_shared_word) .unwrap(); let shared_count = min(left_occurrences.len(), right_occurrences.len()); for occurrence in 0..shared_count { left_positions.push(( left_occurrences[occurrence], uncommon_shared_word, occurrence, )); right_positions.push(( right_occurrences[occurrence], uncommon_shared_word, occurrence, )); } } left_positions.sort(); right_positions.sort(); let mut left_position_map = HashMap::new(); for (i, (_pos, word, occurrence)) in left_positions.iter().enumerate() { left_position_map.insert((*word, *occurrence), i); } let mut left_index_by_right_index = vec![]; for (_pos, word, occurrence) in &right_positions { left_index_by_right_index.push(*left_position_map.get(&(*word, *occurrence)).unwrap()); } let lcs = find_lcs(&left_index_by_right_index); // Produce output ranges, recursing into the modified areas between the elements // in the LCS. let mut result = vec![]; let mut previous_left_position = 0; let mut previous_right_position = 0; for (left_index, right_index) in lcs { let left_position = left_positions[left_index].0; let right_position = right_positions[right_index].0; let skipped_left_positions = previous_left_position..left_position; let skipped_right_positions = previous_right_position..right_position; if !skipped_left_positions.is_empty() || !skipped_right_positions.is_empty() { for unchanged_nested_range in unchanged_ranges( left, right, &left_ranges[skipped_left_positions.clone()], &right_ranges[skipped_right_positions.clone()], ) { result.push(unchanged_nested_range); } } result.push(( left_ranges[left_position].clone(), right_ranges[right_position].clone(), )); previous_left_position = left_position + 1; previous_right_position = right_position + 1; } // Also recurse into range at end (after common ranges). let skipped_left_positions = previous_left_position..left_ranges.len(); let skipped_right_positions = previous_right_position..right_ranges.len(); if !skipped_left_positions.is_empty() || !skipped_right_positions.is_empty() { for unchanged_nested_range in unchanged_ranges( left, right, &left_ranges[skipped_left_positions], &right_ranges[skipped_right_positions], ) { result.push(unchanged_nested_range); } } result} #[derive(Clone, PartialEq, Eq, Debug)]struct UnchangedRange { base_range: Range<usize>, offsets: Vec<isize>,} impl UnchangedRange { fn start(&self, side: usize) -> usize { self.base_range .start .wrapping_add(self.offsets[side] as usize) } fn end(&self, side: usize) -> usize { self.base_range .end .wrapping_add(self.offsets[side] as usize) }} impl PartialOrd for UnchangedRange { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }} impl Ord for UnchangedRange { fn cmp(&self, other: &Self) -> Ordering { self.base_range .start .cmp(&other.base_range.start) .then_with(|| self.base_range.end.cmp(&other.base_range.end)) }} /// Takes any number of inputs and finds regions that are them same between all/// of them.#[derive(Clone, Debug)]pub struct Diff<'input> { base_input: &'input [u8], other_inputs: Vec<&'input [u8]>, // The key is a range in the base input. The value is the start of each non-base region // relative to the base region's start. By making them relative, they don't need to change // when the base range changes. unchanged_regions: Vec<UnchangedRange>,} /// Takes the current regions and intersects it with the new unchanged ranges/// from a 2-way diff. The result is a map of unchanged regions with one more/// offset in the map's values.fn intersect_regions( current_ranges: Vec<UnchangedRange>, new_unchanged_ranges: &[(Range<usize>, Range<usize>)],) -> Vec<UnchangedRange> { let mut result = vec![]; let mut current_ranges_iter = current_ranges.into_iter().peekable(); for (new_base_range, other_range) in new_unchanged_ranges.iter() { assert_eq!(new_base_range.len(), other_range.len()); while let Some(UnchangedRange { base_range, offsets, }) = current_ranges_iter.peek() { // No need to look further if we're past the new range. if base_range.start >= new_base_range.end { break; } // Discard any current unchanged regions that don't match between the base and // the new input. if base_range.end <= new_base_range.start { current_ranges_iter.next(); continue; } let new_start = max(base_range.start, new_base_range.start); let new_end = min(base_range.end, new_base_range.end); let mut new_offsets = offsets.clone(); new_offsets.push(other_range.start.wrapping_sub(new_base_range.start) as isize); result.push(UnchangedRange { base_range: new_start..new_end, offsets: new_offsets, }); if base_range.end >= new_base_range.end { // Break without consuming the item; there may be other new ranges that overlap // with it. break; } current_ranges_iter.next(); } } result} impl<'input> Diff<'input> { pub fn for_tokenizer( inputs: &[&'input [u8]], tokenizer: &impl Fn(&[u8]) -> Vec<Range<usize>>, ) -> Self { assert!(!inputs.is_empty()); let base_input = inputs[0]; let other_inputs = inputs.iter().skip(1).copied().collect_vec(); // First tokenize each input let base_token_ranges: Vec<Range<usize>> = tokenizer(base_input); let other_token_ranges: Vec<Vec<Range<usize>>> = other_inputs .iter() .map(|other_input| tokenizer(other_input)) .collect_vec(); // Look for unchanged regions. Initially consider the whole range of the base // input as unchanged (compared to itself). Then diff each other input // against the base. Intersect the previously found ranges with the // unchanged ranges in the diff. let mut unchanged_regions = vec![UnchangedRange { base_range: 0..base_input.len(), offsets: vec![], }]; for (i, other_token_ranges) in other_token_ranges.iter().enumerate() { let unchanged_diff_ranges = unchanged_ranges( base_input, other_inputs[i], &base_token_ranges, other_token_ranges, ); unchanged_regions = intersect_regions(unchanged_regions, &unchanged_diff_ranges); } // Add an empty range at the end to make life easier for hunks(). let offsets = other_inputs .iter() .map(|input| input.len().wrapping_sub(base_input.len()) as isize) .collect_vec(); unchanged_regions.push(UnchangedRange { base_range: base_input.len()..base_input.len(), offsets, }); let mut diff = Self { base_input, other_inputs, unchanged_regions, }; diff.compact_unchanged_regions(); diff } pub fn unrefined(inputs: &[&'input [u8]]) -> Self { Diff::for_tokenizer(inputs, &|_| vec![]) } // TODO: At least when merging, it's wasteful to refine the diff if e.g. if 2 // out of 3 inputs match in the differing regions. Perhaps the refine() // method should be on the hunk instead (probably returning a new Diff)? // That would let each user decide which hunks to refine. However, it would // probably mean that many callers repeat the same code. Perhaps it // should be possible to refine a whole diff *or* individual hunks. pub fn default_refinement(inputs: &[&'input [u8]]) -> Self { let mut diff = Diff::for_tokenizer(inputs, &find_line_ranges); diff.refine_changed_regions(&find_word_ranges); diff.refine_changed_regions(&find_nonword_ranges); diff } pub fn hunks<'diff>(&'diff self) -> DiffHunkIterator<'diff, 'input> { let previous_offsets = vec![0; self.other_inputs.len()]; DiffHunkIterator { diff: self, previous: UnchangedRange { base_range: 0..0, offsets: previous_offsets, }, unchanged_emitted: true, unchanged_iter: self.unchanged_regions.iter(), } } /// Uses the given tokenizer to split the changed regions into smaller /// regions. Then tries to finds unchanged regions among them. pub fn refine_changed_regions(&mut self, tokenizer: &impl Fn(&[u8]) -> Vec<Range<usize>>) { let mut previous = UnchangedRange { base_range: 0..0, offsets: vec![0; self.other_inputs.len()], }; let mut new_unchanged_ranges = vec![]; for current in self.unchanged_regions.iter() { // For the changed region between the previous region and the current one, // create a new Diff instance. Then adjust the start positions and // offsets to be valid in the context of the larger Diff instance // (`self`). let mut slices = vec![&self.base_input[previous.base_range.end..current.base_range.start]]; for i in 0..current.offsets.len() { let changed_range = previous.end(i)..current.start(i); slices.push(&self.other_inputs[i][changed_range]); } let refined_diff = Diff::for_tokenizer(&slices, tokenizer); for UnchangedRange { base_range, offsets,Lines 1–500 of 1191. Load more
// Copyright 2021 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![allow(missing_docs)] use std::cmp::{max, min, Ordering}; use std::collections::{BTreeMap, HashMap}; use std::fmt::{Debug, Formatter}; use std::ops::Range; use std::slice; use itertools::Itertools; pub fn find_line_ranges(text: &[u8]) -> Vec<Range<usize>> { let mut ranges = vec![]; let mut start = 0; loop { match text[start..].iter().position(|b| *b == b'\n') { None => { break; } Some(i) => { ranges.push(start..start + i + 1); start += i + 1; } } } if start < text.len() { ranges.push(start..text.len()); } ranges } fn is_word_byte(b: u8) -> bool { // TODO: Make this configurable (probably higher up in the call stack) matches!( b, // Count 0x80..0xff as word bytes so multi-byte UTF-8 chars are // treated as a single unit. b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'\x80'..=b'\xff' ) } pub fn find_word_ranges(text: &[u8]) -> Vec<Range<usize>> { let mut word_ranges = vec![]; let mut word_start_pos = 0; let mut in_word = false; for (i, b) in text.iter().enumerate() { if in_word && !is_word_byte(*b) { in_word = false; word_ranges.push(word_start_pos..i); word_start_pos = i; } else if !in_word && is_word_byte(*b) { in_word = true; word_start_pos = i; } } if in_word && word_start_pos < text.len() { word_ranges.push(word_start_pos..text.len()); } word_ranges } pub fn find_nonword_ranges(text: &[u8]) -> Vec<Range<usize>> { let mut ranges = vec![]; for (i, b) in text.iter().enumerate() { if !is_word_byte(*b) { ranges.push(i..i + 1); } } ranges } struct Histogram<'a> { word_to_positions: HashMap<&'a [u8], Vec<usize>>, count_to_words: BTreeMap<usize, Vec<&'a [u8]>>, } impl Histogram<'_> { fn calculate<'a>( text: &'a [u8], ranges: &[Range<usize>], max_occurrences: usize, ) -> Histogram<'a> { let mut word_to_positions: HashMap<&[u8], Vec<usize>> = HashMap::new(); for (i, range) in ranges.iter().enumerate() { let positions = word_to_positions.entry(&text[range.clone()]).or_default(); // Allow one more than max_occurrences, so we can later skip those with more // than max_occurrences if positions.len() <= max_occurrences { positions.push(i); } } let mut count_to_words: BTreeMap<usize, Vec<&[u8]>> = BTreeMap::new(); for (word, ranges) in &word_to_positions { count_to_words.entry(ranges.len()).or_default().push(word); } Histogram { word_to_positions, count_to_words, } } } /// Finds the LCS given a array where the value of `input[i]` indicates that /// the position of element `i` in the right array is at position `input[i]` in /// the left array. /// /// For example (some have multiple valid outputs): /// /// [0,1,2] => [(0,0),(1,1),(2,2)] /// [2,1,0] => [(0,2)] /// [0,1,4,2,3,5,6] => [(0,0),(1,1),(2,3),(3,4),(5,5),(6,6)] /// [0,1,4,3,2,5,6] => [(0,0),(1,1),(4,2),(5,5),(6,6)] fn find_lcs(input: &[usize]) -> Vec<(usize, usize)> { if input.is_empty() { return vec![]; } let mut chain = vec![(0, 0, 0); input.len()]; let mut global_longest = 0; let mut global_longest_right_pos = 0; for (right_pos, &left_pos) in input.iter().enumerate() { let mut longest_from_here = 1; let mut previous_right_pos = usize::MAX; for i in (0..right_pos).rev() { let (previous_len, previous_left_pos, _) = chain[i]; if previous_left_pos < left_pos { let len = previous_len + 1; if len > longest_from_here { longest_from_here = len; previous_right_pos = i; if len > global_longest { global_longest = len; global_longest_right_pos = right_pos; // If this is the longest chain globally so far, we cannot find a // longer one by using a previous value, so break early. break; } } } } chain[right_pos] = (longest_from_here, left_pos, previous_right_pos); } let mut result = vec![]; let mut right_pos = global_longest_right_pos; loop { let (_, left_pos, previous_right_pos) = chain[right_pos]; result.push((left_pos, right_pos)); if previous_right_pos == usize::MAX { break; } right_pos = previous_right_pos; } result.reverse(); result } /// Finds unchanged ranges among the ones given as arguments. The data between /// those ranges is ignored. pub(crate) fn unchanged_ranges( left: &[u8], right: &[u8], left_ranges: &[Range<usize>], right_ranges: &[Range<usize>], ) -> Vec<(Range<usize>, Range<usize>)> { if left_ranges.is_empty() || right_ranges.is_empty() { return vec![]; } let max_occurrences = 100; let mut left_histogram = Histogram::calculate(left, left_ranges, max_occurrences); if *left_histogram.count_to_words.keys().next().unwrap() > max_occurrences { // If there are very many occurrences of all words, then we just give up. return vec![]; } let mut right_histogram = Histogram::calculate(right, right_ranges, max_occurrences); // Look for words with few occurrences in `left` (could equally well have picked // `right`?). If any of them also occur in `right`, then we add the words to // the LCS. let mut uncommon_shared_words = vec![]; while !left_histogram.count_to_words.is_empty() && uncommon_shared_words.is_empty() { let left_words = left_histogram .count_to_words .first_entry() .map(|x| x.remove()) .unwrap(); for left_word in left_words { if right_histogram.word_to_positions.contains_key(left_word) { uncommon_shared_words.push(left_word); } } } if uncommon_shared_words.is_empty() { return vec![]; } // Let's say our inputs are "a b a b" and "a b c c b a b". We will have found // the least common words to be "a" and "b". We now assume that each // occurrence of each word lines up in the left and right input. We do that // by numbering the shared occurrences, effectively instead comparing "a1 b1 // a2 b2" and "a1 b1 c c b2 a2 b". We then walk the common words in the // right input in order (["a1", "b1", "b2", "a2"]), and record the index of // that word in the left input ([0,1,3,2]). We then find the LCS and split // points based on that ([0,1,3] or [0,1,2] are both valid). // [(index into left_ranges, word, occurrence #)] let mut left_positions = vec![]; let mut right_positions = vec![]; for uncommon_shared_word in uncommon_shared_words { let left_occurrences = left_histogram .word_to_positions .get_mut(uncommon_shared_word) .unwrap(); let right_occurrences = right_histogram .word_to_positions .get_mut(uncommon_shared_word) .unwrap(); let shared_count = min(left_occurrences.len(), right_occurrences.len()); for occurrence in 0..shared_count { left_positions.push(( left_occurrences[occurrence], uncommon_shared_word, occurrence, )); right_positions.push(( right_occurrences[occurrence], uncommon_shared_word, occurrence, )); } } left_positions.sort(); right_positions.sort(); let mut left_position_map = HashMap::new(); for (i, (_pos, word, occurrence)) in left_positions.iter().enumerate() { left_position_map.insert((*word, *occurrence), i); } let mut left_index_by_right_index = vec![]; for (_pos, word, occurrence) in &right_positions { left_index_by_right_index.push(*left_position_map.get(&(*word, *occurrence)).unwrap()); } let lcs = find_lcs(&left_index_by_right_index); // Produce output ranges, recursing into the modified areas between the elements // in the LCS. let mut result = vec![]; let mut previous_left_position = 0; let mut previous_right_position = 0; for (left_index, right_index) in lcs { let left_position = left_positions[left_index].0; let right_position = right_positions[right_index].0; let skipped_left_positions = previous_left_position..left_position; let skipped_right_positions = previous_right_position..right_position; if !skipped_left_positions.is_empty() || !skipped_right_positions.is_empty() { for unchanged_nested_range in unchanged_ranges( left, right, &left_ranges[skipped_left_positions.clone()], &right_ranges[skipped_right_positions.clone()], ) { result.push(unchanged_nested_range); } } result.push(( left_ranges[left_position].clone(), right_ranges[right_position].clone(), )); previous_left_position = left_position + 1; previous_right_position = right_position + 1; } // Also recurse into range at end (after common ranges). let skipped_left_positions = previous_left_position..left_ranges.len(); let skipped_right_positions = previous_right_position..right_ranges.len(); if !skipped_left_positions.is_empty() || !skipped_right_positions.is_empty() { for unchanged_nested_range in unchanged_ranges( left, right, &left_ranges[skipped_left_positions], &right_ranges[skipped_right_positions], ) { result.push(unchanged_nested_range); } } result } #[derive(Clone, PartialEq, Eq, Debug)] struct UnchangedRange { base_range: Range<usize>, offsets: Vec<isize>, } impl UnchangedRange { fn start(&self, side: usize) -> usize { self.base_range .start .wrapping_add(self.offsets[side] as usize) } fn end(&self, side: usize) -> usize { self.base_range .end .wrapping_add(self.offsets[side] as usize) } } impl PartialOrd for UnchangedRange { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl Ord for UnchangedRange { fn cmp(&self, other: &Self) -> Ordering { self.base_range .start .cmp(&other.base_range.start) .then_with(|| self.base_range.end.cmp(&other.base_range.end)) } } /// Takes any number of inputs and finds regions that are them same between all /// of them. #[derive(Clone, Debug)] pub struct Diff<'input> { base_input: &'input [u8], other_inputs: Vec<&'input [u8]>, // The key is a range in the base input. The value is the start of each non-base region // relative to the base region's start. By making them relative, they don't need to change // when the base range changes. unchanged_regions: Vec<UnchangedRange>, } /// Takes the current regions and intersects it with the new unchanged ranges /// from a 2-way diff. The result is a map of unchanged regions with one more /// offset in the map's values. fn intersect_regions( current_ranges: Vec<UnchangedRange>, new_unchanged_ranges: &[(Range<usize>, Range<usize>)], ) -> Vec<UnchangedRange> { let mut result = vec![]; let mut current_ranges_iter = current_ranges.into_iter().peekable(); for (new_base_range, other_range) in new_unchanged_ranges.iter() { assert_eq!(new_base_range.len(), other_range.len()); while let Some(UnchangedRange { base_range, offsets, }) = current_ranges_iter.peek() { // No need to look further if we're past the new range. if base_range.start >= new_base_range.end { break; } // Discard any current unchanged regions that don't match between the base and // the new input. if base_range.end <= new_base_range.start { current_ranges_iter.next(); continue; } let new_start = max(base_range.start, new_base_range.start); let new_end = min(base_range.end, new_base_range.end); let mut new_offsets = offsets.clone(); new_offsets.push(other_range.start.wrapping_sub(new_base_range.start) as isize); result.push(UnchangedRange { base_range: new_start..new_end, offsets: new_offsets, }); if base_range.end >= new_base_range.end { // Break without consuming the item; there may be other new ranges that overlap // with it. break; } current_ranges_iter.next(); } } result } impl<'input> Diff<'input> { pub fn for_tokenizer( inputs: &[&'input [u8]], tokenizer: &impl Fn(&[u8]) -> Vec<Range<usize>>, ) -> Self { assert!(!inputs.is_empty()); let base_input = inputs[0]; let other_inputs = inputs.iter().skip(1).copied().collect_vec(); // First tokenize each input let base_token_ranges: Vec<Range<usize>> = tokenizer(base_input); let other_token_ranges: Vec<Vec<Range<usize>>> = other_inputs .iter() .map(|other_input| tokenizer(other_input)) .collect_vec(); // Look for unchanged regions. Initially consider the whole range of the base // input as unchanged (compared to itself). Then diff each other input // against the base. Intersect the previously found ranges with the // unchanged ranges in the diff. let mut unchanged_regions = vec![UnchangedRange { base_range: 0..base_input.len(), offsets: vec![], }]; for (i, other_token_ranges) in other_token_ranges.iter().enumerate() { let unchanged_diff_ranges = unchanged_ranges( base_input, other_inputs[i], &base_token_ranges, other_token_ranges, ); unchanged_regions = intersect_regions(unchanged_regions, &unchanged_diff_ranges); } // Add an empty range at the end to make life easier for hunks(). let offsets = other_inputs .iter() .map(|input| input.len().wrapping_sub(base_input.len()) as isize) .collect_vec(); unchanged_regions.push(UnchangedRange { base_range: base_input.len()..base_input.len(), offsets, }); let mut diff = Self { base_input, other_inputs, unchanged_regions, }; diff.compact_unchanged_regions(); diff } pub fn unrefined(inputs: &[&'input [u8]]) -> Self { Diff::for_tokenizer(inputs, &|_| vec![]) } // TODO: At least when merging, it's wasteful to refine the diff if e.g. if 2 // out of 3 inputs match in the differing regions. Perhaps the refine() // method should be on the hunk instead (probably returning a new Diff)? // That would let each user decide which hunks to refine. However, it would // probably mean that many callers repeat the same code. Perhaps it // should be possible to refine a whole diff *or* individual hunks. pub fn default_refinement(inputs: &[&'input [u8]]) -> Self { let mut diff = Diff::for_tokenizer(inputs, &find_line_ranges); diff.refine_changed_regions(&find_word_ranges); diff.refine_changed_regions(&find_nonword_ranges); diff } pub fn hunks<'diff>(&'diff self) -> DiffHunkIterator<'diff, 'input> { let previous_offsets = vec![0; self.other_inputs.len()]; DiffHunkIterator { diff: self, previous: UnchangedRange { base_range: 0..0, offsets: previous_offsets, }, unchanged_emitted: true, unchanged_iter: self.unchanged_regions.iter(), } } /// Uses the given tokenizer to split the changed regions into smaller /// regions. Then tries to finds unchanged regions among them. pub fn refine_changed_regions(&mut self, tokenizer: &impl Fn(&[u8]) -> Vec<Range<usize>>) { let mut previous = UnchangedRange { base_range: 0..0, offsets: vec![0; self.other_inputs.len()], }; let mut new_unchanged_ranges = vec![]; for current in self.unchanged_regions.iter() { // For the changed region between the previous region and the current one, // create a new Diff instance. Then adjust the start positions and // offsets to be valid in the context of the larger Diff instance // (`self`). let mut slices = vec![&self.base_input[previous.base_range.end..current.base_range.start]]; for i in 0..current.offsets.len() { let changed_range = previous.end(i)..current.start(i); slices.push(&self.other_inputs[i][changed_range]); } let refined_diff = Diff::for_tokenizer(&slices, tokenizer); for UnchangedRange { base_range, offsets,