use std::fmt::Debug;
use std::io::Stdout;
use crate::Renderable;
use crate::cells::cell_len;
use crate::console::{Console, ConsoleOptions, JustifyMethod, OverflowMethod};
use crate::highlighter::{Highlighter, repr_highlighter, repr_highlighter_with_theme};
use crate::measure::Measurement;
use crate::segment::Segments;
use crate::text::Text;
use crate::theme::Theme;
#[derive(Debug, Clone)]
struct Node {
key_repr: String,
value_repr: String,
open_brace: String,
close_brace: String,
empty: String,
last: bool,
is_tuple: bool,
children: Option<Vec<Node>>,
key_separator: String,
separator: String,
}
impl Default for Node {
fn default() -> Self {
Node {
key_repr: String::new(),
value_repr: String::new(),
open_brace: String::new(),
close_brace: String::new(),
empty: String::new(),
last: false,
is_tuple: false,
children: None,
key_separator: ": ".to_string(),
separator: ", ".to_string(),
}
}
}
impl Node {
fn atomic(value: impl Into<String>) -> Self {
Node {
value_repr: value.into(),
..Default::default()
}
}
fn container(open: impl Into<String>, close: impl Into<String>) -> Self {
Node {
open_brace: open.into(),
close_brace: close.into(),
children: Some(Vec::new()),
..Default::default()
}
}
fn iter_tokens(&self) -> Vec<String> {
let mut tokens = Vec::new();
if !self.key_repr.is_empty() {
tokens.push(self.key_repr.clone());
tokens.push(self.key_separator.clone());
}
if !self.value_repr.is_empty() {
tokens.push(self.value_repr.clone());
} else if let Some(ref children) = self.children {
if children.is_empty() {
if !self.empty.is_empty() {
tokens.push(self.empty.clone());
} else {
tokens.push(self.open_brace.clone());
tokens.push(self.close_brace.clone());
}
} else {
tokens.push(self.open_brace.clone());
if self.is_tuple && children.len() == 1 {
tokens.extend(children[0].iter_tokens());
tokens.push(",".to_string());
} else {
for (i, child) in children.iter().enumerate() {
tokens.extend(child.iter_tokens());
if i < children.len() - 1 {
tokens.push(self.separator.clone());
}
}
}
tokens.push(self.close_brace.clone());
}
}
tokens
}
fn check_length(&self, start_length: usize, max_length: usize) -> bool {
let mut total = start_length;
for token in self.iter_tokens() {
total += cell_len(&token);
if total > max_length {
return false;
}
}
true
}
fn to_string_inline(&self) -> String {
self.iter_tokens().join("")
}
fn render(&self, max_width: usize, indent_size: usize, expand_all: bool) -> String {
let mut lines = vec![Line::new(self.clone(), true)];
let mut line_no = 0;
while line_no < lines.len() {
let line = &lines[line_no];
if line.expandable() && !line.expanded {
if expand_all || !line.check_length(max_width) {
let expanded = lines[line_no].expand(indent_size);
lines.splice(line_no..line_no + 1, expanded);
}
}
line_no += 1;
}
lines
.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Debug, Clone)]
struct Line {
node: Option<Node>,
text: String,
suffix: String,
whitespace: String,
expanded: bool,
last: bool,
#[allow(dead_code)]
is_root: bool,
}
impl Line {
fn new(node: Node, is_root: bool) -> Self {
Line {
node: Some(node),
text: String::new(),
suffix: String::new(),
whitespace: String::new(),
expanded: false,
last: false,
is_root,
}
}
fn text_only(text: impl Into<String>, whitespace: impl Into<String>) -> Self {
Line {
node: None,
text: text.into(),
suffix: String::new(),
whitespace: whitespace.into(),
expanded: false,
last: false,
is_root: false,
}
}
fn expandable(&self) -> bool {
if let Some(ref node) = self.node {
node.children.as_ref().is_some_and(|c| !c.is_empty())
} else {
false
}
}
fn check_length(&self, max_length: usize) -> bool {
let start_length = self.whitespace.len() + cell_len(&self.text) + cell_len(&self.suffix);
if let Some(ref node) = self.node {
node.check_length(start_length, max_length)
} else {
start_length <= max_length
}
}
fn expand(&self, indent_size: usize) -> Vec<Line> {
let mut result = Vec::new();
let node = match &self.node {
Some(n) => n,
None => return vec![self.clone()],
};
let children = match &node.children {
Some(c) => c,
None => return vec![self.clone()],
};
let open_text = if !node.key_repr.is_empty() {
format!("{}{}{}", node.key_repr, node.key_separator, node.open_brace)
} else {
node.open_brace.clone()
};
result.push(Line::text_only(open_text, &self.whitespace));
let child_whitespace = format!("{}{}", self.whitespace, " ".repeat(indent_size));
let tuple_of_one = node.is_tuple && children.len() == 1;
for (i, child) in children.iter().enumerate() {
let is_last = i == children.len() - 1;
let separator = if tuple_of_one {
","
} else if !is_last {
&node.separator
} else {
""
};
let mut line = Line::new(child.clone(), false);
line.whitespace = child_whitespace.clone();
line.suffix = separator.to_string();
line.last = is_last && !tuple_of_one;
result.push(line);
}
let mut close_line = Line::text_only(&node.close_brace, &self.whitespace);
close_line.suffix = self.suffix.clone();
close_line.last = self.last;
result.push(close_line);
result
}
}
impl std::fmt::Display for Line {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.last {
write!(
f,
"{}{}{}",
self.whitespace,
self.text,
self.node
.as_ref()
.map_or(String::new(), |n| n.to_string_inline())
)
} else {
write!(
f,
"{}{}{}{}",
self.whitespace,
self.text,
self.node
.as_ref()
.map_or(String::new(), |n| n.to_string_inline()),
self.suffix.trim_end()
)
}
}
}
fn parse_debug_output(
s: &str,
max_length: Option<usize>,
max_string: Option<usize>,
max_depth: Option<usize>,
) -> Node {
let mut parser = DebugParser::new(s, max_length, max_string, max_depth);
parser.parse()
}
struct DebugParser<'a> {
input: &'a str,
pos: usize,
max_length: Option<usize>,
max_string: Option<usize>,
max_depth: Option<usize>,
}
impl<'a> DebugParser<'a> {
fn new(
input: &'a str,
max_length: Option<usize>,
max_string: Option<usize>,
max_depth: Option<usize>,
) -> Self {
DebugParser {
input,
pos: 0,
max_length,
max_string,
max_depth,
}
}
fn peek(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn advance(&mut self) -> Option<char> {
let c = self.peek()?;
self.pos += c.len_utf8();
Some(c)
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.peek() {
if c.is_whitespace() {
self.advance();
} else {
break;
}
}
}
fn parse(&mut self) -> Node {
self.skip_whitespace();
self.parse_value(0)
}
fn parse_value(&mut self, depth: usize) -> Node {
self.skip_whitespace();
if let Some(max_d) = self.max_depth {
if depth >= max_d {
match self.peek() {
Some('[') => {
self.advance();
self.skip_to_closing_bracket(']');
}
Some('{') => {
self.advance();
self.skip_to_closing_bracket('}');
}
Some('(') => {
self.advance();
self.skip_to_closing_bracket(')');
}
Some('"') => {
self.parse_string();
return Node::atomic("...");
}
Some('\'') => {
self.parse_char();
return Node::atomic("...");
}
Some(c) if c.is_alphabetic() || c == '_' => {
self.skip_identifier_or_struct();
}
_ => {
self.parse_literal();
return Node::atomic("...");
}
}
return Node::atomic("...");
}
}
match self.peek() {
None => Node::atomic(""),
Some('[') => self.parse_list(depth),
Some('{') => self.parse_brace_container(depth),
Some('(') => self.parse_tuple(depth),
Some('"') => self.parse_string(),
Some('\'') => self.parse_char(),
Some(c) if c.is_alphabetic() || c == '_' => self.parse_identifier_or_struct(depth),
_ => self.parse_literal(),
}
}
fn parse_list(&mut self, depth: usize) -> Node {
self.advance(); let mut node = Node::container("[", "]");
node.empty = "[]".to_string();
self.skip_whitespace();
let mut count = 0;
while let Some(c) = self.peek() {
if c == ']' {
self.advance();
break;
}
if c == ',' {
self.advance();
self.skip_whitespace();
continue;
}
if let Some(max_len) = self.max_length {
if count >= max_len {
let remaining = self.skip_to_closing_bracket(']');
if remaining > 0 {
let mut ellipsis = Node::atomic(format!("... +{}", remaining));
ellipsis.last = true;
if let Some(ref mut children) = node.children {
children.push(ellipsis);
}
}
break;
}
}
let child = self.parse_value(depth + 1);
if let Some(ref mut children) = node.children {
children.push(child);
}
count += 1;
self.skip_whitespace();
}
if let Some(ref mut children) = node.children {
if let Some(last) = children.last_mut() {
last.last = true;
}
}
node
}
fn parse_brace_container(&mut self, depth: usize) -> Node {
self.advance(); let mut node = Node::container("{", "}");
node.empty = "{}".to_string();
self.skip_whitespace();
let mut count = 0;
while let Some(c) = self.peek() {
if c == '}' {
self.advance();
break;
}
if c == ',' {
self.advance();
self.skip_whitespace();
continue;
}
if let Some(max_len) = self.max_length {
if count >= max_len {
let remaining = self.skip_to_closing_bracket('}');
if remaining > 0 {
let mut ellipsis = Node::atomic(format!("... +{}", remaining));
ellipsis.last = true;
if let Some(ref mut children) = node.children {
children.push(ellipsis);
}
}
break;
}
}
let child = self.parse_key_value_or_value(depth + 1);
if let Some(ref mut children) = node.children {
children.push(child);
}
count += 1;
self.skip_whitespace();
}
if let Some(ref mut children) = node.children {
if let Some(last) = children.last_mut() {
last.last = true;
}
}
node
}
fn parse_tuple(&mut self, depth: usize) -> Node {
self.advance(); let mut node = Node::container("(", ")");
node.empty = "()".to_string();
node.is_tuple = true;
self.skip_whitespace();
let mut count = 0;
while let Some(c) = self.peek() {
if c == ')' {
self.advance();
break;
}
if c == ',' {
self.advance();
self.skip_whitespace();
continue;
}
if let Some(max_len) = self.max_length {
if count >= max_len {
let remaining = self.skip_to_closing_bracket(')');
if remaining > 0 {
let mut ellipsis = Node::atomic(format!("... +{}", remaining));
ellipsis.last = true;
if let Some(ref mut children) = node.children {
children.push(ellipsis);
}
}
break;
}
}
let child = self.parse_value(depth + 1);
if let Some(ref mut children) = node.children {
children.push(child);
}
count += 1;
self.skip_whitespace();
}
if let Some(ref mut children) = node.children {
if let Some(last) = children.last_mut() {
last.last = true;
}
}
node
}
fn parse_string(&mut self) -> Node {
let start = self.pos;
self.advance();
let mut escaped = false;
while let Some(c) = self.peek() {
if escaped {
escaped = false;
self.advance();
} else if c == '\\' {
escaped = true;
self.advance();
} else if c == '"' {
self.advance();
break;
} else {
self.advance();
}
}
let mut s = self.input[start..self.pos].to_string();
if let Some(max_str) = self.max_string {
let chars: Vec<char> = s.chars().collect();
if chars.len() > 2 {
let content_chars: Vec<char> = chars[1..chars.len() - 1].to_vec();
if content_chars.len() > max_str {
let truncated: String = content_chars[..max_str].iter().collect();
let truncated_len = content_chars.len() - max_str;
s = format!("\"{}\"...+{}", truncated, truncated_len);
}
}
}
Node::atomic(double_to_python_quotes(&s))
}
fn parse_char(&mut self) -> Node {
let start = self.pos;
self.advance();
let mut escaped = false;
while let Some(c) = self.peek() {
if escaped {
escaped = false;
self.advance();
} else if c == '\\' {
escaped = true;
self.advance();
} else if c == '\'' {
self.advance();
break;
} else {
self.advance();
}
}
Node::atomic(&self.input[start..self.pos])
}
fn parse_identifier_or_struct(&mut self, depth: usize) -> Node {
let ident = self.parse_identifier();
self.skip_whitespace();
match self.peek() {
Some('(') => {
self.advance(); self.skip_whitespace();
if self.peek() == Some(')') {
self.advance();
return Node::atomic(format!("{}()", ident));
}
let mut node = Node::container(format!("{}(", ident), ")");
node.empty = format!("{}()", ident);
let mut count = 0;
while let Some(c) = self.peek() {
if c == ')' {
self.advance();
break;
}
if c == ',' {
self.advance();
self.skip_whitespace();
continue;
}
if let Some(max_len) = self.max_length {
if count >= max_len {
let remaining = self.skip_to_closing_bracket(')');
if remaining > 0 {
let mut ellipsis = Node::atomic(format!("... +{}", remaining));
ellipsis.last = true;
if let Some(ref mut children) = node.children {
children.push(ellipsis);
}
}
break;
}
}
let child = self.parse_struct_field(depth + 1);
if let Some(ref mut children) = node.children {
children.push(child);
}
count += 1;
self.skip_whitespace();
}
if let Some(ref mut children) = node.children {
if let Some(last) = children.last_mut() {
last.last = true;
}
}
node
}
Some('{') => {
self.advance(); self.skip_whitespace();
if self.peek() == Some('}') {
self.advance();
return Node::atomic(format!("{} {{}}", ident));
}
let mut node = Node::container(format!("{} {{ ", ident), " }");
node.empty = format!("{} {{}}", ident);
let mut count = 0;
while let Some(c) = self.peek() {
if c == '}' {
self.advance();
break;
}
if c == ',' {
self.advance();
self.skip_whitespace();
continue;
}
if let Some(max_len) = self.max_length {
if count >= max_len {
let remaining = self.skip_to_closing_bracket('}');
if remaining > 0 {
let mut ellipsis = Node::atomic(format!("... +{}", remaining));
ellipsis.last = true;
if let Some(ref mut children) = node.children {
children.push(ellipsis);
}
}
break;
}
}
let child = self.parse_struct_field(depth + 1);
if let Some(ref mut children) = node.children {
children.push(child);
}
count += 1;
self.skip_whitespace();
}
if let Some(ref mut children) = node.children {
if let Some(last) = children.last_mut() {
last.last = true;
}
}
node
}
_ => Node::atomic(ident),
}
}
fn parse_struct_field(&mut self, depth: usize) -> Node {
self.skip_whitespace();
let start = self.pos;
match self.peek() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return self.parse_value(depth),
}
let mut field_end = self.pos;
while let Some(c) = self.peek() {
if c.is_alphanumeric() || c == '_' {
self.advance();
field_end = self.pos;
} else {
break;
}
}
self.skip_whitespace();
if self.peek() == Some(':') && !self.input[self.pos..].starts_with("::") {
let field_name = self.input[start..field_end].to_string();
self.advance(); self.skip_whitespace();
let mut value_node = self.parse_value(depth);
value_node.key_repr = field_name;
value_node.key_separator = ": ".to_string();
value_node
} else {
self.pos = start;
self.parse_value(depth)
}
}
fn parse_key_value_or_value(&mut self, depth: usize) -> Node {
self.skip_whitespace();
let start = self.pos;
let key_node = self.parse_value(depth);
self.skip_whitespace();
if self.peek() == Some(':') {
self.advance();
self.skip_whitespace();
let mut value_node = self.parse_value(depth);
value_node.key_repr = key_node.to_string_inline();
value_node.key_separator = ": ".to_string();
value_node
} else {
self.pos = start;
self.parse_value(depth)
}
}
fn parse_identifier(&mut self) -> String {
let start = self.pos;
while let Some(c) = self.peek() {
if c.is_alphanumeric() || c == '_' || c == ':' {
self.advance();
} else {
break;
}
}
self.input[start..self.pos].to_string()
}
fn parse_literal(&mut self) -> Node {
let start = self.pos;
while let Some(c) = self.peek() {
if c == ',' || c == ']' || c == '}' || c == ')' || c.is_whitespace() {
break;
}
self.advance();
}
Node::atomic(&self.input[start..self.pos])
}
fn skip_to_closing_bracket(&mut self, closing: char) -> usize {
let mut depth = 1;
let mut count = 0;
let mut in_string = false;
let mut escaped = false;
while let Some(c) = self.peek() {
if in_string {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_string = false;
}
self.advance();
continue;
}
if c == '"' {
in_string = true;
self.advance();
continue;
}
match c {
'[' | '{' | '(' => depth += 1,
']' | '}' | ')' => {
depth -= 1;
if depth == 0 && c == closing {
self.advance();
return count;
}
}
',' if depth == 1 => count += 1,
_ => {}
}
self.advance();
}
count
}
fn skip_identifier_or_struct(&mut self) {
loop {
while let Some(c) = self.peek() {
if c.is_alphanumeric() || c == '_' {
self.advance();
} else {
break;
}
}
if self.input[self.pos..].starts_with("::") {
self.advance(); self.advance(); continue; }
break;
}
self.skip_whitespace();
match self.peek() {
Some('(') => {
self.advance();
self.skip_to_closing_bracket(')');
}
Some('{') => {
self.advance();
self.skip_to_closing_bracket('}');
}
_ => {}
}
}
}
pub struct Pretty {
debug_str: String,
highlighter: Box<dyn Highlighter>,
indent_size: usize,
#[allow(dead_code)]
justify: Option<JustifyMethod>,
#[allow(dead_code)]
overflow: Option<OverflowMethod>,
#[allow(dead_code)]
no_wrap: bool,
indent_guides: bool,
max_length: Option<usize>,
max_string: Option<usize>,
max_depth: Option<usize>,
expand_all: bool,
margin: usize,
insert_line: bool,
explicit_theme: bool,
}
impl std::fmt::Debug for Pretty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pretty")
.field("debug_str", &self.debug_str)
.field("indent_size", &self.indent_size)
.field("justify", &self.justify)
.field("overflow", &self.overflow)
.field("no_wrap", &self.no_wrap)
.field("indent_guides", &self.indent_guides)
.field("max_length", &self.max_length)
.field("max_string", &self.max_string)
.field("max_depth", &self.max_depth)
.field("expand_all", &self.expand_all)
.field("margin", &self.margin)
.field("insert_line", &self.insert_line)
.field("explicit_theme", &self.explicit_theme)
.finish_non_exhaustive()
}
}
impl Pretty {
pub fn new<T: Debug>(value: &T) -> Self {
Pretty {
debug_str: format!("{:?}", value),
highlighter: Box::new(repr_highlighter()),
indent_size: 4,
justify: None,
overflow: None,
no_wrap: false,
indent_guides: false,
max_length: None,
max_string: None,
max_depth: None,
expand_all: false,
margin: 0,
insert_line: false,
explicit_theme: false,
}
}
pub fn from_str(debug_str: impl Into<String>) -> Self {
Pretty {
debug_str: debug_str.into(),
highlighter: Box::new(repr_highlighter()),
indent_size: 4,
justify: None,
overflow: None,
no_wrap: false,
indent_guides: false,
max_length: None,
max_string: None,
max_depth: None,
expand_all: false,
margin: 0,
insert_line: false,
explicit_theme: false,
}
}
pub fn with_highlighter(mut self, highlighter: impl Highlighter + 'static) -> Self {
self.highlighter = Box::new(highlighter);
self
}
pub fn with_indent_size(mut self, size: usize) -> Self {
self.indent_size = size;
self
}
pub fn with_justify(mut self, justify: JustifyMethod) -> Self {
self.justify = Some(justify);
self
}
pub fn with_overflow(mut self, overflow: OverflowMethod) -> Self {
self.overflow = Some(overflow);
self
}
pub fn with_no_wrap(mut self, no_wrap: bool) -> Self {
self.no_wrap = no_wrap;
self
}
pub fn with_indent_guides(mut self, guides: bool) -> Self {
self.indent_guides = guides;
self
}
pub fn with_theme(mut self, name: &str) -> Self {
if let Some(theme) = Theme::from_name(name) {
self.highlighter = Box::new(repr_highlighter_with_theme(theme));
self.explicit_theme = true;
}
self
}
pub fn with_custom_theme(mut self, theme: Theme) -> Self {
self.highlighter = Box::new(repr_highlighter_with_theme(theme));
self.explicit_theme = true;
self
}
pub fn with_max_length(mut self, max_length: Option<usize>) -> Self {
self.max_length = max_length;
self
}
pub fn with_max_string(mut self, max_string: Option<usize>) -> Self {
self.max_string = max_string;
self
}
pub fn with_max_depth(mut self, max_depth: Option<usize>) -> Self {
self.max_depth = max_depth;
self
}
pub fn with_expand_all(mut self, expand_all: bool) -> Self {
self.expand_all = expand_all;
self
}
pub fn with_margin(mut self, margin: usize) -> Self {
self.margin = margin;
self
}
pub fn with_insert_line(mut self, insert_line: bool) -> Self {
self.insert_line = insert_line;
self
}
pub fn debug_str(&self) -> &str {
&self.debug_str
}
}
impl Renderable for Pretty {
fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
let max_width = options.max_width.saturating_sub(self.margin);
let pretty_str = pretty_repr(
&self.debug_str,
max_width,
self.indent_size,
self.max_length,
self.max_string,
self.max_depth,
self.expand_all,
);
if pretty_str.is_empty() {
let dim_text = Text::styled("<empty repr>", crate::style::Style::new().with_dim(true));
return dim_text.render(console, options);
}
let processed_str: String = if self.indent_guides {
let mut result_lines = Vec::new();
for line in pretty_str.lines() {
let leading_spaces: usize = line.chars().take_while(|c| *c == ' ').count();
let num_guides = leading_spaces / self.indent_size;
if num_guides > 0 {
let mut guide_prefix = String::new();
for _ in 0..num_guides {
guide_prefix.push('│');
for _ in 0..(self.indent_size - 1) {
guide_prefix.push(' ');
}
}
let remaining = &line[leading_spaces..];
result_lines.push(format!("{}{}", guide_prefix, remaining));
} else {
result_lines.push(line.to_string());
}
}
result_lines.join("\n")
} else {
pretty_str.clone()
};
let has_newlines = processed_str.contains('\n');
let mut text = Text::plain(&processed_str);
if !self.explicit_theme && options.theme_name != "default" {
if let Some(theme) = Theme::from_name(&options.theme_name) {
let console_highlighter = repr_highlighter_with_theme(theme);
console_highlighter.highlight(&mut text);
} else {
self.highlighter.highlight(&mut text);
}
} else {
self.highlighter.highlight(&mut text);
}
if self.indent_guides {
use crate::color::SimpleColor;
let guide_style = crate::style::Style::new()
.with_color(SimpleColor::Standard(2)) .with_dim(true);
let plain = text.plain_text().to_string();
for (idx, ch) in plain.char_indices() {
if ch == '│' {
let char_idx = plain[..idx].chars().count();
text.stylize(char_idx, char_idx + 1, guide_style);
}
}
}
let mut result = Segments::new();
if self.insert_line && has_newlines {
result.push(crate::segment::Segment::line());
}
let text_segments = text.render(console, options);
for seg in text_segments {
result.push(seg);
}
result
}
fn measure(&self, _console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
let max_width = options.max_width.saturating_sub(self.margin);
let pretty_str = pretty_repr(
&self.debug_str,
max_width,
self.indent_size,
self.max_length,
self.max_string,
self.max_depth,
self.expand_all,
);
let text_width = if pretty_str.is_empty() {
0
} else {
pretty_str.lines().map(cell_len).max().unwrap_or(0)
};
Measurement::new(text_width, text_width)
}
}
fn double_to_python_quotes(literal: &str) -> String {
let bytes = literal.as_bytes();
if bytes.len() < 2 || bytes[0] != b'"' || bytes[bytes.len() - 1] != b'"' {
return literal.to_string();
}
let inner = &literal[1..literal.len() - 1];
let mut raw = String::with_capacity(inner.len());
let mut has_single = false;
let mut has_double = false;
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('"') => {
raw.push('"');
has_double = true;
}
Some(n) => {
raw.push('\\');
raw.push(n);
}
None => raw.push('\\'),
}
} else {
if c == '\'' {
has_single = true;
} else if c == '"' {
has_double = true;
}
raw.push(c);
}
}
let quote = if has_single && !has_double { '"' } else { '\'' };
let mut out = String::with_capacity(raw.len() + 2);
out.push(quote);
for c in raw.chars() {
if c == quote {
out.push('\\');
}
out.push(c);
}
out.push(quote);
out
}
pub fn pretty_repr(
debug_str: &str,
max_width: usize,
indent_size: usize,
max_length: Option<usize>,
max_string: Option<usize>,
max_depth: Option<usize>,
expand_all: bool,
) -> String {
let node = parse_debug_output(debug_str, max_length, max_string, max_depth);
node.render(max_width, indent_size, expand_all)
}
pub fn pprint<T: Debug>(
value: &T,
console: Option<&mut Console>,
indent_guides: bool,
max_length: Option<usize>,
max_string: Option<usize>,
max_depth: Option<usize>,
expand_all: bool,
) {
let pretty = Pretty::new(value)
.with_indent_guides(indent_guides)
.with_max_length(max_length)
.with_max_string(max_string)
.with_max_depth(max_depth)
.with_expand_all(expand_all)
.with_overflow(OverflowMethod::Ignore);
if let Some(console) = console {
let _ = console.print(&pretty, None, None, None, false, "\n");
} else {
let mut default_console = Console::new();
let _ = default_console.print(&pretty, None, None, None, false, "\n");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_atomic() {
let node = Node::atomic("42");
assert_eq!(node.to_string_inline(), "42");
}
#[test]
fn test_node_container_empty() {
let mut node = Node::container("[", "]");
node.empty = "[]".to_string();
assert_eq!(node.to_string_inline(), "[]");
}
#[test]
fn test_node_container_with_children() {
let mut node = Node::container("[", "]");
if let Some(ref mut children) = node.children {
children.push(Node::atomic("1"));
children.push(Node::atomic("2"));
children.push(Node::atomic("3"));
}
assert_eq!(node.to_string_inline(), "[1, 2, 3]");
}
#[test]
fn test_node_tuple_single_element() {
let mut node = Node::container("(", ")");
node.is_tuple = true;
if let Some(ref mut children) = node.children {
children.push(Node::atomic("1"));
}
assert_eq!(node.to_string_inline(), "(1,)");
}
#[test]
fn test_node_with_key() {
let mut node = Node::atomic("42");
node.key_repr = "answer".to_string();
assert_eq!(node.to_string_inline(), "answer: 42");
}
#[test]
fn test_parse_simple_list() {
let node = parse_debug_output("[1, 2, 3]", None, None, None);
assert_eq!(node.to_string_inline(), "[1, 2, 3]");
}
#[test]
fn test_parse_empty_list() {
let node = parse_debug_output("[]", None, None, None);
assert_eq!(node.to_string_inline(), "[]");
}
#[test]
fn test_parse_nested_list() {
let node = parse_debug_output("[[1, 2], [3, 4]]", None, None, None);
assert_eq!(node.to_string_inline(), "[[1, 2], [3, 4]]");
}
#[test]
fn test_parse_tuple() {
let node = parse_debug_output("(1, 2, 3)", None, None, None);
assert_eq!(node.to_string_inline(), "(1, 2, 3)");
}
#[test]
fn test_parse_single_element_tuple() {
let node = parse_debug_output("(1,)", None, None, None);
assert_eq!(node.to_string_inline(), "(1,)");
}
#[test]
fn test_parse_struct() {
let node = parse_debug_output("Point { x: 1, y: 2 }", None, None, None);
let rendered = node.to_string_inline();
assert!(rendered.contains("Point"));
assert!(rendered.contains("x"));
assert!(rendered.contains("y"));
}
#[test]
fn test_parse_string() {
let node = parse_debug_output("\"hello\"", None, None, None);
assert_eq!(node.to_string_inline(), "'hello'");
}
#[test]
fn test_parse_escaped_string() {
let node = parse_debug_output("\"hello\\nworld\"", None, None, None);
assert_eq!(node.to_string_inline(), "'hello\\nworld'");
}
#[test]
fn test_string_quote_selection() {
let node = parse_debug_output("\"it's\"", None, None, None);
assert_eq!(node.to_string_inline(), "\"it's\"");
let node = parse_debug_output("\"say \\\"hi\\\"\"", None, None, None);
assert_eq!(node.to_string_inline(), "'say \"hi\"'");
}
#[test]
fn test_parse_max_length() {
let node = parse_debug_output("[1, 2, 3, 4, 5]", Some(3), None, None);
let rendered = node.to_string_inline();
assert!(rendered.contains("..."));
}
#[test]
fn test_parse_max_depth() {
let node = parse_debug_output("[[1, 2], [3, 4]]", None, None, Some(1));
let rendered = node.to_string_inline();
assert!(rendered.contains("..."));
}
#[test]
fn test_pretty_new() {
let data = vec![1, 2, 3];
let pretty = Pretty::new(&data);
assert!(pretty.debug_str().contains("[1, 2, 3]"));
}
#[test]
fn test_pretty_with_indent_size() {
let data = vec![1, 2, 3];
let pretty = Pretty::new(&data).with_indent_size(2);
assert_eq!(pretty.indent_size, 2);
}
#[test]
fn test_pretty_with_max_length() {
let data = vec![1, 2, 3, 4, 5];
let pretty = Pretty::new(&data).with_max_length(Some(3));
assert_eq!(pretty.max_length, Some(3));
}
#[test]
fn test_pretty_with_expand_all() {
let data = vec![1, 2, 3];
let pretty = Pretty::new(&data).with_expand_all(true);
assert!(pretty.expand_all);
}
#[test]
fn test_pretty_repr_fits_single_line() {
let result = pretty_repr("[1, 2, 3]", 80, 4, None, None, None, false);
assert_eq!(result, "[1, 2, 3]");
}
#[test]
fn test_pretty_repr_expands_when_too_wide() {
let result = pretty_repr("[1, 2, 3, 4, 5]", 10, 4, None, None, None, false);
assert!(result.contains('\n'));
}
#[test]
fn test_pretty_repr_expand_all() {
let result = pretty_repr("[1, 2, 3]", 80, 4, None, None, None, true);
assert!(result.contains('\n'));
}
#[test]
fn test_pretty_repr_nested() {
let result = pretty_repr("[[1, 2], [3, 4]]", 15, 4, None, None, None, false);
assert!(result.contains('\n'));
}
#[test]
fn test_pretty_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Pretty>();
assert_sync::<Pretty>();
}
#[test]
fn test_pretty_render() {
let data = vec![1, 2, 3];
let pretty = Pretty::new(&data);
let console = Console::with_options(ConsoleOptions {
max_width: 80,
..Default::default()
});
let options = console.options().clone();
let segments = pretty.render(&console, &options);
assert!(!segments.is_empty());
}
#[test]
fn test_pretty_measure() {
let data = vec![1, 2, 3];
let pretty = Pretty::new(&data);
let console = Console::new();
let options = ConsoleOptions::default();
let measurement = pretty.measure(&console, &options);
assert!(measurement.minimum > 0);
assert!(measurement.maximum >= measurement.minimum);
}
#[test]
fn test_pretty_debug() {
let data = vec![1, 2, 3];
let pretty = Pretty::new(&data);
let debug_str = format!("{:?}", pretty);
assert!(debug_str.contains("Pretty"));
}
#[test]
fn test_pretty_hashmap() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
let pretty = Pretty::new(&map);
assert!(pretty.debug_str().contains("a"));
assert!(pretty.debug_str().contains("b"));
}
#[test]
fn test_pretty_option() {
let some_value: Option<i32> = Some(42);
let none_value: Option<i32> = None;
let pretty_some = Pretty::new(&some_value);
let pretty_none = Pretty::new(&none_value);
assert!(pretty_some.debug_str().contains("Some"));
assert!(pretty_some.debug_str().contains("42"));
assert!(pretty_none.debug_str().contains("None"));
}
#[test]
fn test_pretty_result() {
let ok_value: Result<i32, &str> = Ok(42);
let err_value: Result<i32, &str> = Err("error");
let pretty_ok = Pretty::new(&ok_value);
let pretty_err = Pretty::new(&err_value);
assert!(pretty_ok.debug_str().contains("Ok"));
assert!(pretty_err.debug_str().contains("Err"));
}
#[derive(Debug)]
#[allow(dead_code)]
struct TestStruct {
name: String,
value: i32,
items: Vec<i32>,
}
#[test]
fn test_pretty_custom_struct() {
let s = TestStruct {
name: "test".to_string(),
value: 42,
items: vec![1, 2, 3],
};
let pretty = Pretty::new(&s);
assert!(pretty.debug_str().contains("TestStruct"));
assert!(pretty.debug_str().contains("name"));
assert!(pretty.debug_str().contains("value"));
assert!(pretty.debug_str().contains("items"));
}
#[test]
fn test_pretty_deeply_nested() {
let data = vec![vec![vec![1, 2], vec![3, 4]], vec![vec![5, 6], vec![7, 8]]];
let result = pretty_repr(&format!("{:?}", data), 20, 4, None, None, None, false);
assert!(result.contains('\n'));
}
}