use similar::{ChangeTag, TextDiff};
use super::types::{expand_tabs, ChangeType, DiffLine, InlineSegment};
fn has_meaningful_content(s: &str) -> bool {
s.chars().any(|c| !c.is_whitespace())
}
fn compute_word_diff(
old_text: &str,
new_text: &str,
) -> Option<(Vec<InlineSegment>, Vec<InlineSegment>)> {
let diff = TextDiff::configure().diff_unicode_words(old_text, new_text);
let mut old_segments = Vec::new();
let mut new_segments = Vec::new();
let mut unchanged_len = 0usize;
for change in diff.iter_all_changes() {
let text = change.value().to_string();
match change.tag() {
ChangeTag::Equal => {
if has_meaningful_content(&text) {
unchanged_len += text.trim().len();
}
old_segments.push(InlineSegment {
text: text.clone(),
emphasized: false,
});
new_segments.push(InlineSegment {
text,
emphasized: false,
});
}
ChangeTag::Delete => {
old_segments.push(InlineSegment {
text,
emphasized: true,
});
}
ChangeTag::Insert => {
new_segments.push(InlineSegment {
text,
emphasized: true,
});
}
}
}
let old_trimmed_len = old_text.trim().len();
let new_trimmed_len = new_text.trim().len();
let total_len = old_trimmed_len.max(new_trimmed_len);
const MIN_UNCHANGED_RATIO: f64 = 0.20;
if total_len == 0 || (unchanged_len as f64 / total_len as f64) < MIN_UNCHANGED_RATIO {
return None;
}
Some((old_segments, new_segments))
}
pub fn compute_side_by_side(old: &str, new: &str, tab_width: usize) -> Vec<DiffLine> {
let diff = TextDiff::from_lines(old, new);
let mut lines = Vec::new();
let mut old_num = 1usize;
let mut new_num = 1usize;
let changes: Vec<_> = diff.iter_all_changes().collect();
let mut i = 0;
while i < changes.len() {
let change = &changes[i];
match change.tag() {
ChangeTag::Equal => {
let text = expand_tabs(change.value().trim_end(), tab_width);
lines.push(DiffLine {
old_line: Some((old_num, text.clone())),
new_line: Some((new_num, text)),
change_type: ChangeType::Equal,
old_segments: None,
new_segments: None,
});
old_num += 1;
new_num += 1;
i += 1;
}
ChangeTag::Delete => {
let mut deletions = Vec::new();
while i < changes.len() && changes[i].tag() == ChangeTag::Delete {
deletions.push((
old_num,
expand_tabs(changes[i].value().trim_end(), tab_width),
));
old_num += 1;
i += 1;
}
let mut insertions = Vec::new();
while i < changes.len() && changes[i].tag() == ChangeTag::Insert {
insertions.push((
new_num,
expand_tabs(changes[i].value().trim_end(), tab_width),
));
new_num += 1;
i += 1;
}
let max_len = deletions.len().max(insertions.len());
for j in 0..max_len {
let old_line = deletions.get(j).cloned();
let new_line = insertions.get(j).cloned();
let change_type = match (&old_line, &new_line) {
(Some(_), Some(_)) => ChangeType::Modified,
(Some(_), None) => ChangeType::Delete,
(None, Some(_)) => ChangeType::Insert,
(None, None) => unreachable!(),
};
let (old_segments, new_segments) =
if matches!(change_type, ChangeType::Modified) {
let old_text = old_line.as_ref().map(|(_, t)| t.as_str()).unwrap_or("");
let new_text = new_line.as_ref().map(|(_, t)| t.as_str()).unwrap_or("");
if let Some((old_segs, new_segs)) =
compute_word_diff(old_text, new_text)
{
(Some(old_segs), Some(new_segs))
} else {
(None, None)
}
} else {
(None, None)
};
lines.push(DiffLine {
old_line,
new_line,
change_type,
old_segments,
new_segments,
});
}
}
ChangeTag::Insert => {
lines.push(DiffLine {
old_line: None,
new_line: Some((new_num, expand_tabs(change.value().trim_end(), tab_width))),
change_type: ChangeType::Insert,
old_segments: None,
new_segments: None,
});
new_num += 1;
i += 1;
}
}
}
lines
}
pub fn find_hunk_starts(lines: &[DiffLine]) -> Vec<usize> {
let mut hunks = Vec::new();
let mut in_hunk = false;
for (i, line) in lines.iter().enumerate() {
let is_change = !matches!(line.change_type, ChangeType::Equal);
if is_change && !in_hunk {
hunks.push(i);
in_hunk = true;
} else if !is_change {
in_hunk = false;
}
}
hunks
}