use std::borrow::Cow;
use super::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ValueType {
String,
Number,
Boolean,
Null,
#[default]
Other,
}
#[derive(Copy, Clone, Debug, Default)]
pub struct FormattedValue {
pub span: Span,
pub width: usize,
pub value_type: ValueType,
}
impl FormattedValue {
pub const fn new(span: Span, width: usize) -> Self {
Self {
span,
width,
value_type: ValueType::Other,
}
}
pub const fn with_type(span: Span, width: usize, value_type: ValueType) -> Self {
Self {
span,
width,
value_type,
}
}
}
#[derive(Clone, Debug)]
pub enum AttrStatus {
Unchanged { value: FormattedValue },
Changed {
old: FormattedValue,
new: FormattedValue,
},
Deleted { value: FormattedValue },
Inserted { value: FormattedValue },
}
#[derive(Clone, Debug)]
pub struct Attr {
pub name: Cow<'static, str>,
pub name_width: usize,
pub status: AttrStatus,
}
impl Attr {
pub fn unchanged(
name: impl Into<Cow<'static, str>>,
name_width: usize,
value: FormattedValue,
) -> Self {
Self {
name: name.into(),
name_width,
status: AttrStatus::Unchanged { value },
}
}
pub fn changed(
name: impl Into<Cow<'static, str>>,
name_width: usize,
old: FormattedValue,
new: FormattedValue,
) -> Self {
Self {
name: name.into(),
name_width,
status: AttrStatus::Changed { old, new },
}
}
pub fn deleted(
name: impl Into<Cow<'static, str>>,
name_width: usize,
value: FormattedValue,
) -> Self {
Self {
name: name.into(),
name_width,
status: AttrStatus::Deleted { value },
}
}
pub fn inserted(
name: impl Into<Cow<'static, str>>,
name_width: usize,
value: FormattedValue,
) -> Self {
Self {
name: name.into(),
name_width,
status: AttrStatus::Inserted { value },
}
}
pub const fn is_changed(&self) -> bool {
matches!(self.status, AttrStatus::Changed { .. })
}
pub fn line_width(&self) -> usize {
let value_width = match &self.status {
AttrStatus::Unchanged { value } => value.width,
AttrStatus::Changed { old, new } => old.width.max(new.width),
AttrStatus::Deleted { value } => value.width,
AttrStatus::Inserted { value } => value.width,
};
self.name_width + 2 + value_width + 1
}
}
#[derive(Clone, Debug, Default)]
pub struct ChangedGroup {
pub attr_indices: Vec<usize>,
pub max_name_width: usize,
pub max_old_width: usize,
pub max_new_width: usize,
}
impl ChangedGroup {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, index: usize, attr: &Attr) {
self.attr_indices.push(index);
self.max_name_width = self.max_name_width.max(attr.name_width);
if let AttrStatus::Changed { old, new } = &attr.status {
self.max_old_width = self.max_old_width.max(old.width);
self.max_new_width = self.max_new_width.max(new.width);
}
}
pub const fn is_empty(&self) -> bool {
self.attr_indices.is_empty()
}
pub fn minus_line_width(&self, attrs: &[Attr]) -> usize {
if self.attr_indices.is_empty() {
return 0;
}
let mut width = 0;
for (i, &idx) in self.attr_indices.iter().enumerate() {
if i > 0 {
width += 1; }
let attr = &attrs[idx];
if let AttrStatus::Changed { .. } = &attr.status {
width += self.max_name_width + 2 + self.max_old_width + 1;
}
}
width
}
pub fn plus_line_width(&self, attrs: &[Attr]) -> usize {
if self.attr_indices.is_empty() {
return 0;
}
let mut width = 0;
for (i, &idx) in self.attr_indices.iter().enumerate() {
if i > 0 {
width += 1; }
let attr = &attrs[idx];
if let AttrStatus::Changed { .. } = &attr.status {
width += self.max_name_width + 2 + self.max_new_width + 1;
}
}
width
}
}
pub fn group_changed_attrs(
attrs: &[Attr],
max_line_width: usize,
indent_width: usize,
) -> Vec<ChangedGroup> {
let available_width = max_line_width.saturating_sub(indent_width + 2);
let mut groups = Vec::new();
let mut current = ChangedGroup::new();
let mut current_width = 0usize;
for (i, attr) in attrs.iter().enumerate() {
if !attr.is_changed() {
continue;
}
let attr_width = attr.line_width();
let needed = if current.is_empty() {
attr_width
} else {
attr_width + 1 };
if current_width + needed > available_width && !current.is_empty() {
groups.push(std::mem::take(&mut current));
current_width = 0;
}
let needed = if current.is_empty() {
attr_width
} else {
attr_width + 1
};
current_width += needed;
current.add(i, attr);
}
if !current.is_empty() {
groups.push(current);
}
groups
}
#[cfg(test)]
mod tests {
use super::*;
fn make_changed_attr(name: &'static str, old_width: usize, new_width: usize) -> Attr {
Attr::changed(
name,
name.len(),
FormattedValue::new(Span::default(), old_width),
FormattedValue::new(Span::default(), new_width),
)
}
#[test]
fn test_attr_line_width() {
let attr = make_changed_attr("fill", 3, 4);
assert_eq!(attr.line_width(), 4 + 2 + 4 + 1); }
#[test]
fn test_group_single_attr() {
let attrs = vec![make_changed_attr("fill", 3, 4)];
let groups = group_changed_attrs(&attrs, 80, 0);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].attr_indices, vec![0]);
}
#[test]
fn test_group_multiple_fit_one_line() {
let attrs = vec![
make_changed_attr("fill", 3, 4),
make_changed_attr("x", 2, 2),
];
let groups = group_changed_attrs(&attrs, 80, 0);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].attr_indices, vec![0, 1]);
}
#[test]
fn test_group_overflow_to_second_line() {
let attrs = vec![
make_changed_attr("fill", 3, 4),
make_changed_attr("x", 2, 2),
];
let groups = group_changed_attrs(&attrs, 15, 0);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].attr_indices, vec![0]);
assert_eq!(groups[1].attr_indices, vec![1]);
}
#[test]
fn test_group_skips_unchanged() {
let attrs = vec![
make_changed_attr("fill", 3, 4),
Attr::unchanged("x", 1, FormattedValue::new(Span::default(), 2)),
make_changed_attr("y", 2, 2),
];
let groups = group_changed_attrs(&attrs, 80, 0);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].attr_indices, vec![0, 2]); }
#[test]
fn test_group_max_widths() {
let attrs = vec![
make_changed_attr("fill", 3, 4), make_changed_attr("stroke", 5, 3), ];
let groups = group_changed_attrs(&attrs, 80, 0);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].max_name_width, 6); assert_eq!(groups[0].max_old_width, 5);
assert_eq!(groups[0].max_new_width, 4);
}
}