1776 lines · 60.6 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 as _;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> + use<'_> {    text.iter().copied().filter(|b| !b.is_ascii_whitespace())} fn bytes_ignore_whitespace_amount(text: &[u8]) -> impl Iterator<Item = u8> + use<'_> {    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);    }} // Not implementing Eq because the text should be compared by WordComparator.#[derive(Clone, Copy, Debug)]struct HashedWord<'input> {    hash: u64,    text: &'input BStr,} /// 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 {        Self {            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 eq_hashed(&self, left: HashedWord<'_>, right: HashedWord<'_>) -> bool {        left.hash == right.hash && self.compare.eq(left.text, right.text)    }     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 in `DiffSource`.#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]struct WordPosition(usize); /// Index in a list of word (or token) ranges in `LocalDiffSource`.#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]struct LocalWordPosition(usize); #[derive(Clone, Debug)]struct DiffSource<'input, 'aux> {    text: &'input BStr,    ranges: &'aux [Range<usize>],    hashes: Vec<u64>,} impl<'input, 'aux> DiffSource<'input, 'aux> {    fn new<T: AsRef<[u8]> + ?Sized, C: CompareBytes, S: BuildHasher>(        text: &'input T,        ranges: &'aux [Range<usize>],        comp: &WordComparator<C, S>,    ) -> Self {        let text = BStr::new(text);        let hashes = ranges            .iter()            .map(|range| comp.hash_one(&text[range.clone()]))            .collect();        Self {            text,            ranges,            hashes,        }    }     fn local(&self) -> LocalDiffSource<'input, '_> {        LocalDiffSource {            text: self.text,            ranges: self.ranges,            hashes: &self.hashes,            global_offset: WordPosition(0),        }    }     fn range_at(&self, position: WordPosition) -> Range<usize> {        self.ranges[position.0].clone()    }} #[derive(Clone, Debug)]struct LocalDiffSource<'input, 'aux> {    text: &'input BStr,    ranges: &'aux [Range<usize>],    hashes: &'aux [u64],    /// The number of preceding word ranges excluded from the self `ranges`.    global_offset: WordPosition,} impl<'input> LocalDiffSource<'input, '_> {    fn narrowed(&self, positions: Range<LocalWordPosition>) -> Self {        Self {            text: self.text,            ranges: &self.ranges[positions.start.0..positions.end.0],            hashes: &self.hashes[positions.start.0..positions.end.0],            global_offset: self.map_to_global(positions.start),        }    }     fn map_to_global(&self, position: LocalWordPosition) -> WordPosition {        WordPosition(self.global_offset.0 + position.0)    }     fn hashed_words(        &self,    ) -> impl DoubleEndedIterator<Item = HashedWord<'input>> + ExactSizeIterator + use<'_, 'input>    {        iter::zip(self.ranges, self.hashes).map(|(range, &hash)| {            let text = &self.text[range.clone()];            HashedWord { hash, text }        })    }} struct Histogram<'input> {    word_to_positions: HashTable<HistogramEntry<'input>>,} // Many of the words are unique. We can inline up to 2 word positions (16 bytes// on 64-bit platform) in SmallVec for free.type HistogramEntry<'input> = (HashedWord<'input>, SmallVec<[LocalWordPosition; 2]>); impl<'input> Histogram<'input> {    fn calculate<C: CompareBytes, S: BuildHasher>(        source: &LocalDiffSource<'input, '_>,        comp: &WordComparator<C, S>,        max_occurrences: usize,    ) -> Self {        let mut word_to_positions: HashTable<HistogramEntry> = HashTable::new();        for (i, word) in source.hashed_words().enumerate() {            let pos = LocalWordPosition(i);            word_to_positions                .entry(                    word.hash,                    |(w, _)| comp.eq(w.text, word.text),                    |(w, _)| w.hash,                )                .and_modify(|(_, positions)| {                    // Allow one more than max_occurrences, so we can later skip                    // those with more than max_occurrences                    if positions.len() <= max_occurrences {                        positions.push(pos);                    }                })                .or_insert_with(|| (word, smallvec![pos]));        }        Self { 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: HashedWord<'input>,        comp: &WordComparator<C, S>,    ) -> Option<&[LocalWordPosition]> {        let (_, positions) = self            .word_to_positions            .find(word.hash, |(w, _)| comp.eq(w.text, word.text))?;        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: &LocalDiffSource,    right: &LocalDiffSource,    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.hashed_words(), right.hashed_words())        .take_while(|&(l, r)| comp.eq_hashed(l, r))        .count();    let left_hashed_words = left.hashed_words().skip(common_leading_len);    let right_hashed_words = right.hashed_words().skip(common_leading_len);     // Trim trailing common ranges (i.e. grow next unchanged region)    let common_trailing_len = iter::zip(left_hashed_words.rev(), right_hashed_words.rev())        .take_while(|&(l, r)| comp.eq_hashed(l, r))        .count();     found_positions.extend(itertools::chain(        (0..common_leading_len).map(|i| {            (                left.map_to_global(LocalWordPosition(i)),                right.map_to_global(LocalWordPosition(i)),            )        }),        (1..=common_trailing_len).rev().map(|i| {            (                left.map_to_global(LocalWordPosition(left.ranges.len() - i)),                right.map_to_global(LocalWordPosition(right.ranges.len() - i)),            )        }),    ));} fn collect_unchanged_words_lcs<C: CompareBytes, S: BuildHasher>(    found_positions: &mut Vec<(WordPosition, WordPosition)>,    left: &LocalDiffSource,    right: &LocalDiffSource,    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 #})]

Lines 1–500 of 1776.