use crate::diff::Change;
use serde::{Deserialize, Serialize};
const LCS_GUARD: usize = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SegKind {
Keep,
Insert,
Delete,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextSegment {
pub kind: SegKind,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", rename_all_fields = "camelCase")]
pub enum DiffGranularity {
#[default]
Block,
Inline,
Smart {
replace_threshold: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct DiffOptions {
pub text: DiffGranularity,
}
pub fn diff_text(a: &str, b: &str) -> Vec<TextSegment> {
let ac: Vec<char> = a.chars().collect();
let bc: Vec<char> = b.chars().collect();
let mut raw: Vec<(SegKind, String)> = Vec::new();
let mut p = 0;
while p < ac.len() && p < bc.len() && ac[p] == bc[p] {
p += 1;
}
let mut ea = ac.len();
let mut eb = bc.len();
while ea > p && eb > p && ac[ea - 1] == bc[eb - 1] {
ea -= 1;
eb -= 1;
}
if p > 0 {
raw.push((SegKind::Keep, ac[..p].iter().collect()));
}
let (ma, mb) = (&ac[p..ea], &bc[p..eb]);
if ma.is_empty() && mb.is_empty() {
} else if ma.is_empty() {
raw.push((SegKind::Insert, mb.iter().collect()));
} else if mb.is_empty() {
raw.push((SegKind::Delete, ma.iter().collect()));
} else if ma.len().saturating_mul(mb.len()) > LCS_GUARD {
raw.push((SegKind::Delete, ma.iter().collect()));
raw.push((SegKind::Insert, mb.iter().collect()));
} else {
lcs_segments(ma, mb, &mut raw);
}
if ea < ac.len() {
raw.push((SegKind::Keep, ac[ea..].iter().collect()));
}
coalesce(raw)
}
fn lcs_segments(a: &[char], b: &[char], out: &mut Vec<(SegKind, String)>) {
let (m, n) = (a.len(), b.len());
let mut dp = vec![vec![0u32; n + 1]; m + 1];
for i in (0..m).rev() {
for j in (0..n).rev() {
dp[i][j] = if a[i] == b[j] {
dp[i + 1][j + 1] + 1
} else {
dp[i + 1][j].max(dp[i][j + 1])
};
}
}
let (mut i, mut j) = (0usize, 0usize);
let push = |out: &mut Vec<(SegKind, String)>, k: SegKind, c: char| {
out.push((k, c.to_string()));
};
while i < m && j < n {
if a[i] == b[j] {
push(out, SegKind::Keep, a[i]);
i += 1;
j += 1;
} else if dp[i + 1][j] >= dp[i][j + 1] {
push(out, SegKind::Delete, a[i]);
i += 1;
} else {
push(out, SegKind::Insert, b[j]);
j += 1;
}
}
while i < m {
push(out, SegKind::Delete, a[i]);
i += 1;
}
while j < n {
push(out, SegKind::Insert, b[j]);
j += 1;
}
}
fn coalesce(raw: Vec<(SegKind, String)>) -> Vec<TextSegment> {
let mut out: Vec<TextSegment> = Vec::new();
for (kind, text) in raw {
if text.is_empty() {
continue;
}
match out.last_mut() {
Some(last) if last.kind == kind => last.text.push_str(&text),
_ => out.push(TextSegment { kind, text }),
}
}
out
}
pub(crate) fn changed_scalars(segs: &[TextSegment]) -> usize {
segs.iter()
.filter(|s| s.kind != SegKind::Keep)
.map(|s| s.text.chars().count())
.sum()
}
pub(crate) fn splices_from_segments(path: &[usize], segs: &[TextSegment], out: &mut Vec<Change>) {
let mut islands: Vec<Change> = Vec::new();
let mut cursor = 0usize; let mut i = 0;
while i < segs.len() {
if segs[i].kind == SegKind::Keep {
cursor += segs[i].text.chars().count();
i += 1;
continue;
}
let from = cursor;
let mut len_del = 0usize;
let mut insert = String::new();
while i < segs.len() && segs[i].kind != SegKind::Keep {
match segs[i].kind {
SegKind::Delete => {
let n = segs[i].text.chars().count();
len_del += n;
cursor += n;
}
SegKind::Insert => insert.push_str(&segs[i].text),
SegKind::Keep => unreachable!(),
}
i += 1;
}
islands.push(Change::SpliceText {
path: path.to_vec(),
from,
len_del,
insert,
});
}
out.extend(islands.into_iter().rev());
}