Skip to main content

yaml_edit/
debug.rs

1//! Debug utilities for inspecting and understanding YAML structures.
2//!
3//! This module provides debugging tools including:
4//! - CST tree visualization
5//! - Pretty Debug formatting showing actual values
6//! - Visual diffs between documents
7//! - Deep value inspection
8//! - AST visualization (GraphViz output)
9//!
10//! These functions are useful for both contributors working on yaml-edit
11//! and users debugging their YAML documents.
12
13use crate::as_yaml::YamlNode;
14use crate::lex::SyntaxKind;
15use crate::nodes::has_child_token;
16use crate::yaml::{Document, Mapping, Scalar, Sequence, SyntaxNode};
17use std::fmt;
18
19/// Prints the CST (Concrete Syntax Tree) structure to stdout.
20///
21/// This is invaluable for understanding how YAML is parsed into the tree structure,
22/// debugging formatting issues, and verifying that mutations produce the expected tree.
23///
24/// # Example
25///
26/// ```
27/// use yaml_edit::{YamlFile, debug};
28/// use rowan::ast::AstNode;
29/// use std::str::FromStr;
30///
31/// let yaml = YamlFile::from_str("team:\n  - Alice\n  - Bob").unwrap();
32/// debug::print_tree(yaml.syntax());
33/// ```
34///
35/// Output:
36/// ```text
37/// DOCUMENT
38///   MAPPING
39///     MAPPING_ENTRY
40///       KEY
41///         SCALAR
42///           STRING: "team"
43///       COLON: ":"
44///       VALUE
45///         NEWLINE: "\n"
46///         INDENT: "  "
47///         SEQUENCE
48///           SEQUENCE_ENTRY
49///             DASH: "-"
50///             WHITESPACE: " "
51///             SCALAR
52///               STRING: "Alice"
53///             NEWLINE: "\n"
54/// ...
55/// ```
56pub fn print_tree(node: &SyntaxNode) {
57    print_tree_indent(node, 0);
58}
59
60/// Prints the CST structure with a custom starting indentation.
61pub fn print_tree_indent(node: &SyntaxNode, indent: usize) {
62    let mut out = String::new();
63    tree_to_string_indent(node, indent, &mut out);
64    print!("{out}");
65}
66
67/// Returns a string representation of the CST structure.
68///
69/// Like `print_tree` but returns a string instead of printing to stdout.
70///
71/// # Example
72///
73/// ```
74/// use yaml_edit::{YamlFile, debug};
75/// use rowan::ast::AstNode;
76/// use std::str::FromStr;
77///
78/// let yaml = YamlFile::from_str("name: Alice").unwrap();
79/// let tree_str = debug::tree_to_string(yaml.syntax());
80///
81/// let expected = "DOCUMENT\n  MAPPING\n    MAPPING_ENTRY\n      KEY\n        SCALAR\n          STRING: \"name\"\n      COLON: \":\"\n      WHITESPACE: \" \"\n      VALUE\n        SCALAR\n          STRING: \"Alice\"\n";
82/// assert_eq!(tree_str, expected);
83/// ```
84pub fn tree_to_string(node: &SyntaxNode) -> String {
85    let mut result = String::new();
86    tree_to_string_indent(node, 0, &mut result);
87    result
88}
89
90fn tree_to_string_indent(node: &SyntaxNode, indent: usize, result: &mut String) {
91    for child in node.children_with_tokens() {
92        let prefix = "  ".repeat(indent);
93        match child {
94            rowan::NodeOrToken::Node(n) => {
95                result.push_str(&format!("{}{:?}\n", prefix, n.kind()));
96                tree_to_string_indent(&n, indent + 1, result);
97            }
98            rowan::NodeOrToken::Token(t) => {
99                let text = t.text().replace('\n', "\\n").replace('\r', "\\r");
100                result.push_str(&format!("{}{:?}: {:?}\n", prefix, t.kind(), text));
101            }
102        }
103    }
104}
105
106/// Prints statistics about a YAML document's CST.
107///
108/// Shows counts of different node and token types, which can be useful
109/// for understanding document complexity and debugging issues.
110///
111/// # Example
112///
113/// ```
114/// use yaml_edit::{YamlFile, debug};
115/// use rowan::ast::AstNode;
116/// use std::str::FromStr;
117///
118/// let yaml = YamlFile::from_str("team:\n  - Alice\n  - Bob").unwrap();
119/// debug::print_stats(yaml.syntax());
120/// ```
121pub fn print_stats(node: &SyntaxNode) {
122    let mut node_counts = std::collections::HashMap::new();
123    let mut token_counts = std::collections::HashMap::new();
124
125    count_nodes(node, &mut node_counts, &mut token_counts);
126
127    println!("=== Node Counts ===");
128    let mut node_vec: Vec<_> = node_counts.iter().collect();
129    node_vec.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
130    for (kind, count) in node_vec {
131        println!("  {kind:?}: {count}");
132    }
133
134    println!("\n=== Token Counts ===");
135    let mut token_vec: Vec<_> = token_counts.iter().collect();
136    token_vec.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
137    for (kind, count) in token_vec {
138        println!("  {kind:?}: {count}");
139    }
140}
141
142fn count_nodes(
143    node: &SyntaxNode,
144    node_counts: &mut std::collections::HashMap<SyntaxKind, usize>,
145    token_counts: &mut std::collections::HashMap<SyntaxKind, usize>,
146) {
147    *node_counts.entry(node.kind()).or_insert(0) += 1;
148
149    for child in node.children_with_tokens() {
150        match child {
151            rowan::NodeOrToken::Node(n) => {
152                count_nodes(&n, node_counts, token_counts);
153            }
154            rowan::NodeOrToken::Token(t) => {
155                *token_counts.entry(t.kind()).or_insert(0) += 1;
156            }
157        }
158    }
159}
160
161/// Validates that the CST follows expected structural invariants.
162///
163/// This is useful for testing that mutations don't break tree structure.
164/// Returns `Ok(())` if the tree is valid, or an error message if issues are found.
165///
166/// Current checks:
167/// - Every MAPPING_ENTRY has exactly one KEY
168/// - Every MAPPING_ENTRY has exactly one COLON (zero allowed for
169///   explicit-key entries with implicit-null values)
170/// - Every MAPPING_ENTRY has at most one VALUE
171/// - Block-style MAPPING_ENTRY and SEQUENCE_ENTRY nodes are terminated
172///   by a NEWLINE -- either as their own last token, or via a trailing
173///   comment/newline pair at the parent level, or (for MAPPING_ENTRYs)
174///   an implicit-null value
175/// - No two adjacent INDENT tokens (would render as doubled indentation)
176/// - No two adjacent NEWLINE tokens at the tail of a MAPPING_ENTRY (would
177///   render as a stray blank line)
178///
179/// # Example
180///
181/// ```
182/// use yaml_edit::{YamlFile, debug};
183/// use rowan::ast::AstNode;
184/// use std::str::FromStr;
185///
186/// let yaml = YamlFile::from_str("name: Alice\n").unwrap();
187/// debug::validate_tree(yaml.syntax()).expect("Tree should be valid");
188/// ```
189pub fn validate_tree(node: &SyntaxNode) -> Result<(), String> {
190    validate_node(node)?;
191    validate_no_stacked_indents(node)?;
192    validate_no_double_trailing_newline(node)?;
193    Ok(())
194}
195
196/// Roundtrip check: `parse(node.to_string()).to_string()` must equal
197/// `node.to_string()`, and the re-parse must have no errors.
198///
199/// This catches classes of bugs where a mutation produces text that
200/// looks acceptable but re-parses into a different structure (e.g.
201/// missing/extra indentation that changes nesting).
202///
203/// Note: this shares our parser's blindspots. If a mutation produces
204/// a text that our parser reads back into the same shape as the
205/// pre-mutation CST, the check passes even when a different YAML
206/// parser would disagree. It is *not* a differential test against an
207/// oracle parser.
208pub fn roundtrip_ok(node: &SyntaxNode) -> Result<(), String> {
209    let text = node.to_string();
210    let parse = crate::Parse::parse_yaml(&text);
211    if !parse.positioned_errors().is_empty() {
212        return Err(format!(
213            "re-parse produced {} error(s); first: {}",
214            parse.positioned_errors().len(),
215            parse.positioned_errors()[0].message,
216        ));
217    }
218    let round = parse.tree().to_string();
219    if round != text {
220        return Err(format!(
221            "roundtrip mismatch:\n  original: {text:?}\n  reparsed: {round:?}",
222        ));
223    }
224    Ok(())
225}
226
227fn validate_node(node: &SyntaxNode) -> Result<(), String> {
228    match node.kind() {
229        SyntaxKind::MAPPING_ENTRY => {
230            let keys: Vec<_> = node
231                .children()
232                .filter(|c| c.kind() == SyntaxKind::KEY)
233                .collect();
234            if keys.len() != 1 {
235                return Err(format!(
236                    "MAPPING_ENTRY should have exactly 1 KEY, found {}",
237                    keys.len()
238                ));
239            }
240
241            // A MAPPING_ENTRY normally has exactly one COLON. Two forms
242            // may lack it:
243            //   - Explicit-key entries (`? key\n`) with implicit-null value.
244            //   - Flow shorthand entries (`{key}`, `{,}`) where the entire
245            //     entry is implicit key: implicit-value with no COLON.
246            let colons: Vec<_> = node
247                .children_with_tokens()
248                .filter(|c| c.as_token().is_some_and(|t| t.kind() == SyntaxKind::COLON))
249                .collect();
250            let is_explicit_key = has_child_token(node, |k| k == SyntaxKind::QUESTION);
251            let in_flow = is_in_flow_collection(node);
252            let expected_colons = if is_explicit_key || in_flow {
253                0..=1
254            } else {
255                1..=1
256            };
257            if !expected_colons.contains(&colons.len()) {
258                return Err(format!(
259                    "MAPPING_ENTRY should have {} COLON(s), found {}",
260                    if is_explicit_key || in_flow {
261                        "0 or 1"
262                    } else {
263                        "exactly 1"
264                    },
265                    colons.len()
266                ));
267            }
268
269            let values: Vec<_> = node
270                .children()
271                .filter(|c| c.kind() == SyntaxKind::VALUE)
272                .collect();
273            if values.len() > 1 {
274                return Err(format!(
275                    "MAPPING_ENTRY should have at most 1 VALUE, found {}",
276                    values.len()
277                ));
278            }
279
280            // Block MAPPING_ENTRYs must be terminated by a NEWLINE
281            // *somewhere* between themselves and the next sibling entry --
282            // usually inside the entry, sometimes as a trailing
283            // comment+newline at the parent level, sometimes absent
284            // entirely for explicit-key entries with implicit-null
285            // values (`? a\n`).
286            //
287            // The last entry of a mapping is allowed to lack a
288            // terminator: an unterminated source doc (`a: 1\nb: 2`)
289            // is legitimate YAML and must roundtrip.
290            if !is_in_flow_collection(node)
291                && !ends_with_implicit_null(node)
292                && !is_last_entry_in_parent(node)
293                && !entry_is_terminated(node)
294            {
295                return Err(format!(
296                    "Block-style MAPPING_ENTRY not terminated by a NEWLINE (last token: {:?})",
297                    node.last_token().map(|t| t.kind())
298                ));
299            }
300        }
301        SyntaxKind::SEQUENCE_ENTRY
302            if !is_in_flow_collection(node)
303                && !is_last_entry_in_parent(node)
304                && !entry_is_terminated(node) =>
305        {
306            return Err(format!(
307                "Block-style SEQUENCE_ENTRY not terminated by a NEWLINE (last token: {:?})",
308                node.last_token().map(|t| t.kind())
309            ));
310        }
311        _ => {}
312    }
313
314    // Recursively validate children
315    for child in node.children() {
316        validate_node(&child)?;
317    }
318
319    Ok(())
320}
321
322/// Is this entry terminated by a NEWLINE, either as its own last
323/// token or somewhere between itself and the next sibling that starts
324/// meaningful content (another entry, or something that isn't a
325/// trailing WHITESPACE/COMMENT)?
326///
327/// The parser sometimes lifts an entry's terminating NEWLINE out to
328/// the parent when a trailing comment sits between them -- the
329/// SEQUENCE_ENTRY ends with a STRING, then WHITESPACE, COMMENT, and
330/// NEWLINE follow at the SEQUENCE level. That's still a well-formed
331/// terminated entry.
332fn entry_is_terminated(entry: &SyntaxNode) -> bool {
333    if entry
334        .last_token()
335        .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
336    {
337        return true;
338    }
339    let mut sib = entry.next_sibling_or_token();
340    while let Some(el) = sib {
341        match el.as_token().map(|t| t.kind()) {
342            Some(SyntaxKind::NEWLINE) => return true,
343            Some(SyntaxKind::WHITESPACE | SyntaxKind::COMMENT) => {
344                sib = el.next_sibling_or_token();
345            }
346            _ => return false,
347        }
348    }
349    false
350}
351
352/// Is this entry the last MAPPING_ENTRY / SEQUENCE_ENTRY child of its
353/// parent MAPPING / SEQUENCE?
354///
355/// The last entry of a block collection is allowed to lack a trailing
356/// NEWLINE (an unterminated source doc, `a: 1\nb: 2`, is legitimate
357/// YAML and must roundtrip).
358fn is_last_entry_in_parent(entry: &SyntaxNode) -> bool {
359    // Walk siblings after `entry` looking for another of the same
360    // kind. O(distance-to-next-same-kind-sibling) per call, so the
361    // total cost across all entries in one parent is O(n), not O(n^2).
362    let kind = entry.kind();
363    let mut sib = entry.next_sibling();
364    while let Some(s) = sib {
365        if s.kind() == kind {
366            return false;
367        }
368        sib = s.next_sibling();
369    }
370    true
371}
372
373/// Does this MAPPING_ENTRY's terminal VALUE hold a zero-width
374/// implicit-null scalar?
375///
376/// The parser emits `SCALAR { NULL "" }` for an entry like `? key\n`
377/// with no explicit value -- the entry's last leaf is that zero-width
378/// NULL, not a NEWLINE. That's valid parser output; the trailing
379/// NEWLINE character lives inside the KEY subtree instead.
380fn ends_with_implicit_null(entry: &SyntaxNode) -> bool {
381    let Some(last_token) = entry.last_token() else {
382        return false;
383    };
384    last_token.kind() == SyntaxKind::NULL && last_token.text().is_empty()
385}
386
387/// Walk every node and reject adjacent INDENT tokens.
388///
389/// Two INDENTs concatenate at render time -- the sequence-under-empty-key
390/// bug (fixed for #38) manifested exactly this way (parent VALUE's INDENT
391/// followed by SEQUENCE's own INDENT rendered as 4 spaces).
392fn validate_no_stacked_indents(node: &SyntaxNode) -> Result<(), String> {
393    let mut prev_was_indent = false;
394    for child in node.children_with_tokens() {
395        match &child {
396            rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::INDENT => {
397                if prev_was_indent {
398                    return Err(format!(
399                        "adjacent INDENT tokens inside {:?} -- would render as stacked indentation",
400                        node.kind()
401                    ));
402                }
403                prev_was_indent = true;
404            }
405            rowan::NodeOrToken::Token(_) => prev_was_indent = false,
406            rowan::NodeOrToken::Node(n) => {
407                prev_was_indent = false;
408                validate_no_stacked_indents(n)?;
409            }
410        }
411    }
412    Ok(())
413}
414
415/// An entry should not end with two adjacent NEWLINE tokens as direct
416/// children.
417///
418/// That shape shows up when a placeholder NEWLINE isn't stripped after a
419/// child collection gains content (see issue #18 and the sequence-under-
420/// empty-key bug). Renders as a spurious blank line.
421///
422/// Only checked for MAPPING_ENTRY and SEQUENCE_ENTRY. Not applied to
423/// MAPPING / SEQUENCE containers themselves: those legitimately carry
424/// bare NEWLINE tokens between entries (blank-line separators are
425/// valid formatting) and at the tail (trailing blank lines are valid
426/// YAML input).
427fn validate_no_double_trailing_newline(node: &SyntaxNode) -> Result<(), String> {
428    let checks_apply = matches!(
429        node.kind(),
430        SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
431    );
432    if checks_apply {
433        let tokens: Vec<_> = node
434            .children_with_tokens()
435            .filter_map(|c| c.into_token())
436            .collect();
437        let last = tokens.len();
438        if last >= 2
439            && tokens[last - 1].kind() == SyntaxKind::NEWLINE
440            && tokens[last - 2].kind() == SyntaxKind::NEWLINE
441        {
442            return Err(format!(
443                "{:?} ends with two NEWLINE tokens -- would render as a stray blank line: {:?}",
444                node.kind(),
445                node.text().to_string(),
446            ));
447        }
448    }
449    for child in node.children() {
450        validate_no_double_trailing_newline(&child)?;
451    }
452    Ok(())
453}
454
455fn is_in_flow_collection(node: &SyntaxNode) -> bool {
456    // Look at the parent MAPPING or SEQUENCE node
457    let mut current = node.parent();
458    while let Some(parent) = current {
459        match parent.kind() {
460            // Flow collections carry their opening bracket as a direct child.
461            SyntaxKind::MAPPING => {
462                return has_child_token(&parent, |k| k == SyntaxKind::LEFT_BRACE)
463            }
464            SyntaxKind::SEQUENCE => {
465                return has_child_token(&parent, |k| k == SyntaxKind::LEFT_BRACKET)
466            }
467            _ => current = parent.parent(),
468        }
469    }
470    false
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::yaml::YamlFile;
477    use rowan::ast::AstNode;
478    use std::str::FromStr;
479
480    #[test]
481    fn test_print_tree() {
482        let yaml = YamlFile::from_str("name: Alice").unwrap();
483        // Should not panic
484        print_tree(yaml.syntax());
485    }
486
487    #[test]
488    fn test_tree_to_string() {
489        let yaml = YamlFile::from_str("name: Alice").unwrap();
490        let s = tree_to_string(yaml.syntax());
491        let expected = "DOCUMENT\n  MAPPING\n    MAPPING_ENTRY\n      KEY\n        SCALAR\n          STRING: \"name\"\n      COLON: \":\"\n      WHITESPACE: \" \"\n      VALUE\n        SCALAR\n          STRING: \"Alice\"\n";
492        assert_eq!(s, expected);
493    }
494
495    #[test]
496    fn test_tree_to_string_sequence() {
497        let yaml = YamlFile::from_str("- item1\n- item2\n").unwrap();
498        let s = tree_to_string(yaml.syntax());
499        assert_eq!(
500            s,
501            concat!(
502                "DOCUMENT\n",
503                "  SEQUENCE\n",
504                "    SEQUENCE_ENTRY\n",
505                "      DASH: \"-\"\n",
506                "      WHITESPACE: \" \"\n",
507                "      SCALAR\n",
508                "        STRING: \"item1\"\n",
509                "      NEWLINE: \"\\\\n\"\n",
510                "    SEQUENCE_ENTRY\n",
511                "      DASH: \"-\"\n",
512                "      WHITESPACE: \" \"\n",
513                "      SCALAR\n",
514                "        STRING: \"item2\"\n",
515                "      NEWLINE: \"\\\\n\"\n",
516            )
517        );
518    }
519
520    #[test]
521    fn test_print_stats() {
522        let yaml = YamlFile::from_str("name: Alice\nage: 30\n").unwrap();
523        // Should not panic
524        print_stats(yaml.syntax());
525    }
526
527    #[test]
528    fn test_validate_tree() {
529        let yaml = YamlFile::from_str("name: Alice\nage: 30\n").unwrap();
530        validate_tree(yaml.syntax()).expect("Tree should be valid");
531    }
532}
533
534/// Pretty-print a YAML document showing its structure and values.
535///
536/// This provides a human-readable view of the document structure,
537/// showing both the logical YAML structure and the actual values.
538///
539/// # Example
540///
541/// ```
542/// use yaml_edit::{Document, debug::PrettyDebug};
543/// use std::str::FromStr;
544///
545/// let doc = Document::from_str("name: Alice\nage: 30").unwrap();
546/// println!("{}", PrettyDebug(&doc));
547/// ```
548pub struct PrettyDebug<'a, T>(pub &'a T);
549
550impl<'a> fmt::Display for PrettyDebug<'a, Document> {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        writeln!(f, "Document {{")?;
553
554        if let Some(mapping) = self.0.as_mapping() {
555            write_indented(f, &format!("{}", PrettyDebug(&mapping)), 1)?;
556        } else if let Some(sequence) = self.0.as_sequence() {
557            write_indented(f, &format!("{}", PrettyDebug(&sequence)), 1)?;
558        } else if let Some(scalar) = self.0.as_scalar() {
559            writeln!(f, "  {}", PrettyDebug(&scalar))?;
560        }
561
562        writeln!(f, "}}")
563    }
564}
565
566impl<'a> fmt::Display for PrettyDebug<'a, Mapping> {
567    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568        writeln!(f, "Mapping [")?;
569
570        for (key, value) in self.0.iter() {
571            write!(f, "  {key:?}: ")?;
572            format_node(f, &value, 1)?;
573            writeln!(f)?;
574        }
575
576        write!(f, "]")
577    }
578}
579
580impl<'a> fmt::Display for PrettyDebug<'a, Sequence> {
581    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582        writeln!(f, "Sequence [")?;
583
584        for (i, value) in self.0.values().enumerate() {
585            write!(f, "  [{i}]: ")?;
586            format_node(f, &value, 1)?;
587            writeln!(f)?;
588        }
589
590        write!(f, "]")
591    }
592}
593
594impl<'a> fmt::Display for PrettyDebug<'a, Scalar> {
595    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596        write!(f, "Scalar({:?})", self.0.value())
597    }
598}
599
600fn write_indented(f: &mut fmt::Formatter<'_>, text: &str, indent: usize) -> fmt::Result {
601    let indent_str = "  ".repeat(indent);
602    for line in text.lines() {
603        writeln!(f, "{indent_str}{line}")?;
604    }
605    Ok(())
606}
607
608fn format_node(f: &mut fmt::Formatter<'_>, node: &YamlNode, indent: usize) -> fmt::Result {
609    let indent_str = "  ".repeat(indent);
610
611    match node {
612        YamlNode::Scalar(s) => {
613            let sv = crate::scalar::ScalarValue::from_scalar(s);
614            write!(f, "{:?} (type: {:?})", sv.value(), sv.scalar_type())?;
615        }
616        YamlNode::Mapping(m) => {
617            writeln!(f, "Mapping {{")?;
618            for (k, v) in m.iter() {
619                write!(f, "{indent_str}  ")?;
620                format_node(f, &k, 0)?;
621                write!(f, ": ")?;
622                format_node(f, &v, indent + 1)?;
623                writeln!(f)?;
624            }
625            write!(f, "{indent_str}}}")?;
626        }
627        YamlNode::Sequence(s) => {
628            writeln!(f, "Sequence [")?;
629            for (i, v) in s.values().enumerate() {
630                write!(f, "{indent_str}  [{i}]: ")?;
631                format_node(f, &v, indent + 1)?;
632                writeln!(f)?;
633            }
634            write!(f, "{indent_str}]")?;
635        }
636        YamlNode::Alias(a) => {
637            write!(f, "Alias(*{})", a.name())?;
638        }
639        YamlNode::TaggedNode(t) => {
640            write!(f, "Tagged({:?})", t.tag().unwrap_or_default())?;
641        }
642    }
643
644    Ok(())
645}
646
647/// Value inspector for deep type analysis.
648///
649/// Provides detailed information about a YAML value including:
650/// - Parsed type
651/// - Raw text representation
652/// - Coercion possibilities
653///
654/// # Example
655///
656/// ```
657/// use yaml_edit::{YamlFile, debug::ValueInspector};
658/// use std::str::FromStr;
659///
660/// let yaml = YamlFile::from_str("port: 8080").unwrap();
661/// let doc = yaml.document().unwrap();
662/// let mapping = doc.as_mapping().unwrap();
663/// let value = mapping.get("port").unwrap();
664///
665/// println!("{}", ValueInspector(&value));
666/// ```
667pub struct ValueInspector<'a>(pub &'a crate::as_yaml::YamlNode);
668
669impl<'a> fmt::Display for ValueInspector<'a> {
670    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671        writeln!(f, "Value Inspector:")?;
672        writeln!(f, "===============")?;
673
674        match self.0 {
675            crate::as_yaml::YamlNode::Scalar(s) => {
676                writeln!(f, "Type: Scalar")?;
677                writeln!(f, "  Value: {:?}", s.as_string())?;
678
679                writeln!(f, "\nCoercion Capabilities:")?;
680                writeln!(f, "  to_i64: {:?}", self.0.to_i64())?;
681                writeln!(f, "  to_f64: {:?}", self.0.to_f64())?;
682                writeln!(f, "  to_bool: {:?}", self.0.to_bool())?;
683            }
684            crate::as_yaml::YamlNode::Mapping(m) => {
685                writeln!(f, "Type: Mapping")?;
686                writeln!(f, "  Size: {} entries", m.len())?;
687            }
688            crate::as_yaml::YamlNode::Sequence(s) => {
689                writeln!(f, "Type: Sequence")?;
690                writeln!(f, "  Length: {} items", s.len())?;
691            }
692            crate::as_yaml::YamlNode::Alias(a) => {
693                writeln!(f, "Type: Alias")?;
694                writeln!(f, "  Anchor name: {}", a.name())?;
695            }
696            crate::as_yaml::YamlNode::TaggedNode(_) => {
697                writeln!(f, "Type: TaggedNode")?;
698            }
699        }
700
701        Ok(())
702    }
703}
704
705/// Visual diff between two YAML documents.
706///
707/// Shows additions, deletions, and modifications between two versions.
708///
709/// # Example
710///
711/// ```
712/// use yaml_edit::{Document, debug::VisualDiff};
713/// use std::str::FromStr;
714///
715/// let before = Document::from_str("name: Alice\nage: 30").unwrap();
716/// let after = Document::from_str("name: Bob\nage: 30").unwrap();
717///
718/// println!("{}", VisualDiff {
719///     before: &before,
720///     after: &after,
721/// });
722/// ```
723pub struct VisualDiff<'a> {
724    /// The original document
725    pub before: &'a Document,
726    /// The modified document
727    pub after: &'a Document,
728}
729
730impl<'a> fmt::Display for VisualDiff<'a> {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        writeln!(f, "Visual Diff:")?;
733        writeln!(f, "============")?;
734
735        writeln!(f, "\nBefore:")?;
736        writeln!(f, "-------")?;
737        for line in self.before.to_string().lines() {
738            writeln!(f, "  {line}")?;
739        }
740
741        writeln!(f, "\nAfter:")?;
742        writeln!(f, "------")?;
743        for line in self.after.to_string().lines() {
744            writeln!(f, "  {line}")?;
745        }
746
747        writeln!(f, "\nChanges:")?;
748        writeln!(f, "--------")?;
749
750        // Simple line-based diff
751        let before_str = self.before.to_string();
752        let after_str = self.after.to_string();
753        let before_lines: Vec<_> = before_str.lines().collect();
754        let after_lines: Vec<_> = after_str.lines().collect();
755
756        for (i, (b, a)) in before_lines.iter().zip(after_lines.iter()).enumerate() {
757            if b != a {
758                writeln!(f, "Line {}: {:?} -> {:?}", i + 1, b, a)?;
759            }
760        }
761
762        if before_lines.len() > after_lines.len() {
763            writeln!(
764                f,
765                "Removed {} lines",
766                before_lines.len() - after_lines.len()
767            )?;
768        } else if after_lines.len() > before_lines.len() {
769            writeln!(f, "Added {} lines", after_lines.len() - before_lines.len())?;
770        }
771
772        Ok(())
773    }
774}
775
776/// Generate GraphViz DOT format visualization of the CST.
777///
778/// This creates a visual graph representation suitable for rendering
779/// with GraphViz tools like `dot`.
780///
781/// # Example
782///
783/// ```
784/// use yaml_edit::{YamlFile, debug::graphviz_dot};
785/// use rowan::ast::AstNode;
786/// use std::str::FromStr;
787///
788/// let yaml = YamlFile::from_str("name: Alice").unwrap();
789/// let dot = graphviz_dot(yaml.syntax());
790/// // Save to file and render with: dot -Tpng output.dot -o output.png
791/// ```
792pub fn graphviz_dot(node: &SyntaxNode) -> String {
793    let mut result = String::from("digraph CST {\n");
794    result.push_str("  node [shape=box, fontname=\"Courier\"];\n");
795    result.push_str("  edge [fontsize=10];\n\n");
796
797    let mut counter = 0;
798    graphviz_node(&mut result, node, None, &mut counter);
799
800    result.push_str("}\n");
801    result
802}
803
804fn graphviz_node(
805    result: &mut String,
806    node: &SyntaxNode,
807    parent_id: Option<usize>,
808    counter: &mut usize,
809) {
810    let node_id = *counter;
811    *counter += 1;
812
813    // Create node
814    let label = format!("{:?}", node.kind());
815    result.push_str(&format!("  n{node_id} [label=\"{label}\"];\n"));
816
817    // Link to parent
818    if let Some(pid) = parent_id {
819        result.push_str(&format!("  n{pid} -> n{node_id};\n"));
820    }
821
822    // Process children
823    for child in node.children_with_tokens() {
824        match child {
825            rowan::NodeOrToken::Node(n) => {
826                graphviz_node(result, &n, Some(node_id), counter);
827            }
828            rowan::NodeOrToken::Token(t) => {
829                let token_id = *counter;
830                *counter += 1;
831
832                let text = t
833                    .text()
834                    .replace('\\', "\\\\")
835                    .replace('"', "\\\"")
836                    .replace('\n', "\\\\n")
837                    .replace('\r', "\\\\r");
838                let label = format!("{:?}\\n{:?}", t.kind(), text);
839                result.push_str(&format!(
840                    "  n{token_id} [label=\"{label}\", shape=ellipse, style=filled, fillcolor=lightgray];\n"
841                ));
842                result.push_str(&format!("  n{node_id} -> n{token_id};\n"));
843            }
844        }
845    }
846}
847
848#[cfg(test)]
849mod new_debug_tests {
850    use super::*;
851    use crate::yaml::YamlFile;
852    use rowan::ast::AstNode;
853    use std::str::FromStr;
854
855    #[test]
856    fn test_pretty_debug_document() {
857        let doc = Document::from_str("name: Alice\nage: 30").unwrap();
858        let output = format!("{}", PrettyDebug(&doc));
859
860        // Check structure exists (exact formatting may vary)
861        assert_eq!(output.lines().next(), Some("Document {"));
862        assert_eq!(output.lines().last(), Some("}"));
863    }
864
865    #[test]
866    fn test_value_inspector_scalar() {
867        let yaml = YamlFile::from_str("port: 8080").unwrap();
868        let doc = yaml.document().unwrap();
869        let mapping = doc.as_mapping().unwrap();
870        let value = mapping.get("port").unwrap();
871
872        let output = format!("{}", ValueInspector(&value));
873
874        assert_eq!(output.lines().next(), Some("Value Inspector:"));
875        assert_eq!(output.lines().nth(1), Some("==============="));
876        assert_eq!(output.lines().nth(2), Some("Type: Scalar"));
877    }
878
879    #[test]
880    fn test_visual_diff_simple() {
881        let before = Document::from_str("name: Alice").unwrap();
882        let after = Document::from_str("name: Bob").unwrap();
883
884        let diff = VisualDiff {
885            before: &before,
886            after: &after,
887        };
888
889        let output = format!("{}", diff);
890
891        assert_eq!(output.lines().next(), Some("Visual Diff:"));
892        assert_eq!(output.lines().nth(1), Some("============"));
893    }
894
895    #[test]
896    fn test_graphviz_dot_structure() {
897        let yaml = YamlFile::from_str("name: Alice").unwrap();
898        let dot = graphviz_dot(yaml.syntax());
899
900        assert_eq!(dot.lines().next(), Some("digraph CST {"));
901        assert_eq!(dot.lines().last(), Some("}"));
902    }
903
904    #[test]
905    fn test_graphviz_dot_contains_nodes() {
906        let yaml = YamlFile::from_str("key: value").unwrap();
907        let dot = graphviz_dot(yaml.syntax());
908
909        // Should have node declarations (at least one "node" line in the DOT output)
910        let node_count = dot.lines().filter(|l| l.trim().starts_with("node")).count();
911        assert!(
912            node_count >= 1,
913            "expected at least one node declaration in dot output"
914        );
915    }
916}