• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 20107877096

10 Dec 2025 05:40PM UTC coverage: 57.698% (-0.9%) from 58.588%
20107877096

Pull #1220

github

web-flow
Merge 20b27caa2 into fe1531898
Pull Request #1220: feat(cinereus): improve tree matching for leaf nodes and filter no-op moves

1748 of 3613 new or added lines in 18 files covered. (48.38%)

462 existing lines in 6 files now uncovered.

28548 of 49478 relevant lines covered (57.7%)

808.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.26
/facet-diff-core/src/layout/node.rs
1
//! Layout tree nodes.
2

3
use indextree::{Arena, NodeId};
4

5
use super::{Attr, ChangedGroup, FormatArena, FormattedValue};
6

7
/// How an element changed (affects its prefix and color).
8
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
9
pub 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

23
impl ElementChange {
24
    /// Get the prefix character for this change type.
25
    pub fn prefix(self) -> Option<char> {
11✔
26
        match self {
11✔
27
            Self::None => None,
5✔
28
            Self::Deleted => Some('-'),
2✔
29
            Self::Inserted => Some('+'),
2✔
30
            Self::MovedFrom => Some('←'),
1✔
31
            Self::MovedTo => Some('→'),
1✔
32
        }
33
    }
11✔
34

35
    /// Check if this change type uses any prefix.
NEW
36
    pub fn has_prefix(self) -> bool {
×
NEW
37
        !matches!(self, Self::None)
×
NEW
38
    }
×
39
}
40

41
/// A node in the layout tree.
42
#[derive(Clone, Debug)]
43
pub 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

97
impl LayoutNode {
98
    /// Create an element node with no changes.
99
    pub fn element(tag: &'static str) -> Self {
5✔
100
        Self::Element {
5✔
101
            tag,
5✔
102
            field_name: None,
5✔
103
            attrs: Vec::new(),
5✔
104
            changed_groups: Vec::new(),
5✔
105
            change: ElementChange::None,
5✔
106
        }
5✔
107
    }
5✔
108

109
    /// Create an element node with a specific change type.
NEW
110
    pub fn element_with_change(tag: &'static str, change: ElementChange) -> Self {
×
NEW
111
        Self::Element {
×
NEW
112
            tag,
×
NEW
113
            field_name: None,
×
NEW
114
            attrs: Vec::new(),
×
NEW
115
            changed_groups: Vec::new(),
×
NEW
116
            change,
×
NEW
117
        }
×
NEW
118
    }
×
119

120
    /// Create a sequence node.
NEW
121
    pub fn sequence(change: ElementChange, item_type: &'static str) -> Self {
×
NEW
122
        Self::Sequence {
×
NEW
123
            change,
×
NEW
124
            item_type,
×
NEW
125
            field_name: None,
×
NEW
126
        }
×
NEW
127
    }
×
128

129
    /// Create a collapsed node.
130
    pub fn collapsed(count: usize) -> Self {
2✔
131
        Self::Collapsed { count }
2✔
132
    }
2✔
133

134
    /// Create a text node.
NEW
135
    pub fn text(value: FormattedValue, change: ElementChange) -> Self {
×
NEW
136
        Self::Text { value, change }
×
NEW
137
    }
×
138

139
    /// Create an item group node.
NEW
140
    pub fn item_group(
×
NEW
141
        items: Vec<FormattedValue>,
×
NEW
142
        change: ElementChange,
×
NEW
143
        collapsed_suffix: Option<usize>,
×
NEW
144
        item_type: &'static str,
×
NEW
145
    ) -> Self {
×
NEW
146
        Self::ItemGroup {
×
NEW
147
            items,
×
NEW
148
            change,
×
NEW
149
            collapsed_suffix,
×
NEW
150
            item_type,
×
NEW
151
        }
×
NEW
152
    }
×
153

154
    /// Get the element change type (if applicable).
NEW
155
    pub fn change(&self) -> ElementChange {
×
NEW
156
        match self {
×
NEW
157
            Self::Element { change, .. } => *change,
×
NEW
158
            Self::Sequence { change, .. } => *change,
×
NEW
159
            Self::Collapsed { .. } => ElementChange::None,
×
NEW
160
            Self::Text { change, .. } => *change,
×
NEW
161
            Self::ItemGroup { change, .. } => *change,
×
162
        }
NEW
163
    }
×
164

165
    /// Check if this node has any actual changes to render.
166
    pub fn has_changes(&self) -> bool {
1✔
167
        match self {
1✔
168
            Self::Element {
NEW
169
                attrs,
×
NEW
170
                change,
×
NEW
171
                changed_groups,
×
172
                ..
173
            } => {
NEW
174
                change.has_prefix()
×
NEW
175
                    || !changed_groups.is_empty()
×
NEW
176
                    || attrs.iter().any(|a| {
×
NEW
177
                        matches!(
×
NEW
178
                            a.status,
×
179
                            super::AttrStatus::Changed { .. }
180
                                | super::AttrStatus::Deleted { .. }
181
                                | super::AttrStatus::Inserted { .. }
182
                        )
NEW
183
                    })
×
184
            }
NEW
185
            Self::Sequence { change, .. } => change.has_prefix(),
×
186
            Self::Collapsed { .. } => false,
1✔
NEW
187
            Self::Text { change, .. } => change.has_prefix(),
×
NEW
188
            Self::ItemGroup { change, .. } => change.has_prefix(),
×
189
        }
190
    }
1✔
191
}
192

193
/// The complete layout ready for rendering.
194
pub 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

203
impl 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 {
4✔
206
        let root = tree.new_node(root_node);
4✔
207
        Self {
4✔
208
            strings,
4✔
209
            tree,
4✔
210
            root,
4✔
211
        }
4✔
212
    }
4✔
213

214
    /// Get a reference to a node by ID.
215
    pub fn get(&self, id: NodeId) -> Option<&LayoutNode> {
10✔
216
        self.tree.get(id).map(|n| n.get())
10✔
217
    }
10✔
218

219
    /// Get a mutable reference to a node by ID.
NEW
220
    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut LayoutNode> {
×
NEW
221
        self.tree.get_mut(id).map(|n| n.get_mut())
×
NEW
222
    }
×
223

224
    /// Add a child to a parent node.
225
    pub fn add_child(&mut self, parent: NodeId, child_node: LayoutNode) -> NodeId {
2✔
226
        let child = self.tree.new_node(child_node);
2✔
227
        parent.append(child, &mut self.tree);
2✔
228
        child
2✔
229
    }
2✔
230

231
    /// Iterate over children of a node.
232
    pub fn children(&self, parent: NodeId) -> impl Iterator<Item = NodeId> + '_ {
7✔
233
        parent.children(&self.tree)
7✔
234
    }
7✔
235

236
    /// Get the string for a span from the arena.
237
    pub fn get_string(&self, span: super::Span) -> &str {
8✔
238
        self.strings.get(span)
8✔
239
    }
8✔
240
}
241

242
#[cfg(test)]
243
mod tests {
244
    use super::*;
245

246
    #[test]
247
    fn test_element_change_prefix() {
1✔
248
        assert_eq!(ElementChange::None.prefix(), None);
1✔
249
        assert_eq!(ElementChange::Deleted.prefix(), Some('-'));
1✔
250
        assert_eq!(ElementChange::Inserted.prefix(), Some('+'));
1✔
251
        assert_eq!(ElementChange::MovedFrom.prefix(), Some('←'));
1✔
252
        assert_eq!(ElementChange::MovedTo.prefix(), Some('→'));
1✔
253
    }
1✔
254

255
    #[test]
256
    fn test_layout_tree() {
1✔
257
        let arena = FormatArena::new();
1✔
258
        let tree = Arena::new();
1✔
259

260
        let mut layout = Layout::new(arena, tree, LayoutNode::element("root"));
1✔
261

262
        let child1 = layout.add_child(layout.root, LayoutNode::element("child1"));
1✔
263
        let child2 = layout.add_child(layout.root, LayoutNode::element("child2"));
1✔
264

265
        let children: Vec<_> = layout.children(layout.root).collect();
1✔
266
        assert_eq!(children.len(), 2);
1✔
267
        assert_eq!(children[0], child1);
1✔
268
        assert_eq!(children[1], child2);
1✔
269
    }
1✔
270

271
    #[test]
272
    fn test_collapsed_node() {
1✔
273
        let node = LayoutNode::collapsed(5);
1✔
274
        assert!(!node.has_changes());
1✔
275

276
        if let LayoutNode::Collapsed { count } = node {
1✔
277
            assert_eq!(count, 5);
1✔
278
        } else {
NEW
279
            panic!("expected Collapsed node");
×
280
        }
281
    }
1✔
282
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc