Skip to main content

ptx_parser/pretty_print/
tree_formatter.rs

1use super::TreeDisplay;
2use crate::Span;
3/// Helper struct for formatting tree structures with box-drawing characters.
4///
5/// Manages indentation, prefixes, and provides utility methods for common
6/// formatting patterns like displaying fields, vectors, and options.
7use std::fmt::{self, Write};
8
9pub struct TreeFormatter {
10    buffer: String,
11    indent_stack: Vec<bool>, // true = continue line (│), false = last item (space)
12}
13
14impl TreeFormatter {
15    /// Create a new TreeFormatter.
16    pub fn new() -> Self {
17        TreeFormatter {
18            buffer: String::new(),
19            indent_stack: Vec::new(),
20        }
21    }
22
23    /// Get the formatted output.
24    pub fn finish(self) -> String {
25        self.buffer
26    }
27
28    /// Write a line with proper indentation and prefix.
29    fn write_line(&mut self, is_last: bool, content: &str) -> fmt::Result {
30        // Write indentation
31        for &continues in &self.indent_stack {
32            if continues {
33                write!(self.buffer, "│   ")?;
34            } else {
35                write!(self.buffer, "    ")?;
36            }
37        }
38
39        // Write prefix
40        if is_last {
41            write!(self.buffer, "└── ")?;
42        } else {
43            write!(self.buffer, "├── ")?;
44        }
45
46        // Write content
47        writeln!(self.buffer, "{}", content)?;
48        Ok(())
49    }
50
51    /// Display a root node (no indentation prefix).
52    /// Only use this for the very top-level node.
53    pub fn root(&mut self, content: &str) -> fmt::Result {
54        if self.indent_stack.is_empty() {
55            // Top level - no tree prefix
56            writeln!(self.buffer, "{}", content)?;
57        } else {
58            // Nested node - write with tree structure
59            self.node(content)?;
60        }
61        Ok(())
62    }
63
64    /// Display a node (with tree structure based on current indent level).
65    fn node(&mut self, content: &str) -> fmt::Result {
66        // Write indentation
67        for &continues in &self.indent_stack {
68            if continues {
69                write!(self.buffer, "│   ")?;
70            } else {
71                write!(self.buffer, "    ")?;
72            }
73        }
74
75        // Write content without prefix (child nodes don't get ├── or └──)
76        writeln!(self.buffer, "{}", content)?;
77        Ok(())
78    }
79
80    /// Display a field with a name and value.
81    pub fn field(&mut self, is_last: bool, name: &str, value: &str) -> fmt::Result {
82        self.write_line(is_last, &format!("{}: {}", name, value))
83    }
84
85    /// Display a field with a child node that implements TreeDisplay.
86    pub fn field_with_child<T: TreeDisplay>(
87        &mut self,
88        is_last: bool,
89        name: &str,
90        value: &T,
91        source: &str,
92    ) -> fmt::Result {
93        self.write_line(is_last, name)?;
94        self.indent_stack.push(!is_last);
95        value.tree_display(self, source)?;
96        self.indent_stack.pop();
97        Ok(())
98    }
99
100    /// Display an optional field.
101    pub fn field_option<T: TreeDisplay>(
102        &mut self,
103        is_last: bool,
104        name: &str,
105        value: &Option<T>,
106        source: &str,
107    ) -> fmt::Result {
108        match value {
109            Some(v) => {
110                self.write_line(is_last, &format!("{}: Some", name))?;
111                self.indent_stack.push(!is_last);
112                v.tree_display(self, source)?;
113                self.indent_stack.pop();
114            }
115            None => {
116                self.write_line(is_last, &format!("{}: None", name))?;
117            }
118        }
119        Ok(())
120    }
121
122    /// Display a vector of items.
123    pub fn field_vec<T: TreeDisplay>(
124        &mut self,
125        is_last: bool,
126        name: &str,
127        items: &[T],
128        source: &str,
129    ) -> fmt::Result {
130        self.write_line(is_last, &format!("{}: Vec ({} items)", name, items.len()))?;
131        if !items.is_empty() {
132            self.indent_stack.push(!is_last);
133            for (i, item) in items.iter().enumerate() {
134                let is_last_item = i == items.len() - 1;
135                self.write_line(is_last_item, &format!("[{}]", i))?;
136                self.indent_stack.push(!is_last_item);
137                item.tree_display(self, source)?;
138                self.indent_stack.pop();
139            }
140            self.indent_stack.pop();
141        }
142        Ok(())
143    }
144
145    /// Display an array of items.
146    pub fn field_array<T: TreeDisplay, const N: usize>(
147        &mut self,
148        is_last: bool,
149        name: &str,
150        items: &[T; N],
151        source: &str,
152    ) -> fmt::Result {
153        self.field_vec(is_last, name, items, source)
154    }
155
156    /// Display a tuple of 2 items.
157    pub fn field_tuple2<T1: TreeDisplay, T2: TreeDisplay>(
158        &mut self,
159        is_last: bool,
160        name: &str,
161        value: &(T1, T2),
162        source: &str,
163    ) -> fmt::Result {
164        self.write_line(is_last, &format!("{}: (2 items)", name))?;
165        self.indent_stack.push(!is_last);
166
167        self.write_line(false, "[0]")?;
168        self.indent_stack.push(true);
169        value.0.tree_display(self, source)?;
170        self.indent_stack.pop();
171
172        self.write_line(true, "[1]")?;
173        self.indent_stack.push(false);
174        value.1.tree_display(self, source)?;
175        self.indent_stack.pop();
176
177        self.indent_stack.pop();
178        Ok(())
179    }
180
181    /// Extract and format raw source text from a span.
182    ///
183    /// If the text is longer than 40 characters, it will be truncated to show
184    /// the first 20 and last 20 characters with "..." in the middle.
185    /// Newlines and other whitespace are normalized to single spaces.
186    pub fn format_raw(&self, span: Span, source: &str) -> String {
187        let raw = extract_span_text(span, source);
188        // Normalize whitespace: replace all whitespace sequences with a single space
189        let normalized = raw.split_whitespace().collect::<Vec<_>>().join(" ");
190        truncate_with_ellipsis(&normalized, 40)
191    }
192}
193
194impl Default for TreeFormatter {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200/// Extract the text corresponding to a span from the source.
201fn extract_span_text(span: Span, source: &str) -> String {
202    if span.start <= span.end && span.end <= source.len() {
203        source[span.start..span.end].to_string()
204    } else {
205        format!("<invalid span: {}..{}>", span.start, span.end)
206    }
207}
208
209/// Truncate a string to max_len, showing head and tail with "..." in middle.
210///
211/// If the string is longer than max_len, shows first max_len/2 characters,
212/// "...", then last max_len/2 characters.
213pub(crate) fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
214    if s.len() <= max_len {
215        s.to_string()
216    } else {
217        let half = max_len / 2;
218        let start = &s[..half.min(s.len())];
219        let end_start = s.len().saturating_sub(half);
220        let end = &s[end_start..];
221        format!("{}...{}", start, end)
222    }
223}