use similar::{capture_diff_slices, Algorithm, DiffOp};
#[derive(Debug, Clone, Copy, Default)]
pub struct CombinedDiffWsOptions {
pub ignore_all_space: bool,
pub ignore_space_change: bool,
pub ignore_space_at_eol: bool,
pub ignore_cr_at_eol: bool,
}
impl CombinedDiffWsOptions {
#[must_use]
pub fn any(self) -> bool {
self.ignore_all_space
|| self.ignore_space_change
|| self.ignore_space_at_eol
|| self.ignore_cr_at_eol
}
}
#[derive(Clone)]
struct LostSeg {
text: String,
parent_map: u32,
}
fn strip_trailing_cr(s: &str, ignore: bool) -> &str {
if ignore && s.ends_with('\r') {
&s[..s.len().saturating_sub(1)]
} else {
s
}
}
fn line_key(line: &str, ws: CombinedDiffWsOptions) -> String {
let s = strip_trailing_cr(line, ws.ignore_cr_at_eol);
if ws.ignore_all_space {
s.chars().filter(|c| !c.is_whitespace()).collect()
} else if ws.ignore_space_change {
s.split_whitespace().collect::<Vec<_>>().join(" ")
} else if ws.ignore_space_at_eol {
s.trim_end_matches(|c: char| c.is_whitespace()).to_string()
} else {
s.to_string()
}
}
fn lines_match(a: &str, b: &str, ws: CombinedDiffWsOptions) -> bool {
line_key(a, ws) == line_key(b, ws)
}
fn coalesce_lost(
base: Vec<LostSeg>,
incoming: Vec<LostSeg>,
parent_bit: u32,
ws: CombinedDiffWsOptions,
) -> Vec<LostSeg> {
if incoming.is_empty() {
return base;
}
if base.is_empty() {
return incoming;
}
let ob = base.len();
let nw = incoming.len();
#[derive(Clone, Copy)]
enum Dir {
Match,
Base,
New,
}
let mut lcs = vec![vec![0usize; nw + 1]; ob + 1];
let mut dir = vec![vec![Dir::Base; nw + 1]; ob + 1];
for j in 1..=nw {
dir[0][j] = Dir::New;
}
for i in 1..=ob {
dir[i][0] = Dir::Base;
}
for i in 1..=ob {
for j in 1..=nw {
if lines_match(&base[i - 1].text, &incoming[j - 1].text, ws) {
lcs[i][j] = lcs[i - 1][j - 1] + 1;
dir[i][j] = Dir::Match;
} else if lcs[i][j - 1] >= lcs[i - 1][j] {
lcs[i][j] = lcs[i][j - 1];
dir[i][j] = Dir::New;
} else {
lcs[i][j] = lcs[i - 1][j];
dir[i][j] = Dir::Base;
}
}
}
let mut out: Vec<LostSeg> = Vec::new();
let mut i = ob;
let mut j = nw;
while i > 0 || j > 0 {
match dir[i][j] {
Dir::Match => {
let mut seg = base[i - 1].clone();
seg.parent_map |= parent_bit;
out.push(seg);
i -= 1;
j -= 1;
}
Dir::New => {
out.push(incoming[j - 1].clone());
j -= 1;
}
Dir::Base => {
out.push(base[i - 1].clone());
i -= 1;
}
}
}
out.reverse();
out
}
struct Sline {
lost: Vec<LostSeg>,
plost: Vec<LostSeg>,
flag: u32,
bol: String,
p_lno: Vec<u32>,
}
fn split_lines_with_incomplete(text: &str) -> (Vec<String>, usize) {
if text.is_empty() {
return (Vec::new(), 0);
}
let lines: Vec<String> = text.lines().map(str::to_owned).collect();
let cnt = lines.len();
(lines, cnt)
}
fn combine_one_parent(
slines: &mut [Sline],
cnt: usize,
parent_lines: &[String],
n: usize,
_num_parent: usize,
ws: CombinedDiffWsOptions,
) {
let nmask = 1u32 << n;
let old_keys: Vec<String> = parent_lines.iter().map(|l| line_key(l, ws)).collect();
let new_keys: Vec<String> = slines[..cnt].iter().map(|s| line_key(&s.bol, ws)).collect();
let ops = capture_diff_slices(Algorithm::Myers, &old_keys, &new_keys);
for op in ops {
match op {
DiffOp::Equal { .. } => {}
DiffOp::Delete {
old_index,
old_len,
new_index,
..
} => {
let mut b = new_index.min(cnt);
if old_len > 0 && b == 0 && cnt > 0 {
b = 1;
}
let b = b.min(cnt.saturating_sub(1));
for k in 0..old_len {
slines[b].plost.push(LostSeg {
text: parent_lines[old_index + k].clone(),
parent_map: nmask,
});
}
}
DiffOp::Insert {
new_index, new_len, ..
} => {
for k in 0..new_len {
slines[new_index + k].flag |= nmask;
}
}
DiffOp::Replace {
old_index,
old_len,
new_index,
new_len,
} => {
let b = if new_len == 0 {
let mut b = new_index.min(cnt);
if old_len > 0 && b == 0 && cnt > 0 {
b = 1;
}
b.min(cnt.saturating_sub(1))
} else {
new_index.saturating_sub(1).min(cnt.saturating_sub(1))
};
for k in 0..old_len {
slines[b].plost.push(LostSeg {
text: parent_lines[old_index + k].clone(),
parent_map: nmask,
});
}
for k in 0..new_len {
slines[new_index + k].flag |= nmask;
}
}
}
}
let mut p_lno = 1u32;
for lno in 0..=cnt {
slines[lno].p_lno[n] = p_lno;
if !slines[lno].plost.is_empty() {
let incoming = std::mem::take(&mut slines[lno].plost);
slines[lno].lost =
coalesce_lost(std::mem::take(&mut slines[lno].lost), incoming, nmask, ws);
}
for seg in &slines[lno].lost {
if seg.parent_map & nmask != 0 {
p_lno = p_lno.saturating_add(1);
}
}
if lno < cnt && slines[lno].flag & nmask == 0 {
p_lno = p_lno.saturating_add(1);
}
}
}
fn interesting(s: &Sline, all_mask: u32) -> bool {
(s.flag & all_mask) != 0 || !s.lost.is_empty()
}
fn find_next(slines: &[Sline], mark: u32, mut i: usize, cnt: usize, want_unmarked: bool) -> usize {
while i <= cnt {
let marked = slines[i].flag & mark != 0;
if want_unmarked != marked {
return i;
}
i += 1;
}
cnt + 1
}
fn adjust_hunk_tail(slines: &[Sline], all_mask: u32, hunk_begin: usize, mut i: usize) -> usize {
if hunk_begin < i && slines[i - 1].flag & all_mask == 0 {
i -= 1;
}
i
}
fn give_context(slines: &mut [Sline], cnt: usize, num_parent: usize, context: usize) {
let all_mask = (1u32 << num_parent) - 1;
let mark = 1u32 << num_parent;
let no_pre_delete = 2u32 << num_parent;
let mut i = find_next(slines, mark, 0, cnt, false);
if cnt < i {
return;
}
while i <= cnt {
let mut j = i.saturating_sub(context);
while j < i {
if slines[j].flag & mark == 0 {
slines[j].flag |= no_pre_delete;
}
slines[j].flag |= mark;
j += 1;
}
loop {
j = find_next(slines, mark, i, cnt, true);
if cnt < j {
return;
}
let k = find_next(slines, mark, j, cnt, false);
let j_adj = adjust_hunk_tail(slines, all_mask, i, j);
if k < j_adj + context {
let mut t = j;
while t < k {
slines[t].flag |= mark;
t += 1;
}
i = k;
continue;
}
i = k;
let k_end = (j + context).min(cnt + 1);
let mut t = j;
while t < k_end {
slines[t].flag |= mark;
t += 1;
}
break;
}
}
}
fn make_hunks(slines: &mut [Sline], cnt: usize, num_parent: usize, dense: bool, context: usize) {
let all_mask = (1u32 << num_parent) - 1;
let mark = 1u32 << num_parent;
for i in 0..=cnt {
if interesting(&slines[i], all_mask) {
slines[i].flag |= mark;
} else {
slines[i].flag &= !mark;
}
}
if dense {
let mut i = 0usize;
while i <= cnt {
while i <= cnt && slines[i].flag & mark == 0 {
i += 1;
}
if cnt < i {
break;
}
let hunk_begin = i;
let mut j = i + 1;
while j <= cnt {
if slines[j].flag & mark == 0 {
let j_adj = adjust_hunk_tail(slines, all_mask, hunk_begin, j);
let la = (j_adj + context).min(cnt + 1);
let mut contin = false;
let mut la2 = la;
while la2 > j {
la2 -= 1;
if slines[la2].flag & mark != 0 {
contin = true;
break;
}
}
if !contin {
break;
}
j = la2;
}
j += 1;
}
let hunk_end = j;
let mut same_diff = 0u32;
let mut has_interesting = false;
let mut jj = hunk_begin;
while jj < hunk_end && !has_interesting {
let mut this_diff = slines[jj].flag & all_mask;
if this_diff != 0 {
if same_diff == 0 {
same_diff = this_diff;
} else if same_diff != this_diff {
has_interesting = true;
break;
}
}
let ll_iter = slines[jj].lost.iter();
for seg in ll_iter {
if has_interesting {
break;
}
this_diff = seg.parent_map;
if same_diff == 0 {
same_diff = this_diff;
} else if same_diff != this_diff {
has_interesting = true;
}
}
jj += 1;
}
if !has_interesting && same_diff != 0 && same_diff != all_mask {
for k in hunk_begin..hunk_end {
slines[k].flag &= !mark;
}
}
i = hunk_end;
}
}
give_context(slines, cnt, num_parent, context);
}
fn show_parent_lno(slines: &[Sline], l0: usize, l1: usize, n: usize, null_ctx: u32) -> String {
let a = slines[l0].p_lno[n];
let b = slines[l1].p_lno[n];
format!(" -{a},{}", b.saturating_sub(a).saturating_sub(null_ctx))
}
fn dump_slines(slines: &[Sline], cnt: usize, num_parent: usize, context: usize) -> String {
let mark = 1u32 << num_parent;
let no_pre_delete = 2u32 << num_parent;
let mut out = String::new();
let mut lno = 0usize;
loop {
while lno <= cnt && slines[lno].flag & mark == 0 {
lno += 1;
}
if cnt < lno {
break;
}
let h_start = lno;
let mut h_end = lno + 1;
while h_end <= cnt && slines[h_end].flag & mark != 0 {
h_end += 1;
}
let mut rlines = h_end - h_start;
if cnt < h_end {
rlines = rlines.saturating_sub(1);
}
let mut null_ctx = 0u32;
if context == 0 {
for j in h_start..h_end {
if slines[j].flag & (mark - 1) == 0 {
null_ctx = null_ctx.saturating_add(1);
}
}
rlines = rlines.saturating_sub(null_ctx as usize);
}
out.push_str("@@@");
for _ in 0..=num_parent {
out.push('@');
}
for n in 0..num_parent {
out.push_str(&show_parent_lno(slines, h_start, h_end, n, null_ctx));
}
out.push_str(&format!(" +{},{} ", h_start + 1, rlines));
for _ in 0..=num_parent {
out.push('@');
}
out.push('\n');
while lno < h_end {
let sl = &slines[lno];
lno += 1;
let show_lost = if sl.flag & no_pre_delete == 0 {
sl.lost.as_slice()
} else {
&[][..]
};
for seg in show_lost {
out.push('-');
for j in 0..num_parent {
if seg.parent_map & (1u32 << j) != 0 {
out.push('-');
} else {
out.push(' ');
}
}
out.push_str(&seg.text);
out.push('\n');
}
if cnt < lno {
break;
}
let sl = &slines[lno - 1];
if sl.flag & (mark - 1) == 0 {
if context == 0 {
continue;
}
out.push(' ');
} else {
out.push('+');
}
let mut p_mask = 1u32;
for _ in 0..num_parent {
if p_mask & sl.flag != 0 {
out.push('+');
} else {
out.push(' ');
}
p_mask <<= 1;
}
out.push_str(&sl.bol);
out.push('\n');
}
}
out
}
fn reuse_parent(slines: &mut [Sline], cnt: usize, i: usize, j: usize) {
let im = 1u32 << i;
let jm = 1u32 << j;
for lno in 0..=cnt {
for seg in &mut slines[lno].lost {
if seg.parent_map & jm != 0 {
seg.parent_map |= im;
}
}
if slines[lno].flag & jm != 0 {
slines[lno].flag |= im;
}
slines[lno].p_lno[i] = slines[lno].p_lno[j];
}
}
#[must_use]
pub fn format_combined_diff_body(
parent_texts: &[String],
result_text: &str,
context: usize,
dense: bool,
ws: CombinedDiffWsOptions,
) -> String {
let num_parent = parent_texts.len();
if num_parent == 0 {
return String::new();
}
let (res_lines, cnt) = split_lines_with_incomplete(result_text);
if cnt == 0 && result_text.is_empty() {
return String::new();
}
let mut slines: Vec<Sline> = (0..=cnt + 1)
.map(|idx| Sline {
lost: Vec::new(),
plost: Vec::new(),
flag: 0,
bol: if idx < cnt {
res_lines[idx].clone()
} else {
String::new()
},
p_lno: vec![0; num_parent],
})
.collect();
let parents: Vec<Vec<String>> = parent_texts
.iter()
.map(|p| p.lines().map(str::to_owned).collect())
.collect();
for i in 0..num_parent {
let mut reused = false;
for j in 0..i {
if parents[i] == parents[j] {
reuse_parent(&mut slines, cnt, i, j);
reused = true;
break;
}
}
if !reused {
combine_one_parent(&mut slines, cnt, &parents[i], i, num_parent, ws);
}
}
make_hunks(&mut slines, cnt, num_parent, dense, context);
dump_slines(&slines, cnt, num_parent, context)
}