use crate::{
core::{
assets,
attrs::{component_attributes, diff_attribute},
},
models::diff as model,
prelude::*,
theme::use_stylesheets,
};
const BEM: Bem = Bem::new("fc-diff");
const STAT: Bem = Bem::new("fc-diff__stat");
fc_modifier! { enum DiffLayout for BEM { Wrap => None, Scroll => "scroll" } }
fc_modifier! { enum StatKind for STAT { Plain => None, Added => "added", Removed => "removed" } }
#[derive(Debug, Clone, PartialEq, Eq)]
enum Row {
Line(model::DiffLine),
Gap {
from: usize,
lines: Vec<model::DiffLine>,
},
}
fn fold(lines: Vec<model::DiffLine>, context: Option<usize>) -> Vec<Row> {
let Some(context) = context else {
return lines.into_iter().map(Row::Line).collect();
};
let changed: Vec<bool> = lines
.iter()
.map(|l| l.kind != model::DiffKind::Equal)
.collect();
let near = |i: usize| {
let low = i.saturating_sub(context);
let high = (i + context).min(changed.len().saturating_sub(1));
changed.get(low..=high).is_some_and(|w| w.contains(&true))
};
let mut rows = Vec::new();
let mut pending: Vec<model::DiffLine> = Vec::new();
let mut pending_from = 0;
for (index, line) in lines.into_iter().enumerate() {
if near(index) {
if !pending.is_empty() {
rows.push(Row::Gap {
from: pending_from,
lines: std::mem::take(&mut pending),
});
}
rows.push(Row::Line(line));
} else {
if pending.is_empty() {
pending_from = index;
}
pending.push(line);
}
}
if !pending.is_empty() {
rows.push(Row::Gap {
from: pending_from,
lines: pending,
});
}
rows
}
#[component]
pub fn DiffView(
before: String,
after: String,
#[props(default)]
scroll: Option<AccessibleName>,
#[props(default)]
context: Option<usize>,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let lines = model::diff_lines(&before, &after);
let stats = model::diff_stats(&lines);
let rows = fold(lines, context);
let mut opened = use_signal(Vec::<usize>::new);
let mut attributes = component_attributes(
attributes,
ClassList::new(BEM.block())
.with_modifier(
BEM,
if scroll.is_some() {
DiffLayout::Scroll
} else {
DiffLayout::Wrap
},
)
.extend(class.as_deref()),
&["dir", "role", "aria-label", "tabindex"],
);
if let Some(label) = &scroll {
attributes.push(Attribute::new("role", "region", None, false));
attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
attributes.push(Attribute::new("tabindex", "0", None, false));
}
let stat = |kind: StatKind| {
ClassList::new(STAT.block())
.with_modifier(STAT, kind)
.to_string()
};
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_DIFF_CSS } }
div { dir: "ltr", ..attributes,
div { class: BEM.element("bar"),
span { class: stat(StatKind::Added), "+{stats.added}" }
span { class: stat(StatKind::Removed), "−{stats.removed}" }
if stats.identical { span { class: stat(StatKind::Plain), "No changes" } }
}
div {
for row in rows {
match row {
Row::Line(line) => line_view(&line, false),
Row::Gap { from, lines } => {
let open = opened().contains(&from);
let count = lines.len();
let plural = if count == 1 { "" } else { "s" };
rsx! {
if !open {
button { key: "gap-{from}", class: BEM.element("gap"), r#type: "button",
onclick: move |_| opened.write().push(from),
"{count} unchanged line{plural}"
}
}
for line in lines { {line_view(&line, !open)} }
}
}
}
}
}
}
}
}
fn line_view(line: &model::DiffLine, hidden: bool) -> Element {
let (marker, label, data) = match line.kind {
model::DiffKind::Add => (
"+",
Some(format!("Added: {}", line.text)),
crate::spread![diff_attribute("add")],
),
model::DiffKind::Remove => (
"−",
Some(format!("Removed: {}", line.text)),
crate::spread![diff_attribute("remove")],
),
model::DiffKind::Equal => (" ", None, Spread::default()),
};
let old = line.old_line.map(|n| n.to_string()).unwrap_or_default();
let new = line.new_line.map(|n| n.to_string()).unwrap_or_default();
rsx! {
div { class: BEM.element("line"), hidden, aria_label: label, ..data,
span { class: BEM.element("marker"), aria_hidden: "true", "{marker}" }
span { class: BEM.element("num"), aria_hidden: "true", "{old}" }
span { class: BEM.element("num"), aria_hidden: "true", "{new}" }
span { class: BEM.element("content"), "{line.text}" }
}
}
}