1712 lines · 58.9 KB · Rust
RawDownload
// 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::collections::BTreeMap;use std::hash::BuildHasher;use std::hash::Hash;use std::hash::Hasher;use std::hash::RandomState;use std::iter;use std::ops::Range;use std::slice; use bstr::BStr;use hashbrown::HashTable;use itertools::Itertools;use smallvec::smallvec;use smallvec::SmallVec; pub fn find_line_ranges(text: &[u8]) -> Vec<Range<usize>> {    text.split_inclusive(|b| *b == b'\n')        .scan(0, |total, line| {            let start = *total;            *total += line.len();            Some(start..*total)        })        .collect()} 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>> {    text.iter()        .positions(|b| !is_word_byte(*b))        .map(|i| i..i + 1)        .collect()} fn bytes_ignore_all_whitespace(text: &[u8]) -> impl Iterator<Item = u8> + '_ {    text.iter().copied().filter(|b| !b.is_ascii_whitespace())} fn bytes_ignore_whitespace_amount(text: &[u8]) -> impl Iterator<Item = u8> + '_ {    let mut prev_was_space = false;    text.iter().filter_map(move |&b| {        let was_space = prev_was_space;        let is_space = b.is_ascii_whitespace();        prev_was_space = is_space;        match (was_space, is_space) {            (_, false) => Some(b),            (false, true) => Some(b' '),            (true, true) => None,        }    })} fn hash_with_length_suffix<I, H>(data: I, state: &mut H)where    I: IntoIterator,    I::Item: Hash,    H: Hasher,{    let mut len: usize = 0;    for d in data {        d.hash(state);        len += 1;    }    state.write_usize(len);} /// Compares byte sequences based on a certain equivalence property.////// This isn't a newtype `Wrapper<'a>(&'a [u8])` but an external comparison/// object for the following reasons:////// a. If it were newtype, a generic `wrap` function would be needed. It///    couldn't be expressed as a simple closure:///    `for<'a> Fn(&'a [u8]) -> ???<'a>`/// b. Dynamic comparison object can be implemented intuitively. For example,///    `pattern: &Regex` would have to be copied to all newtype instances if it///    were newtype./// c. Hash values can be cached if hashing is controlled externally.pub trait CompareBytes {    /// Returns true if `left` and `right` are equivalent.    fn eq(&self, left: &[u8], right: &[u8]) -> bool;     /// Generates hash which respects the following property:    /// `eq(left, right) => hash(left) == hash(right)`    fn hash<H: Hasher>(&self, text: &[u8], state: &mut H);} // An instance might have e.g. Regex pattern, which can't be trivially copied.// Such comparison object can be passed by reference.impl<C: CompareBytes + ?Sized> CompareBytes for &C {    fn eq(&self, left: &[u8], right: &[u8]) -> bool {        <C as CompareBytes>::eq(self, left, right)    }     fn hash<H: Hasher>(&self, text: &[u8], state: &mut H) {        <C as CompareBytes>::hash(self, text, state);    }} /// Compares byte sequences literally.#[derive(Clone, Debug, Default)]pub struct CompareBytesExactly; impl CompareBytes for CompareBytesExactly {    fn eq(&self, left: &[u8], right: &[u8]) -> bool {        left == right    }     fn hash<H: Hasher>(&self, text: &[u8], state: &mut H) {        text.hash(state);    }} /// Compares byte sequences ignoring any whitespace occurrences.#[derive(Clone, Debug, Default)]pub struct CompareBytesIgnoreAllWhitespace; impl CompareBytes for CompareBytesIgnoreAllWhitespace {    fn eq(&self, left: &[u8], right: &[u8]) -> bool {        bytes_ignore_all_whitespace(left).eq(bytes_ignore_all_whitespace(right))    }     fn hash<H: Hasher>(&self, text: &[u8], state: &mut H) {        hash_with_length_suffix(bytes_ignore_all_whitespace(text), state);    }} /// Compares byte sequences ignoring changes in whitespace amount.#[derive(Clone, Debug, Default)]pub struct CompareBytesIgnoreWhitespaceAmount; impl CompareBytes for CompareBytesIgnoreWhitespaceAmount {    fn eq(&self, left: &[u8], right: &[u8]) -> bool {        bytes_ignore_whitespace_amount(left).eq(bytes_ignore_whitespace_amount(right))    }     fn hash<H: Hasher>(&self, text: &[u8], state: &mut H) {        hash_with_length_suffix(bytes_ignore_whitespace_amount(text), state);    }} /// Compares words (or tokens) under a certain hasher configuration.#[derive(Clone, Debug, Default)]struct WordComparator<C, S> {    compare: C,    hash_builder: S,} impl<C: CompareBytes> WordComparator<C, RandomState> {    fn new(compare: C) -> Self {        WordComparator {            compare,            // TODO: switch to ahash for better performance?            hash_builder: RandomState::new(),        }    }} impl<C: CompareBytes, S: BuildHasher> WordComparator<C, S> {    fn eq(&self, left: &[u8], right: &[u8]) -> bool {        self.compare.eq(left, right)    }     fn hash_one(&self, text: &[u8]) -> u64 {        let mut state = self.hash_builder.build_hasher();        self.compare.hash(text, &mut state);        state.finish()    }} /// Index in a list of word (or token) ranges.#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]struct WordPosition(usize); #[derive(Clone, Debug)]struct DiffSource<'input, 'aux> {    text: &'input BStr,    ranges: &'aux [Range<usize>],    /// The number of preceding word ranges excluded from the self `ranges`.    global_offset: WordPosition,} impl<'input, 'aux> DiffSource<'input, 'aux> {    fn new<T: AsRef<[u8]> + ?Sized>(text: &'input T, ranges: &'aux [Range<usize>]) -> Self {        DiffSource {            text: BStr::new(text),            ranges,            global_offset: WordPosition(0),        }    }     fn narrowed(&self, positions: Range<WordPosition>) -> Self {        DiffSource {            text: self.text,            ranges: &self.ranges[positions.start.0..positions.end.0],            global_offset: self.map_to_global(positions.start),        }    }     fn range_at(&self, position: WordPosition) -> Range<usize> {        self.ranges[position.0].clone()    }     fn map_to_global(&self, position: WordPosition) -> WordPosition {        WordPosition(self.global_offset.0 + position.0)    }} struct Histogram<'input> {    word_to_positions: HashTable<HistogramEntry<'input>>,} type HistogramEntry<'input> = (&'input BStr, Vec<WordPosition>); impl<'input> Histogram<'input> {    fn calculate<C: CompareBytes, S: BuildHasher>(        source: &DiffSource<'input, '_>,        comp: &WordComparator<C, S>,        max_occurrences: usize,    ) -> Self {        let mut word_to_positions: HashTable<HistogramEntry> = HashTable::new();        for (i, range) in source.ranges.iter().enumerate() {            let word = &source.text[range.clone()];            let hash = comp.hash_one(word);            let (_, positions) = word_to_positions                .entry(hash, |(w, _)| comp.eq(w, word), |(w, _)| comp.hash_one(w))                .or_insert_with(|| (word, vec![]))                .into_mut();            // Allow one more than max_occurrences, so we can later skip those with more            // than max_occurrences            if positions.len() <= max_occurrences {                positions.push(WordPosition(i));            }        }        Histogram { word_to_positions }    }     fn build_count_to_entries(&self) -> BTreeMap<usize, Vec<&HistogramEntry<'input>>> {        let mut count_to_entries: BTreeMap<usize, Vec<_>> = BTreeMap::new();        for entry in &self.word_to_positions {            let (_, positions) = entry;            let entries = count_to_entries.entry(positions.len()).or_default();            entries.push(entry);        }        count_to_entries    }     fn positions_by_word<C: CompareBytes, S: BuildHasher>(        &self,        word: &BStr,        comp: &WordComparator<C, S>,    ) -> Option<&[WordPosition]> {        let hash = comp.hash_one(word);        let (_, positions) = self            .word_to_positions            .find(hash, |(w, _)| comp.eq(w, word))?;        Some(positions)    }} /// 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 word (or token) positions among the ones given as/// arguments. The data between those words is ignored.fn collect_unchanged_words<C: CompareBytes, S: BuildHasher>(    found_positions: &mut Vec<(WordPosition, WordPosition)>,    left: &DiffSource,    right: &DiffSource,    comp: &WordComparator<C, S>,) {    if left.ranges.is_empty() || right.ranges.is_empty() {        return;    }     // Prioritize LCS-based algorithm than leading/trailing matches    let old_len = found_positions.len();    collect_unchanged_words_lcs(found_positions, left, right, comp);    if found_positions.len() != old_len {        return;    }     // Trim leading common ranges (i.e. grow previous unchanged region)    let common_leading_len = iter::zip(left.ranges, right.ranges)        .take_while(|&(l, r)| comp.eq(&left.text[l.clone()], &right.text[r.clone()]))        .count();    let left_ranges = &left.ranges[common_leading_len..];    let right_ranges = &right.ranges[common_leading_len..];     // Trim trailing common ranges (i.e. grow next unchanged region)    let common_trailing_len = iter::zip(left_ranges.iter().rev(), right_ranges.iter().rev())        .take_while(|&(l, r)| comp.eq(&left.text[l.clone()], &right.text[r.clone()]))        .count();     found_positions.extend(itertools::chain(        (0..common_leading_len).map(|i| {            (                left.map_to_global(WordPosition(i)),                right.map_to_global(WordPosition(i)),            )        }),        (1..=common_trailing_len).rev().map(|i| {            (                left.map_to_global(WordPosition(left.ranges.len() - i)),                right.map_to_global(WordPosition(right.ranges.len() - i)),            )        }),    ));} fn collect_unchanged_words_lcs<C: CompareBytes, S: BuildHasher>(    found_positions: &mut Vec<(WordPosition, WordPosition)>,    left: &DiffSource,    right: &DiffSource,    comp: &WordComparator<C, S>,) {    let max_occurrences = 100;    let left_histogram = Histogram::calculate(left, comp, max_occurrences);    let left_count_to_entries = left_histogram.build_count_to_entries();    if *left_count_to_entries.keys().next().unwrap() > max_occurrences {        // If there are very many occurrences of all words, then we just give up.        return;    }    let right_histogram = Histogram::calculate(right, comp, 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 Some(uncommon_shared_word_positions) =        left_count_to_entries.values().find_map(|left_entries| {            let mut both_positions = left_entries                .iter()                .filter_map(|&(word, left_positions)| {                    let right_positions = right_histogram.positions_by_word(word, comp)?;                    (left_positions.len() == right_positions.len())                        .then_some((left_positions, right_positions))                })                .peekable();            both_positions.peek().is_some().then_some(both_positions)        })    else {        return;    };     // [(index into ranges, serial to identify {word, occurrence #})]    let (mut left_positions, mut right_positions): (Vec<_>, Vec<_>) =        uncommon_shared_word_positions            .flat_map(|(lefts, rights)| iter::zip(lefts, rights))            .enumerate()            .map(|(serial, (&left_pos, &right_pos))| ((left_pos, serial), (right_pos, serial)))            .unzip();    left_positions.sort_unstable_by_key(|&(pos, _serial)| pos);    right_positions.sort_unstable_by_key(|&(pos, _serial)| pos);    let left_index_by_right_index: Vec<usize> = {        let mut left_index_map = vec![0; left_positions.len()];        for (i, &(_pos, serial)) in left_positions.iter().enumerate() {            left_index_map[serial] = i;        }        right_positions            .iter()            .map(|&(_pos, serial)| left_index_map[serial])            .collect()    };     let lcs = find_lcs(&left_index_by_right_index);     // Produce output word positions, recursing into the modified areas between    // the elements in the LCS.    let mut previous_left_position = WordPosition(0);    let mut previous_right_position = WordPosition(0);    for (left_index, right_index) in lcs {        let (left_position, _) = left_positions[left_index];        let (right_position, _) = right_positions[right_index];        collect_unchanged_words(            found_positions,            &left.narrowed(previous_left_position..left_position),            &right.narrowed(previous_right_position..right_position),            comp,        );        found_positions.push((            left.map_to_global(left_position),            right.map_to_global(right_position),        ));        previous_left_position = WordPosition(left_position.0 + 1);        previous_right_position = WordPosition(right_position.0 + 1);    }    // Also recurse into range at end (after common ranges).    collect_unchanged_words(        found_positions,        &left.narrowed(previous_left_position..WordPosition(left.ranges.len())),        &right.narrowed(previous_right_position..WordPosition(right.ranges.len())),        comp,    );} /// Intersects two sorted sequences of `(base, other)` word positions by/// `base`. `base` positions should refer to the same source text.fn intersect_unchanged_words(    current_positions: Vec<(WordPosition, Vec<WordPosition>)>,    new_positions: &[(WordPosition, WordPosition)],) -> Vec<(WordPosition, Vec<WordPosition>)> {    itertools::merge_join_by(        current_positions,        new_positions,

Lines 1–500 of 1712.