use std::ops::Range;
use similar::{ChangeTag, TextDiff};
pub fn intraline(old: &str, new: &str) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
let diff = TextDiff::from_graphemes(old, new);
let mut old_ranges: Vec<Range<usize>> = Vec::new();
let mut new_ranges: Vec<Range<usize>> = Vec::new();
let mut old_pos = 0usize;
let mut new_pos = 0usize;
for change in diff.iter_all_changes() {
let len = change.value().len();
match change.tag() {
ChangeTag::Equal => {
old_pos += len;
new_pos += len;
}
ChangeTag::Delete => {
push_range(&mut old_ranges, old_pos..old_pos + len);
old_pos += len;
}
ChangeTag::Insert => {
push_range(&mut new_ranges, new_pos..new_pos + len);
new_pos += len;
}
}
}
(old_ranges, new_ranges)
}
fn push_range(ranges: &mut Vec<Range<usize>>, range: Range<usize>) {
if let Some(last) = ranges.last_mut()
&& last.end == range.start
{
last.end = range.end;
return;
}
ranges.push(range);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equal_lines_have_no_emphasis() {
let (old, new) = intraline("same line", "same line");
assert!(old.is_empty());
assert!(new.is_empty());
}
#[test]
fn ranges_are_in_bounds_and_ascending() {
let old = "if claims.expiry < now():";
let new = "if claims.expiry <= now() - LEEWAY:";
let (old_r, new_r) = intraline(old, new);
for r in &old_r {
assert!(r.end <= old.len());
}
let mut prev_end = 0;
for r in &new_r {
assert!(r.start >= prev_end && r.end <= new.len());
prev_end = r.end;
}
}
#[test]
fn insertion_is_emphasized_on_new_side_only() {
let (old, new) = intraline("session.touch()", "session.touch(now())");
assert!(old.is_empty());
let joined: String = new
.iter()
.map(|r| &"session.touch(now())"[r.clone()])
.collect();
assert_eq!(joined, "now()");
}
#[test]
fn adjacent_ranges_are_merged() {
let (_, new) = intraline("ab", "aXYb");
assert_eq!(new, vec![1..3]);
}
#[test]
fn combining_characters_stay_whole() {
let new_line = "cafe\u{301}";
let (_, new) = intraline("cafe", new_line);
for r in &new {
assert!(new_line.is_char_boundary(r.start), "range splits a char");
assert!(new_line.is_char_boundary(r.end), "range splits a char");
}
let joined: String = new.iter().map(|r| &new_line[r.clone()]).collect();
assert!(joined.contains('\u{301}'));
}
#[test]
fn empty_inputs() {
let (old, new) = intraline("", "");
assert!(old.is_empty());
assert!(new.is_empty());
}
}