diff options
Diffstat (limited to 'helix-core/src/diff.rs')
-rw-r--r-- | helix-core/src/diff.rs | 70 |
1 files changed, 70 insertions, 0 deletions
diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs new file mode 100644 index 00000000..9c1fc999 --- /dev/null +++ b/helix-core/src/diff.rs @@ -0,0 +1,70 @@ +use ropey::Rope; + +use crate::{Change, Transaction}; + +/// Compares `old` and `new` to generate a [`Transaction`] describing +/// the steps required to get from `old` to `new`. +pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction { + // `similar` only works on contiguous data, so a `Rope` has + // to be temporarily converted into a `String`. + let old_converted = old.to_string(); + let new_converted = new.to_string(); + + // A timeout is set so after 1 seconds, the algorithm will start + // approximating. This is especially important for big `Rope`s or + // `Rope`s that are extremely dissimilar to each other. + // + // Note: Ignore the clippy warning, as the trait bounds of + // `Transaction::change()` require an iterator implementing + // `ExactIterator`. + let mut config = similar::TextDiff::configure(); + config.timeout(std::time::Duration::from_secs(1)); + + let diff = config.diff_chars(&old_converted, &new_converted); + + // The current position of the change needs to be tracked to + // construct the `Change`s. + let mut pos = 0; + let changes: Vec<Change> = diff + .ops() + .iter() + .map(|op| op.as_tag_tuple()) + .filter_map(|(tag, old_range, new_range)| { + // `old_pos..pos` is equivalent to `start..end` for where + // the change should be applied. + let old_pos = pos; + pos += old_range.end - old_range.start; + + match tag { + // Semantically, inserts and replacements are the same thing. + similar::DiffTag::Insert | similar::DiffTag::Replace => { + // This is the text from the `new` rope that should be + // inserted into `old`. + let text: &str = { + let start = new.char_to_byte(new_range.start); + let end = new.char_to_byte(new_range.end); + &new_converted[start..end] + }; + Some((old_pos, pos, Some(text.into()))) + } + similar::DiffTag::Delete => Some((old_pos, pos, None)), + similar::DiffTag::Equal => None, + } + }) + .collect(); + Transaction::change(old, changes.into_iter()) +} + +#[cfg(test)] +mod tests { + use super::*; + + quickcheck::quickcheck! { + fn test_compare_ropes(a: String, b: String) -> bool { + let mut old = Rope::from(a); + let new = Rope::from(b); + compare_ropes(&old, &new).apply(&mut old); + old.to_string() == new.to_string() + } + } +} |