facet_diff_core/layout/
node.rs

1//! Layout tree nodes.
2
3use std::borrow::Cow;
4
5use indextree::{Arena, NodeId};
6
7use super::{Attr, ChangedGroup, FormatArena, FormattedValue};
8
9/// How an element changed (affects its prefix and color).
10#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
11pub enum ElementChange {
12    /// No change to the element itself (may have changed children/attrs)
13    #[default]
14    None,
15    /// Element was deleted (- prefix, red)
16    Deleted,
17    /// Element was inserted (+ prefix, green)
18    Inserted,
19    /// Element was moved from this position (← prefix, blue)
20    MovedFrom,
21    /// Element was moved to this position (→ prefix, blue)
22    MovedTo,
23}
24
25impl ElementChange {
26    /// Get the prefix character for this change type.
27    pub fn prefix(self) -> Option<char> {
28        match self {
29            Self::None => None,
30            Self::Deleted => Some('-'),
31            Self::Inserted => Some('+'),
32            Self::MovedFrom => Some('←'),
33            Self::MovedTo => Some('→'),
34        }
35    }
36
37    /// Check if this change type uses any prefix.
38    pub fn has_prefix(self) -> bool {
39        !matches!(self, Self::None)
40    }
41}
42
43/// A node in the layout tree.
44#[derive(Clone, Debug)]
45pub enum LayoutNode {
46    /// An element/struct with attributes and children.
47    Element {
48        /// Element tag name
49        tag: Cow<'static, str>,
50        /// Field name if this element is a struct field (e.g., "point" for `point: Inner`)
51        field_name: Option<&'static str>,
52        /// All attributes (unchanged, changed, deleted, inserted)
53        attrs: Vec<Attr>,
54        /// Changed attributes grouped for -/+ line alignment
55        changed_groups: Vec<ChangedGroup>,
56        /// How this element itself changed
57        change: ElementChange,
58    },
59
60    /// A sequence/array with children.
61    Sequence {
62        /// How this sequence itself changed
63        change: ElementChange,
64        /// The type name of items in this sequence (e.g., "i32", "Item")
65        item_type: &'static str,
66        /// Field name if this sequence is a struct field value (e.g., "elements" for `elements: [...]`)
67        field_name: Option<&'static str>,
68    },
69
70    /// A collapsed run of unchanged siblings.
71    Collapsed {
72        /// Number of collapsed elements
73        count: usize,
74    },
75
76    /// Text content.
77    Text {
78        /// Formatted text value
79        value: FormattedValue,
80        /// How this text changed
81        change: ElementChange,
82    },
83
84    /// A group of items rendered on a single line (for sequences).
85    /// Used to group consecutive unchanged/deleted/inserted items.
86    ItemGroup {
87        /// Formatted values for each visible item in the group.
88        items: Vec<FormattedValue>,
89        /// How these items changed (all have the same change type).
90        change: ElementChange,
91        /// Number of additional collapsed items (shown as "...N more").
92        /// None means all items are visible.
93        collapsed_suffix: Option<usize>,
94        /// The type name of items (e.g., "i32", "Item") for XML wrapping.
95        item_type: &'static str,
96    },
97}
98
99impl LayoutNode {
100    /// Create an element node with no changes.
101    pub fn element(tag: impl Into<Cow<'static, str>>) -> Self {
102        Self::Element {
103            tag: tag.into(),
104            field_name: None,
105            attrs: Vec::new(),
106            changed_groups: Vec::new(),
107            change: ElementChange::None,
108        }
109    }
110
111    /// Create an element node with a specific change type.
112    pub fn element_with_change(tag: impl Into<Cow<'static, str>>, change: ElementChange) -> Self {
113        Self::Element {
114            tag: tag.into(),
115            field_name: None,
116            attrs: Vec::new(),
117            changed_groups: Vec::new(),
118            change,
119        }
120    }
121
122    /// Create a sequence node.
123    pub fn sequence(change: ElementChange, item_type: &'static str) -> Self {
124        Self::Sequence {
125            change,
126            item_type,
127            field_name: None,
128        }
129    }
130
131    /// Create a collapsed node.
132    pub fn collapsed(count: usize) -> Self {
133        Self::Collapsed { count }
134    }
135
136    /// Create a text node.
137    pub fn text(value: FormattedValue, change: ElementChange) -> Self {
138        Self::Text { value, change }
139    }
140
141    /// Create an item group node.
142    pub fn item_group(
143        items: Vec<FormattedValue>,
144        change: ElementChange,
145        collapsed_suffix: Option<usize>,
146        item_type: &'static str,
147    ) -> Self {
148        Self::ItemGroup {
149            items,
150            change,
151            collapsed_suffix,
152            item_type,
153        }
154    }
155
156    /// Get the element change type (if applicable).
157    pub fn change(&self) -> ElementChange {
158        match self {
159            Self::Element { change, .. } => *change,
160            Self::Sequence { change, .. } => *change,
161            Self::Collapsed { .. } => ElementChange::None,
162            Self::Text { change, .. } => *change,
163            Self::ItemGroup { change, .. } => *change,
164        }
165    }
166
167    /// Check if this node has any actual changes to render.
168    pub fn has_changes(&self) -> bool {
169        match self {
170            Self::Element {
171                attrs,
172                change,
173                changed_groups,
174                ..
175            } => {
176                change.has_prefix()
177                    || !changed_groups.is_empty()
178                    || attrs.iter().any(|a| {
179                        matches!(
180                            a.status,
181                            super::AttrStatus::Changed { .. }
182                                | super::AttrStatus::Deleted { .. }
183                                | super::AttrStatus::Inserted { .. }
184                        )
185                    })
186            }
187            Self::Sequence { change, .. } => change.has_prefix(),
188            Self::Collapsed { .. } => false,
189            Self::Text { change, .. } => change.has_prefix(),
190            Self::ItemGroup { change, .. } => change.has_prefix(),
191        }
192    }
193}
194
195/// The complete layout ready for rendering.
196pub struct Layout {
197    /// Formatted strings arena
198    pub strings: FormatArena,
199    /// Tree of layout nodes
200    pub tree: Arena<LayoutNode>,
201    /// Root node ID
202    pub root: NodeId,
203}
204
205impl Layout {
206    /// Create a new layout with the given root node.
207    pub fn new(strings: FormatArena, mut tree: Arena<LayoutNode>, root_node: LayoutNode) -> Self {
208        let root = tree.new_node(root_node);
209        Self {
210            strings,
211            tree,
212            root,
213        }
214    }
215
216    /// Get a reference to a node by ID.
217    pub fn get(&self, id: NodeId) -> Option<&LayoutNode> {
218        self.tree.get(id).map(|n| n.get())
219    }
220
221    /// Get a mutable reference to a node by ID.
222    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut LayoutNode> {
223        self.tree.get_mut(id).map(|n| n.get_mut())
224    }
225
226    /// Add a child to a parent node.
227    pub fn add_child(&mut self, parent: NodeId, child_node: LayoutNode) -> NodeId {
228        let child = self.tree.new_node(child_node);
229        parent.append(child, &mut self.tree);
230        child
231    }
232
233    /// Iterate over children of a node.
234    pub fn children(&self, parent: NodeId) -> impl Iterator<Item = NodeId> + '_ {
235        parent.children(&self.tree)
236    }
237
238    /// Get the string for a span from the arena.
239    pub fn get_string(&self, span: super::Span) -> &str {
240        self.strings.get(span)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_element_change_prefix() {
250        assert_eq!(ElementChange::None.prefix(), None);
251        assert_eq!(ElementChange::Deleted.prefix(), Some('-'));
252        assert_eq!(ElementChange::Inserted.prefix(), Some('+'));
253        assert_eq!(ElementChange::MovedFrom.prefix(), Some('←'));
254        assert_eq!(ElementChange::MovedTo.prefix(), Some('→'));
255    }
256
257    #[test]
258    fn test_layout_tree() {
259        let arena = FormatArena::new();
260        let tree = Arena::new();
261
262        let mut layout = Layout::new(arena, tree, LayoutNode::element("root"));
263
264        let child1 = layout.add_child(layout.root, LayoutNode::element("child1"));
265        let child2 = layout.add_child(layout.root, LayoutNode::element("child2"));
266
267        let children: Vec<_> = layout.children(layout.root).collect();
268        assert_eq!(children.len(), 2);
269        assert_eq!(children[0], child1);
270        assert_eq!(children[1], child2);
271    }
272
273    #[test]
274    fn test_collapsed_node() {
275        let node = LayoutNode::collapsed(5);
276        assert!(!node.has_changes());
277
278        if let LayoutNode::Collapsed { count } = node {
279            assert_eq!(count, 5);
280        } else {
281            panic!("expected Collapsed node");
282        }
283    }
284}