facet_diff_core/layout/
node.rs

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