ptx_parser/pretty_print/
tree_formatter.rs

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