use leptos::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LineKind {
Equal,
Insert,
Delete,
}
#[derive(Clone)]
pub struct DiffLine {
pub kind: LineKind,
pub text: String,
pub old_line: Option<usize>,
pub new_line: Option<usize>,
}
pub fn diff_lines(old: &str, new: &str) -> Vec<DiffLine> {
let left: Vec<&str> = old.lines().collect();
let right: Vec<&str> = new.lines().collect();
let rows = left.len();
let cols = right.len();
let mut lcs = vec![vec![0usize; cols + 1]; rows + 1];
for index in (0..rows).rev() {
for other in (0..cols).rev() {
lcs[index][other] = if left[index] == right[other] {
lcs[index + 1][other + 1] + 1
} else {
lcs[index + 1][other].max(lcs[index][other + 1])
};
}
}
let mut out = Vec::new();
let mut index = 0;
let mut other = 0;
let mut old_line = 1;
let mut new_line = 1;
while index < rows && other < cols {
if left[index] == right[other] {
out.push(DiffLine {
kind: LineKind::Equal,
text: left[index].to_string(),
old_line: Some(old_line),
new_line: Some(new_line),
});
index += 1;
other += 1;
old_line += 1;
new_line += 1;
} else if lcs[index + 1][other] >= lcs[index][other + 1] {
out.push(DiffLine {
kind: LineKind::Delete,
text: left[index].to_string(),
old_line: Some(old_line),
new_line: None,
});
index += 1;
old_line += 1;
} else {
out.push(DiffLine {
kind: LineKind::Insert,
text: right[other].to_string(),
old_line: None,
new_line: Some(new_line),
});
other += 1;
new_line += 1;
}
}
while index < rows {
out.push(DiffLine {
kind: LineKind::Delete,
text: left[index].to_string(),
old_line: Some(old_line),
new_line: None,
});
index += 1;
old_line += 1;
}
while other < cols {
out.push(DiffLine {
kind: LineKind::Insert,
text: right[other].to_string(),
old_line: None,
new_line: Some(new_line),
});
other += 1;
new_line += 1;
}
out
}
fn marker(kind: LineKind) -> &'static str {
match kind {
LineKind::Equal => " ",
LineKind::Insert => "+",
LineKind::Delete => "-",
}
}
fn kind_class(kind: LineKind) -> &'static str {
match kind {
LineKind::Equal => "equal",
LineKind::Insert => "insert",
LineKind::Delete => "delete",
}
}
#[component]
pub fn Diff(#[prop(into)] old: Signal<String>, #[prop(into)] new: Signal<String>) -> impl IntoView {
view! {
<div class="nightshade-diff">
{move || {
diff_lines(&old.get(), &new.get())
.into_iter()
.map(|line| {
let old_number = line
.old_line
.map(|value| value.to_string())
.unwrap_or_default();
let new_number = line
.new_line
.map(|value| value.to_string())
.unwrap_or_default();
view! {
<div class=format!("nightshade-diff-line {}", kind_class(line.kind))>
<span class="nightshade-diff-num">{old_number}</span>
<span class="nightshade-diff-num">{new_number}</span>
<span class="nightshade-diff-marker">{marker(line.kind)}</span>
<span class="nightshade-diff-text">{line.text}</span>
</div>
}
})
.collect_view()
}}
</div>
}
}
#[cfg(test)]
mod tests {
use super::{LineKind, diff_lines};
#[test]
fn identical_text_is_all_equal() {
let lines = diff_lines("a\nb\nc", "a\nb\nc");
assert!(lines.iter().all(|line| line.kind == LineKind::Equal));
assert_eq!(lines.len(), 3);
}
#[test]
fn detects_insert_and_delete() {
let lines = diff_lines("a\nb\nc", "a\nc\nd");
let inserts = lines.iter().filter(|l| l.kind == LineKind::Insert).count();
let deletes = lines.iter().filter(|l| l.kind == LineKind::Delete).count();
assert_eq!(deletes, 1);
assert_eq!(inserts, 1);
assert!(
lines
.iter()
.any(|l| l.kind == LineKind::Equal && l.text == "a")
);
}
}