use crate::diff::text_range::TextRange;
use super::render_options::{RangeMatch, TextOperation};
use super::{common_prefix_byte_len, common_suffix_byte_len};
pub(crate) const PLAIN_TEXT_MAX_EDIT: usize = 10_000;
pub fn plain_text_line_diff(before: &str, after: &str) -> (Vec<RangeMatch>, Vec<RangeMatch>) {
plain_text_line_diff_with_max_edit(before, after, PLAIN_TEXT_MAX_EDIT)
}
pub(crate) fn plain_text_line_diff_with_max_edit(
before: &str,
after: &str,
max_edit: usize,
) -> (Vec<RangeMatch>, Vec<RangeMatch>) {
match line_diff_core(before, after, max_edit) {
Some(core) => {
let before_lines: Vec<&str> = before.lines().collect();
let after_lines: Vec<&str> = after.lines().collect();
debug_assert_eq!(before_lines.len(), core.before_line_count);
debug_assert_eq!(after_lines.len(), core.after_line_count);
build_line_ranges(&before_lines, &after_lines, &core.pairs)
}
None => {
let before_line_count = before.lines().count();
let after_line_count = after.lines().count();
whole_file_replaced(before_line_count, after_line_count)
}
}
}
pub struct LineDiffCore {
pub pairs: Vec<(usize, usize)>,
pub before_line_count: usize,
pub after_line_count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WholeFileClass {
Identical,
InsertOnly,
DeleteOnly,
Mixed,
}
impl LineDiffCore {
pub fn whole_file_class(&self) -> WholeFileClass {
let has_delete = self.pairs.len() < self.before_line_count;
let has_insert = self.pairs.len() < self.after_line_count;
match (has_delete, has_insert) {
(false, false) => WholeFileClass::Identical,
(true, false) => WholeFileClass::DeleteOnly,
(false, true) => WholeFileClass::InsertOnly,
(true, true) => WholeFileClass::Mixed,
}
}
}
pub fn whole_file_text_class(before: &str, after: &str) -> WholeFileClass {
match line_diff_core(before, after, PLAIN_TEXT_MAX_EDIT) {
Some(core) => core.whole_file_class(),
None => WholeFileClass::Mixed,
}
}
pub fn line_diff_core(before: &str, after: &str, max_edit: usize) -> Option<LineDiffCore> {
let before_lines: Vec<&str> = before.lines().collect();
let after_lines: Vec<&str> = after.lines().collect();
let before_hashes = hash_lines(&before_lines);
let after_hashes = hash_lines(&after_lines);
let pairs = crate::diff::apted::myers_lcs(&before_hashes, &after_hashes, max_edit)?;
Some(LineDiffCore {
pairs,
before_line_count: before_lines.len(),
after_line_count: after_lines.len(),
})
}
pub(crate) fn hash_lines(lines: &[&str]) -> Vec<u64> {
use std::hash::{Hash, Hasher};
lines
.iter()
.map(|line| {
let mut hasher = rustc_hash::FxHasher::default();
line.hash(&mut hasher);
hasher.finish()
})
.collect()
}
pub(crate) fn whole_line_range(row: usize) -> TextRange {
TextRange::new(row, 0, row + 1, 0)
}
pub(crate) const MIN_SHARED_AFFIX_PERCENT: usize = 50;
pub(crate) fn shared_affix(before_line: &str, after_line: &str) -> Option<(usize, usize)> {
let prefix = common_prefix_byte_len(before_line, after_line);
let suffix = common_suffix_byte_len(&before_line[prefix..], &after_line[prefix..]);
let longer = before_line.len().max(after_line.len());
if longer == 0 || (prefix + suffix) * 100 < longer * MIN_SHARED_AFFIX_PERCENT {
return None;
}
if before_line.len() - suffix == prefix && after_line.len() - suffix == prefix {
return None;
}
Some((prefix, suffix))
}
pub(crate) fn intra_line_ranges(
before_row: usize,
before_line: &str,
after_row: usize,
after_line: &str,
) -> Option<(Vec<RangeMatch>, Vec<RangeMatch>)> {
let (prefix, suffix) = shared_affix(before_line, after_line)?;
let before_middle_end = before_line.len() - suffix;
let after_middle_end = after_line.len() - suffix;
let mut before_ranges = Vec::with_capacity(3);
let mut after_ranges = Vec::with_capacity(3);
let mut push = |b: TextRange, a: TextRange, operation: TextOperation| {
before_ranges.push(RangeMatch {
source: b.clone(),
destination: a.clone(),
operation: operation.clone(),
});
after_ranges.push(RangeMatch {
source: a,
destination: b,
operation,
});
};
if prefix > 0 {
push(
TextRange::new(before_row, 0, before_row, prefix),
TextRange::new(after_row, 0, after_row, prefix),
TextOperation::Identical,
);
}
push(
TextRange::new(before_row, prefix, before_row, before_middle_end),
TextRange::new(after_row, prefix, after_row, after_middle_end),
TextOperation::Update,
);
if suffix > 0 {
push(
TextRange::new(before_row, before_middle_end, before_row, before_line.len()),
TextRange::new(after_row, after_middle_end, after_row, after_line.len()),
TextOperation::Identical,
);
}
Some((before_ranges, after_ranges))
}
pub(crate) fn build_line_ranges(
before_lines: &[&str],
after_lines: &[&str],
pairs: &[(usize, usize)],
) -> (Vec<RangeMatch>, Vec<RangeMatch>) {
let (before_line_count, after_line_count) = (before_lines.len(), after_lines.len());
let mut before_ranges = Vec::new();
let mut after_ranges = Vec::new();
let mut next_before_row = 0;
let mut next_after_row = 0;
let mut last_before_match = TextRange::zero();
let mut last_after_match = TextRange::zero();
for &(bi, ai) in pairs {
emit_gap(
before_lines,
after_lines,
next_before_row..bi,
next_after_row..ai,
&last_before_match,
&last_after_match,
&mut before_ranges,
&mut after_ranges,
);
let before_line = whole_line_range(bi);
let after_line = whole_line_range(ai);
before_ranges.push(RangeMatch {
source: before_line.clone(),
destination: after_line.clone(),
operation: TextOperation::Identical,
});
after_ranges.push(RangeMatch {
source: after_line.clone(),
destination: before_line.clone(),
operation: TextOperation::Identical,
});
last_before_match = before_line;
last_after_match = after_line;
next_before_row = bi + 1;
next_after_row = ai + 1;
}
emit_gap(
before_lines,
after_lines,
next_before_row..before_line_count,
next_after_row..after_line_count,
&last_before_match,
&last_after_match,
&mut before_ranges,
&mut after_ranges,
);
(before_ranges, after_ranges)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit_gap(
before_lines: &[&str],
after_lines: &[&str],
before_rows: std::ops::Range<usize>,
after_rows: std::ops::Range<usize>,
last_before_match: &TextRange,
last_after_match: &TextRange,
before_ranges: &mut Vec<RangeMatch>,
after_ranges: &mut Vec<RangeMatch>,
) {
if before_rows.is_empty() && after_rows.is_empty() {
return;
}
let plan = plan_gap(
before_lines,
after_lines,
before_rows.clone(),
after_rows.clone(),
);
if !plan.iter().any(|op| matches!(op, GapOp::Pair(..))) {
if !before_rows.is_empty() {
before_ranges.push(RangeMatch {
source: TextRange::new(before_rows.start, 0, before_rows.end, 0),
destination: last_after_match.right_limit(),
operation: TextOperation::Delete,
});
}
if !after_rows.is_empty() {
after_ranges.push(RangeMatch {
source: TextRange::new(after_rows.start, 0, after_rows.end, 0),
destination: last_before_match.right_limit(),
operation: TextOperation::Insert,
});
}
return;
}
let mut pending_delete: Option<std::ops::Range<usize>> = None;
let mut pending_insert: Option<std::ops::Range<usize>> = None;
let flush_delete = |pending: &mut Option<std::ops::Range<usize>>, out: &mut Vec<RangeMatch>| {
if let Some(rows) = pending.take() {
out.push(RangeMatch {
source: TextRange::new(rows.start, 0, rows.end, 0),
destination: last_after_match.right_limit(),
operation: TextOperation::Delete,
});
}
};
let flush_insert = |pending: &mut Option<std::ops::Range<usize>>, out: &mut Vec<RangeMatch>| {
if let Some(rows) = pending.take() {
out.push(RangeMatch {
source: TextRange::new(rows.start, 0, rows.end, 0),
destination: last_before_match.right_limit(),
operation: TextOperation::Insert,
});
}
};
for op in plan {
match op {
GapOp::Pair(b, a) => {
flush_delete(&mut pending_delete, before_ranges);
flush_insert(&mut pending_insert, after_ranges);
let (before_parts, after_parts) =
intra_line_ranges(b, before_lines[b], a, after_lines[a])
.expect("plan_gap only emits Pair for rows shared_affix accepted");
before_ranges.extend(before_parts);
after_ranges.extend(after_parts);
}
GapOp::Delete(b) => match &mut pending_delete {
Some(rows) if rows.end == b => rows.end = b + 1,
_ => {
flush_delete(&mut pending_delete, before_ranges);
pending_delete = Some(b..b + 1);
}
},
GapOp::Insert(a) => match &mut pending_insert {
Some(rows) if rows.end == a => rows.end = a + 1,
_ => {
flush_insert(&mut pending_insert, after_ranges);
pending_insert = Some(a..a + 1);
}
},
}
}
flush_delete(&mut pending_delete, before_ranges);
flush_insert(&mut pending_insert, after_ranges);
}
pub(crate) enum GapOp {
Pair(usize, usize),
Delete(usize),
Insert(usize),
}
pub(crate) const GAP_RESYNC_WINDOW: usize = 16;
pub(crate) fn plan_gap(
before_lines: &[&str],
after_lines: &[&str],
before_rows: std::ops::Range<usize>,
after_rows: std::ops::Range<usize>,
) -> Vec<GapOp> {
let pairs_up = |b: usize, a: usize| shared_affix(before_lines[b], after_lines[a]).is_some();
let mut plan = Vec::new();
let (mut b, mut a) = (before_rows.start, after_rows.start);
while b < before_rows.end && a < after_rows.end {
if pairs_up(b, a) {
plan.push(GapOp::Pair(b, a));
b += 1;
a += 1;
continue;
}
let resync = (1..=GAP_RESYNC_WINDOW).find_map(|d| {
if a + d < after_rows.end && pairs_up(b, a + d) {
Some((d, true))
} else if b + d < before_rows.end && pairs_up(b + d, a) {
Some((d, false))
} else {
None
}
});
match resync {
Some((d, true)) => {
plan.extend((a..a + d).map(GapOp::Insert));
a += d;
}
Some((d, false)) => {
plan.extend((b..b + d).map(GapOp::Delete));
b += d;
}
None => {
plan.push(GapOp::Delete(b));
plan.push(GapOp::Insert(a));
b += 1;
a += 1;
}
}
}
plan.extend((b..before_rows.end).map(GapOp::Delete));
plan.extend((a..after_rows.end).map(GapOp::Insert));
plan
}
pub(crate) fn whole_file_replaced(
before_line_count: usize,
after_line_count: usize,
) -> (Vec<RangeMatch>, Vec<RangeMatch>) {
let before_ranges = if before_line_count == 0 {
Vec::new()
} else {
vec![RangeMatch {
source: TextRange::new(0, 0, before_line_count, 0),
destination: TextRange::zero(),
operation: TextOperation::Delete,
}]
};
let after_ranges = if after_line_count == 0 {
Vec::new()
} else {
vec![RangeMatch {
source: TextRange::new(0, 0, after_line_count, 0),
destination: TextRange::zero(),
operation: TextOperation::Insert,
}]
};
(before_ranges, after_ranges)
}