facet_diff_core/layout/
build.rs

1//! Build a Layout from a Diff.
2//!
3//! This module converts a `Diff<'mem, 'facet>` into a `Layout` that can be rendered.
4//!
5//! # Architecture
6//!
7//! The build process walks the Diff tree while simultaneously navigating the original
8//! `from` and `to` Peek values. This allows us to:
9//! - Look up unchanged field values from the original structs
10//! - Decide whether to show unchanged fields or collapse them
11//!
12//! The Diff itself only stores what changed - the original Peeks provide context.
13
14use std::borrow::Cow;
15
16use facet_core::{Def, NumericType, PrimitiveType, Shape, StructKind, TextualType, Type, UserType};
17use facet_reflect::Peek;
18use indextree::{Arena, NodeId};
19
20use super::{
21    Attr, DiffFlavor, ElementChange, FormatArena, FormattedValue, Layout, LayoutNode, ValueType,
22    group_changed_attrs,
23};
24use crate::{Diff, ReplaceGroup, Updates, UpdatesGroup, Value};
25
26/// Get the display name for a shape, respecting the `rename` attribute.
27fn get_shape_display_name(shape: &Shape) -> &'static str {
28    if let Some(renamed) = shape.get_builtin_attr_value::<&str>("rename") {
29        return renamed;
30    }
31    shape.type_identifier
32}
33
34/// Determine the type of a value for coloring purposes.
35fn determine_value_type(peek: Peek<'_, '_>) -> ValueType {
36    let shape = peek.shape();
37
38    // Check the Def first for special types like Option
39    if let Def::Option(_) = shape.def {
40        // Check if it's None
41        if let Ok(opt) = peek.into_option() {
42            if opt.is_none() {
43                return ValueType::Null;
44            }
45            // If Some, recurse to get inner type
46            if let Some(inner) = opt.value() {
47                return determine_value_type(inner);
48            }
49        }
50        return ValueType::Other;
51    }
52
53    // Check the Type for primitives
54    match shape.ty {
55        Type::Primitive(p) => match p {
56            PrimitiveType::Boolean => ValueType::Boolean,
57            PrimitiveType::Numeric(NumericType::Integer { .. })
58            | PrimitiveType::Numeric(NumericType::Float) => ValueType::Number,
59            PrimitiveType::Textual(TextualType::Char)
60            | PrimitiveType::Textual(TextualType::Str) => ValueType::String,
61            PrimitiveType::Never => ValueType::Null,
62        },
63        _ => ValueType::Other,
64    }
65}
66
67/// Options for building a layout from a diff.
68#[derive(Clone, Debug)]
69pub struct BuildOptions {
70    /// Maximum line width for attribute grouping.
71    pub max_line_width: usize,
72    /// Maximum number of unchanged fields to show inline.
73    /// If more than this many unchanged fields exist, collapse to "N unchanged".
74    pub max_unchanged_fields: usize,
75    /// Minimum run length to collapse unchanged sequence elements.
76    pub collapse_threshold: usize,
77}
78
79impl Default for BuildOptions {
80    fn default() -> Self {
81        Self {
82            max_line_width: 80,
83            max_unchanged_fields: 5,
84            collapse_threshold: 3,
85        }
86    }
87}
88
89/// Build a Layout from a Diff.
90///
91/// This is the main entry point for converting a diff into a renderable layout.
92///
93/// # Arguments
94///
95/// * `diff` - The diff to render
96/// * `from` - The original "from" value (for looking up unchanged fields)
97/// * `to` - The original "to" value (for looking up unchanged fields)
98/// * `opts` - Build options
99/// * `flavor` - The output flavor (Rust, JSON, XML)
100pub fn build_layout<'mem, 'facet, F: DiffFlavor>(
101    diff: &Diff<'mem, 'facet>,
102    from: Peek<'mem, 'facet>,
103    to: Peek<'mem, 'facet>,
104    opts: &BuildOptions,
105    flavor: &F,
106) -> Layout {
107    let mut builder = LayoutBuilder::new(opts.clone(), flavor);
108    let root_id = builder.build(diff, Some(from), Some(to));
109    builder.finish(root_id)
110}
111
112/// Internal builder state.
113struct LayoutBuilder<'f, F: DiffFlavor> {
114    /// Arena for formatted strings.
115    strings: FormatArena,
116    /// Arena for layout nodes.
117    tree: Arena<LayoutNode>,
118    /// Build options.
119    opts: BuildOptions,
120    /// Output flavor for formatting.
121    flavor: &'f F,
122}
123
124impl<'f, F: DiffFlavor> LayoutBuilder<'f, F> {
125    fn new(opts: BuildOptions, flavor: &'f F) -> Self {
126        Self {
127            strings: FormatArena::new(),
128            tree: Arena::new(),
129            opts,
130            flavor,
131        }
132    }
133
134    /// Build the layout from a diff, with optional context Peeks.
135    fn build<'mem, 'facet>(
136        &mut self,
137        diff: &Diff<'mem, 'facet>,
138        from: Option<Peek<'mem, 'facet>>,
139        to: Option<Peek<'mem, 'facet>>,
140    ) -> NodeId {
141        self.build_diff(diff, from, to, ElementChange::None)
142    }
143
144    /// Build a node from a diff with a given element change type.
145    fn build_diff<'mem, 'facet>(
146        &mut self,
147        diff: &Diff<'mem, 'facet>,
148        from: Option<Peek<'mem, 'facet>>,
149        to: Option<Peek<'mem, 'facet>>,
150        change: ElementChange,
151    ) -> NodeId {
152        match diff {
153            Diff::Equal { value } => {
154                // For equal values, render as unchanged text
155                if let Some(peek) = value {
156                    self.build_peek(*peek, ElementChange::None)
157                } else {
158                    // No value available, create a placeholder
159                    let (span, width) = self.strings.push_str("(equal)");
160                    let value = FormattedValue::new(span, width);
161                    self.tree.new_node(LayoutNode::Text {
162                        value,
163                        change: ElementChange::None,
164                    })
165                }
166            }
167            Diff::Replace { from, to } => {
168                // Create a container element with deleted and inserted children
169                let root = self.tree.new_node(LayoutNode::element("_replace"));
170
171                let from_node = self.build_peek(*from, ElementChange::Deleted);
172                let to_node = self.build_peek(*to, ElementChange::Inserted);
173
174                root.append(from_node, &mut self.tree);
175                root.append(to_node, &mut self.tree);
176
177                root
178            }
179            Diff::User {
180                from: from_shape,
181                to: _to_shape,
182                variant,
183                value,
184            } => {
185                // Get type name for the tag, respecting `rename` attribute
186                let tag = get_shape_display_name(from_shape);
187
188                match value {
189                    Value::Struct {
190                        updates,
191                        deletions,
192                        insertions,
193                        unchanged,
194                    } => self.build_struct(
195                        tag, *variant, updates, deletions, insertions, unchanged, from, to, change,
196                    ),
197                    Value::Tuple { updates } => {
198                        self.build_tuple(tag, *variant, updates, from, to, change)
199                    }
200                }
201            }
202            Diff::Sequence {
203                from: _seq_shape_from,
204                to: _seq_shape_to,
205                updates,
206            } => {
207                // Get item type from the from/to Peek values passed to build_diff
208                let item_type = from
209                    .and_then(|p| p.into_list_like().ok())
210                    .and_then(|list| list.iter().next())
211                    .or_else(|| {
212                        to.and_then(|p| p.into_list_like().ok())
213                            .and_then(|list| list.iter().next())
214                    })
215                    .map(|item| get_shape_display_name(item.shape()))
216                    .unwrap_or("item");
217                self.build_sequence(updates, change, item_type)
218            }
219        }
220    }
221
222    /// Build a node from a Peek value.
223    fn build_peek(&mut self, peek: Peek<'_, '_>, change: ElementChange) -> NodeId {
224        let shape = peek.shape();
225
226        // Check if this is a struct we can recurse into
227        match (shape.def, shape.ty) {
228            (_, Type::User(UserType::Struct(ty))) if ty.kind == StructKind::Struct => {
229                // Build as element with fields as attributes
230                if let Ok(struct_peek) = peek.into_struct() {
231                    let tag = get_shape_display_name(shape);
232                    let mut attrs = Vec::new();
233
234                    for (i, field) in ty.fields.iter().enumerate() {
235                        if let Ok(field_value) = struct_peek.field(i) {
236                            let formatted_value = self.format_peek(field_value);
237                            let attr = match change {
238                                ElementChange::None => {
239                                    Attr::unchanged(field.name, field.name.len(), formatted_value)
240                                }
241                                ElementChange::Deleted => {
242                                    Attr::deleted(field.name, field.name.len(), formatted_value)
243                                }
244                                ElementChange::Inserted => {
245                                    Attr::inserted(field.name, field.name.len(), formatted_value)
246                                }
247                                ElementChange::MovedFrom | ElementChange::MovedTo => {
248                                    // For moved elements, show fields as unchanged
249                                    Attr::unchanged(field.name, field.name.len(), formatted_value)
250                                }
251                            };
252                            attrs.push(attr);
253                        }
254                    }
255
256                    let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
257
258                    return self.tree.new_node(LayoutNode::Element {
259                        tag,
260                        field_name: None,
261                        attrs,
262                        changed_groups,
263                        change,
264                    });
265                }
266            }
267            (_, Type::User(UserType::Enum(_))) => {
268                // Build enum as element with variant name as tag
269                if let Ok(enum_peek) = peek.into_enum()
270                    && let Ok(variant) = enum_peek.active_variant()
271                {
272                    let tag = variant.name;
273                    let fields = &variant.data.fields;
274
275                    // If variant has fields, build as element with those fields
276                    if !fields.is_empty() {
277                        // Check for newtype pattern: single field with same-named inner type
278                        // e.g., `Circle(Circle)` where we want to show Circle's fields directly
279                        if fields.len() == 1
280                            && let Ok(Some(inner_value)) = enum_peek.field(0)
281                        {
282                            let inner_shape = inner_value.shape();
283                            // If it's a struct, recurse into it but use the variant name
284                            if let Type::User(UserType::Struct(s)) = inner_shape.ty
285                                && s.kind == StructKind::Struct
286                                && let Ok(struct_peek) = inner_value.into_struct()
287                            {
288                                let mut attrs = Vec::new();
289
290                                for (i, field) in s.fields.iter().enumerate() {
291                                    if let Ok(field_value) = struct_peek.field(i) {
292                                        let formatted_value = self.format_peek(field_value);
293                                        let attr = match change {
294                                            ElementChange::None => Attr::unchanged(
295                                                field.name,
296                                                field.name.len(),
297                                                formatted_value,
298                                            ),
299                                            ElementChange::Deleted => Attr::deleted(
300                                                field.name,
301                                                field.name.len(),
302                                                formatted_value,
303                                            ),
304                                            ElementChange::Inserted => Attr::inserted(
305                                                field.name,
306                                                field.name.len(),
307                                                formatted_value,
308                                            ),
309                                            ElementChange::MovedFrom | ElementChange::MovedTo => {
310                                                Attr::unchanged(
311                                                    field.name,
312                                                    field.name.len(),
313                                                    formatted_value,
314                                                )
315                                            }
316                                        };
317                                        attrs.push(attr);
318                                    }
319                                }
320
321                                let changed_groups =
322                                    group_changed_attrs(&attrs, self.opts.max_line_width, 0);
323
324                                return self.tree.new_node(LayoutNode::Element {
325                                    tag,
326                                    field_name: None,
327                                    attrs,
328                                    changed_groups,
329                                    change,
330                                });
331                            }
332                        }
333
334                        // General case: show variant fields directly
335                        let mut attrs = Vec::new();
336
337                        for (i, field) in fields.iter().enumerate() {
338                            if let Ok(Some(field_value)) = enum_peek.field(i) {
339                                let formatted_value = self.format_peek(field_value);
340                                let attr = match change {
341                                    ElementChange::None => Attr::unchanged(
342                                        field.name,
343                                        field.name.len(),
344                                        formatted_value,
345                                    ),
346                                    ElementChange::Deleted => {
347                                        Attr::deleted(field.name, field.name.len(), formatted_value)
348                                    }
349                                    ElementChange::Inserted => Attr::inserted(
350                                        field.name,
351                                        field.name.len(),
352                                        formatted_value,
353                                    ),
354                                    ElementChange::MovedFrom | ElementChange::MovedTo => {
355                                        Attr::unchanged(
356                                            field.name,
357                                            field.name.len(),
358                                            formatted_value,
359                                        )
360                                    }
361                                };
362                                attrs.push(attr);
363                            }
364                        }
365
366                        let changed_groups =
367                            group_changed_attrs(&attrs, self.opts.max_line_width, 0);
368
369                        return self.tree.new_node(LayoutNode::Element {
370                            tag,
371                            field_name: None,
372                            attrs,
373                            changed_groups,
374                            change,
375                        });
376                    } else {
377                        // Unit variant - just show the variant name as text
378                        let (span, width) = self.strings.push_str(tag);
379                        return self.tree.new_node(LayoutNode::Text {
380                            value: FormattedValue::new(span, width),
381                            change,
382                        });
383                    }
384                }
385            }
386            _ => {}
387        }
388
389        // Default: format as text
390        let formatted = self.format_peek(peek);
391        self.tree.new_node(LayoutNode::Text {
392            value: formatted,
393            change,
394        })
395    }
396
397    /// Build a struct diff as an element with attributes.
398    #[allow(clippy::too_many_arguments)]
399    fn build_struct<'mem, 'facet>(
400        &mut self,
401        tag: &'static str,
402        variant: Option<&'static str>,
403        updates: &std::collections::HashMap<Cow<'static, str>, Diff<'mem, 'facet>>,
404        deletions: &std::collections::HashMap<Cow<'static, str>, Peek<'mem, 'facet>>,
405        insertions: &std::collections::HashMap<Cow<'static, str>, Peek<'mem, 'facet>>,
406        unchanged: &std::collections::HashSet<Cow<'static, str>>,
407        from: Option<Peek<'mem, 'facet>>,
408        to: Option<Peek<'mem, 'facet>>,
409        change: ElementChange,
410    ) -> NodeId {
411        let element_tag = tag;
412
413        // If there's a variant, we should indicate it somehow.
414        // TODO: LayoutNode::Element should have an optional variant: Option<&'static str>
415        if variant.is_some() {
416            // For now, just use the tag
417        }
418
419        let mut attrs = Vec::new();
420        let mut child_nodes = Vec::new();
421
422        // Handle unchanged fields - try to get values from the original Peek
423        if !unchanged.is_empty() {
424            let unchanged_count = unchanged.len();
425
426            if unchanged_count <= self.opts.max_unchanged_fields {
427                // Show unchanged fields with their values (if we have the original Peek)
428                if let Some(from_peek) = from {
429                    if let Ok(struct_peek) = from_peek.into_struct() {
430                        let mut sorted_unchanged: Vec<_> = unchanged.iter().collect();
431                        sorted_unchanged.sort();
432
433                        for field_name in sorted_unchanged {
434                            if let Ok(field_value) = struct_peek.field_by_name(field_name) {
435                                let formatted = self.format_peek(field_value);
436                                let name_width = field_name.len();
437                                let attr =
438                                    Attr::unchanged(field_name.clone(), name_width, formatted);
439                                attrs.push(attr);
440                            }
441                        }
442                    }
443                } else {
444                    // No original Peek available - add a collapsed placeholder
445                    // We'll handle this after building the element
446                }
447            }
448            // If more than max_unchanged_fields, we'll add a collapsed node as a child
449        }
450
451        // Process updates - these become changed attributes or nested children
452        let mut sorted_updates: Vec<_> = updates.iter().collect();
453        sorted_updates.sort_by(|(a, _), (b, _)| a.cmp(b));
454
455        for (field_name, field_diff) in sorted_updates {
456            // Navigate into the field in from/to Peeks for nested context
457            let field_from = from.and_then(|p| {
458                p.into_struct()
459                    .ok()
460                    .and_then(|s| s.field_by_name(field_name).ok())
461            });
462            let field_to = to.and_then(|p| {
463                p.into_struct()
464                    .ok()
465                    .and_then(|s| s.field_by_name(field_name).ok())
466            });
467
468            match field_diff {
469                Diff::Replace { from, to } => {
470                    // Check if this is a complex type that should be built as children
471                    let from_shape = from.shape();
472                    let is_complex = match from_shape.ty {
473                        Type::User(UserType::Enum(_)) => true,
474                        Type::User(UserType::Struct(s)) if s.kind == StructKind::Struct => true,
475                        _ => false,
476                    };
477
478                    if is_complex {
479                        // Build from/to as separate child elements
480                        let from_node = self.build_peek(*from, ElementChange::Deleted);
481                        let to_node = self.build_peek(*to, ElementChange::Inserted);
482
483                        // Set field name on both nodes
484                        if let Cow::Borrowed(name) = field_name {
485                            if let Some(node) = self.tree.get_mut(from_node)
486                                && let LayoutNode::Element { field_name, .. } = node.get_mut()
487                            {
488                                *field_name = Some(name);
489                            }
490                            if let Some(node) = self.tree.get_mut(to_node)
491                                && let LayoutNode::Element { field_name, .. } = node.get_mut()
492                            {
493                                *field_name = Some(name);
494                            }
495                        }
496
497                        child_nodes.push(from_node);
498                        child_nodes.push(to_node);
499                    } else {
500                        // Scalar replacement - show as changed attribute
501                        let old_value = self.format_peek(*from);
502                        let new_value = self.format_peek(*to);
503                        let name_width = field_name.len();
504                        let attr =
505                            Attr::changed(field_name.clone(), name_width, old_value, new_value);
506                        attrs.push(attr);
507                    }
508                }
509                _ => {
510                    // Nested diff - build as child element or sequence
511                    let child =
512                        self.build_diff(field_diff, field_from, field_to, ElementChange::None);
513
514                    // Set the field name on the child (only for borrowed names for now)
515                    // TODO: Support owned field names for nested elements
516                    if let Cow::Borrowed(name) = field_name
517                        && let Some(node) = self.tree.get_mut(child)
518                    {
519                        match node.get_mut() {
520                            LayoutNode::Element { field_name, .. } => {
521                                *field_name = Some(name);
522                            }
523                            LayoutNode::Sequence { field_name, .. } => {
524                                *field_name = Some(name);
525                            }
526                            _ => {}
527                        }
528                    }
529
530                    child_nodes.push(child);
531                }
532            }
533        }
534
535        // Process deletions
536        let mut sorted_deletions: Vec<_> = deletions.iter().collect();
537        sorted_deletions.sort_by(|(a, _), (b, _)| a.cmp(b));
538
539        for (field_name, value) in sorted_deletions {
540            let formatted = self.format_peek(*value);
541            let name_width = field_name.len();
542            let attr = Attr::deleted(field_name.clone(), name_width, formatted);
543            attrs.push(attr);
544        }
545
546        // Process insertions
547        let mut sorted_insertions: Vec<_> = insertions.iter().collect();
548        sorted_insertions.sort_by(|(a, _), (b, _)| a.cmp(b));
549
550        for (field_name, value) in sorted_insertions {
551            let formatted = self.format_peek(*value);
552            let name_width = field_name.len();
553            let attr = Attr::inserted(field_name.clone(), name_width, formatted);
554            attrs.push(attr);
555        }
556
557        // Group changed attributes for alignment
558        let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
559
560        // Create the element node
561        let node = self.tree.new_node(LayoutNode::Element {
562            tag: element_tag,
563            field_name: None, // Will be set by parent if this is a struct field
564            attrs,
565            changed_groups,
566            change,
567        });
568
569        // Add children
570        for child in child_nodes {
571            node.append(child, &mut self.tree);
572        }
573
574        // Add collapsed unchanged fields indicator if needed
575        let unchanged_count = unchanged.len();
576        if unchanged_count > self.opts.max_unchanged_fields
577            || (unchanged_count > 0 && from.is_none())
578        {
579            let collapsed = self.tree.new_node(LayoutNode::collapsed(unchanged_count));
580            node.append(collapsed, &mut self.tree);
581        }
582
583        node
584    }
585
586    /// Build a tuple diff.
587    fn build_tuple<'mem, 'facet>(
588        &mut self,
589        tag: &'static str,
590        variant: Option<&'static str>,
591        updates: &Updates<'mem, 'facet>,
592        _from: Option<Peek<'mem, 'facet>>,
593        _to: Option<Peek<'mem, 'facet>>,
594        change: ElementChange,
595    ) -> NodeId {
596        // Same variant issue as build_struct
597        if variant.is_some() {
598            // TODO: LayoutNode::Element should support variant display
599        }
600
601        // Create element for the tuple
602        let node = self.tree.new_node(LayoutNode::Element {
603            tag,
604            field_name: None,
605            attrs: Vec::new(),
606            changed_groups: Vec::new(),
607            change,
608        });
609
610        // Build children from updates (tuple items don't have specific type names)
611        self.build_updates_children(node, updates, "item");
612
613        node
614    }
615
616    /// Build a sequence diff.
617    fn build_sequence(
618        &mut self,
619        updates: &Updates<'_, '_>,
620        change: ElementChange,
621        item_type: &'static str,
622    ) -> NodeId {
623        // Create sequence node with item type info
624        let node = self.tree.new_node(LayoutNode::Sequence {
625            change,
626            item_type,
627            field_name: None,
628        });
629
630        // Build children from updates
631        self.build_updates_children(node, updates, item_type);
632
633        node
634    }
635
636    /// Build children from an Updates structure and append to parent.
637    ///
638    /// This groups consecutive items by their change type (unchanged, deleted, inserted)
639    /// and renders them on single lines with optional collapsing for long runs.
640    /// Nested diffs (struct items with internal changes) are built as full child nodes.
641    fn build_updates_children(
642        &mut self,
643        parent: NodeId,
644        updates: &Updates<'_, '_>,
645        _item_type: &'static str,
646    ) {
647        // Collect simple items (adds/removes) and nested diffs separately
648        let mut items: Vec<(Peek<'_, '_>, ElementChange)> = Vec::new();
649        let mut nested_diffs: Vec<&Diff<'_, '_>> = Vec::new();
650
651        let interspersed = &updates.0;
652
653        // Process first update group if present
654        if let Some(update_group) = &interspersed.first {
655            self.collect_updates_group_items(&mut items, &mut nested_diffs, update_group);
656        }
657
658        // Process interleaved (unchanged, update) pairs
659        for (unchanged_items, update_group) in &interspersed.values {
660            // Add unchanged items
661            for item in unchanged_items {
662                items.push((*item, ElementChange::None));
663            }
664
665            self.collect_updates_group_items(&mut items, &mut nested_diffs, update_group);
666        }
667
668        // Process trailing unchanged items
669        if let Some(unchanged_items) = &interspersed.last {
670            for item in unchanged_items {
671                items.push((*item, ElementChange::None));
672            }
673        }
674
675        tracing::debug!(
676            items_count = items.len(),
677            nested_diffs_count = nested_diffs.len(),
678            "collected sequence items"
679        );
680
681        // Build nested diffs as full child nodes (struct items with internal changes)
682        for diff in nested_diffs {
683            // Get from/to Peek from the diff for context
684            let (from_peek, to_peek) = match diff {
685                Diff::User { .. } => {
686                    // For User diffs, we need the actual Peek values
687                    // The diff contains the shapes but we need to find the corresponding Peeks
688                    // For now, pass None - the build_diff will use the shape info
689                    (None, None)
690                }
691                Diff::Replace { from, to } => (Some(*from), Some(*to)),
692                _ => (None, None),
693            };
694            let child = self.build_diff(diff, from_peek, to_peek, ElementChange::None);
695            parent.append(child, &mut self.tree);
696        }
697
698        // TODO: Also handle simple items (adds/removes) - for now they're not rendered
699        // This is fine since nested diffs are the main use case
700        let _ = items; // suppress unused warning for now
701    }
702
703    /// Collect items from an UpdatesGroup into the items list.
704    /// Also returns nested diffs that need to be built as full child nodes.
705    fn collect_updates_group_items<'a, 'mem: 'a, 'facet: 'a>(
706        &self,
707        items: &mut Vec<(Peek<'mem, 'facet>, ElementChange)>,
708        nested_diffs: &mut Vec<&'a Diff<'mem, 'facet>>,
709        group: &'a UpdatesGroup<'mem, 'facet>,
710    ) {
711        let interspersed = &group.0;
712
713        // Process first replace group if present
714        if let Some(replace) = &interspersed.first {
715            self.collect_replace_group_items(items, replace);
716        }
717
718        // Process interleaved (diffs, replace) pairs
719        for (diffs, replace) in &interspersed.values {
720            // Collect nested diffs - these are struct items with internal changes
721            for diff in diffs {
722                nested_diffs.push(diff);
723            }
724            self.collect_replace_group_items(items, replace);
725        }
726
727        // Process trailing diffs (if any)
728        if let Some(diffs) = &interspersed.last {
729            for diff in diffs {
730                nested_diffs.push(diff);
731            }
732        }
733    }
734
735    /// Collect items from a ReplaceGroup into the items list.
736    fn collect_replace_group_items<'a, 'mem: 'a, 'facet: 'a>(
737        &self,
738        items: &mut Vec<(Peek<'mem, 'facet>, ElementChange)>,
739        group: &'a ReplaceGroup<'mem, 'facet>,
740    ) {
741        // Add removals as deleted
742        for removal in &group.removals {
743            items.push((*removal, ElementChange::Deleted));
744        }
745
746        // Add additions as inserted
747        for addition in &group.additions {
748            items.push((*addition, ElementChange::Inserted));
749        }
750    }
751
752    /// Format a Peek value into the arena using the flavor.
753    fn format_peek(&mut self, peek: Peek<'_, '_>) -> FormattedValue {
754        let (span, width) = self.strings.format(|w| self.flavor.format_value(peek, w));
755        let value_type = determine_value_type(peek);
756        FormattedValue::with_type(span, width, value_type)
757    }
758
759    /// Finish building and return the Layout.
760    fn finish(self, root: NodeId) -> Layout {
761        Layout {
762            strings: self.strings,
763            tree: self.tree,
764            root,
765        }
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use crate::layout::render::{RenderOptions, render_to_string};
773    use crate::layout::{RustFlavor, XmlFlavor};
774
775    #[test]
776    fn test_build_equal_diff() {
777        let value = 42i32;
778        let peek = Peek::new(&value);
779        let diff = Diff::Equal { value: Some(peek) };
780
781        let layout = build_layout(&diff, peek, peek, &BuildOptions::default(), &RustFlavor);
782
783        // Should produce a single text node
784        let root = layout.get(layout.root).unwrap();
785        assert!(matches!(root, LayoutNode::Text { .. }));
786    }
787
788    #[test]
789    fn test_build_replace_diff() {
790        let from = 10i32;
791        let to = 20i32;
792        let diff = Diff::Replace {
793            from: Peek::new(&from),
794            to: Peek::new(&to),
795        };
796
797        let layout = build_layout(
798            &diff,
799            Peek::new(&from),
800            Peek::new(&to),
801            &BuildOptions::default(),
802            &RustFlavor,
803        );
804
805        // Should produce an element with two children
806        let root = layout.get(layout.root).unwrap();
807        assert!(matches!(
808            root,
809            LayoutNode::Element {
810                tag: "_replace",
811                ..
812            }
813        ));
814
815        let children: Vec<_> = layout.children(layout.root).collect();
816        assert_eq!(children.len(), 2);
817    }
818
819    #[test]
820    fn test_build_and_render_replace() {
821        let from = 10i32;
822        let to = 20i32;
823        let diff = Diff::Replace {
824            from: Peek::new(&from),
825            to: Peek::new(&to),
826        };
827
828        let layout = build_layout(
829            &diff,
830            Peek::new(&from),
831            Peek::new(&to),
832            &BuildOptions::default(),
833            &RustFlavor,
834        );
835        let output = render_to_string(&layout, &RenderOptions::plain(), &XmlFlavor);
836
837        // Should contain both values with appropriate markers
838        assert!(
839            output.contains("10"),
840            "output should contain old value: {}",
841            output
842        );
843        assert!(
844            output.contains("20"),
845            "output should contain new value: {}",
846            output
847        );
848    }
849
850    #[test]
851    fn test_build_options_default() {
852        let opts = BuildOptions::default();
853        assert_eq!(opts.max_line_width, 80);
854        assert_eq!(opts.max_unchanged_fields, 5);
855        assert_eq!(opts.collapse_threshold, 3);
856    }
857}