Skip to main content

palladium/errors/
pretty.rs

1// Pretty error formatting for Palladium
2// "Making errors beautiful and informative"
3
4use super::{Diagnostic, DiagnosticLevel};
5use std::fmt::Write;
6
7/// ANSI color codes for terminal output
8pub mod colors {
9    pub const RESET: &str = "\x1b[0m";
10    pub const BOLD: &str = "\x1b[1m";
11    pub const DIM: &str = "\x1b[2m";
12    pub const UNDERLINE: &str = "\x1b[4m";
13
14    // Foreground colors
15    pub const RED: &str = "\x1b[31m";
16    pub const GREEN: &str = "\x1b[32m";
17    pub const YELLOW: &str = "\x1b[33m";
18    pub const BLUE: &str = "\x1b[34m";
19    pub const MAGENTA: &str = "\x1b[35m";
20    pub const CYAN: &str = "\x1b[36m";
21    pub const WHITE: &str = "\x1b[37m";
22
23    // Bright colors
24    pub const BRIGHT_RED: &str = "\x1b[91m";
25    pub const BRIGHT_GREEN: &str = "\x1b[92m";
26    pub const BRIGHT_YELLOW: &str = "\x1b[93m";
27    pub const BRIGHT_BLUE: &str = "\x1b[94m";
28    pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
29    pub const BRIGHT_CYAN: &str = "\x1b[96m";
30}
31
32/// Style configuration for error output
33pub struct ErrorStyle {
34    pub use_color: bool,
35    pub use_unicode: bool,
36    pub compact: bool,
37}
38
39impl Default for ErrorStyle {
40    fn default() -> Self {
41        Self {
42            use_color: true,
43            use_unicode: true,
44            compact: false,
45        }
46    }
47}
48
49impl ErrorStyle {
50    /// Get the appropriate error level styling
51    pub fn level_style(&self, level: DiagnosticLevel) -> (&'static str, String, &'static str) {
52        if !self.use_color {
53            return match level {
54                DiagnosticLevel::Error => ("error", String::new(), ""),
55                DiagnosticLevel::Warning => ("warning", String::new(), ""),
56                DiagnosticLevel::Info => ("info", String::new(), ""),
57                DiagnosticLevel::Help => ("help", String::new(), ""),
58            };
59        }
60
61        match level {
62            DiagnosticLevel::Error => (
63                "error",
64                format!("{}{}", colors::BOLD, colors::BRIGHT_RED),
65                colors::RESET,
66            ),
67            DiagnosticLevel::Warning => (
68                "warning",
69                format!("{}{}", colors::BOLD, colors::BRIGHT_YELLOW),
70                colors::RESET,
71            ),
72            DiagnosticLevel::Info => (
73                "info",
74                format!("{}{}", colors::BOLD, colors::BRIGHT_CYAN),
75                colors::RESET,
76            ),
77            DiagnosticLevel::Help => (
78                "help",
79                format!("{}{}", colors::BOLD, colors::BRIGHT_GREEN),
80                colors::RESET,
81            ),
82        }
83    }
84
85    /// Get the style for file paths
86    pub fn path_style(&self) -> (String, &'static str) {
87        if self.use_color {
88            (format!("{}{}", colors::BOLD, colors::BLUE), colors::RESET)
89        } else {
90            (String::new(), "")
91        }
92    }
93
94    /// Get the style for line numbers
95    pub fn line_number_style(&self) -> (&'static str, &'static str) {
96        if self.use_color {
97            (colors::BLUE, colors::RESET)
98        } else {
99            ("", "")
100        }
101    }
102
103    /// Get the style for error underlining
104    pub fn error_style(&self) -> (String, &'static str) {
105        if self.use_color {
106            (
107                format!("{}{}", colors::BOLD, colors::BRIGHT_RED),
108                colors::RESET,
109            )
110        } else {
111            (String::new(), "")
112        }
113    }
114
115    /// Get the style for notes
116    pub fn note_style(&self) -> (String, &'static str) {
117        if self.use_color {
118            (
119                format!("{}{}", colors::BOLD, colors::BRIGHT_CYAN),
120                colors::RESET,
121            )
122        } else {
123            (String::new(), "")
124        }
125    }
126
127    /// Get the style for suggestions
128    pub fn suggestion_style(&self) -> (String, &'static str) {
129        if self.use_color {
130            (
131                format!("{}{}", colors::BOLD, colors::BRIGHT_GREEN),
132                colors::RESET,
133            )
134        } else {
135            (String::new(), "")
136        }
137    }
138
139    /// Get the style for dimmed context
140    pub fn dim_style(&self) -> (&'static str, &'static str) {
141        if self.use_color {
142            (colors::DIM, colors::RESET)
143        } else {
144            ("", "")
145        }
146    }
147
148    /// Get unicode or ASCII characters for drawing
149    pub fn get_chars(&self) -> DrawingChars {
150        if self.use_unicode {
151            DrawingChars::unicode()
152        } else {
153            DrawingChars::ascii()
154        }
155    }
156}
157
158/// Characters used for drawing error indicators
159pub struct DrawingChars {
160    pub vertical: &'static str,
161    pub horizontal: &'static str,
162    pub top_left: &'static str,
163    pub arrow: &'static str,
164    pub pointer_start: &'static str,
165    pub pointer_line: &'static str,
166}
167
168impl DrawingChars {
169    pub fn unicode() -> Self {
170        Self {
171            vertical: "│",
172            horizontal: "─",
173            top_left: "┌",
174            arrow: "→",
175            pointer_start: "^",
176            pointer_line: "─",
177        }
178    }
179
180    pub fn ascii() -> Self {
181        Self {
182            vertical: "|",
183            horizontal: "-",
184            top_left: "+",
185            arrow: "->",
186            pointer_start: "^",
187            pointer_line: "~",
188        }
189    }
190}
191
192/// Format a diagnostic message with pretty colors and formatting
193pub fn format_diagnostic(diagnostic: &Diagnostic, style: &ErrorStyle) -> String {
194    let mut output = String::new();
195
196    // Format the main error message
197    let (level_text, level_start, level_end) = style.level_style(diagnostic.level);
198    write!(
199        &mut output,
200        "{}{}{}: {}{}{}",
201        level_start,
202        level_text,
203        level_end,
204        colors::BOLD,
205        diagnostic.message,
206        colors::RESET
207    )
208    .unwrap();
209
210    output
211}
212
213/// Create a fancy box around important messages
214pub fn boxed_message(title: &str, content: &str, style: &ErrorStyle) -> String {
215    let chars = style.get_chars();
216    let width = content
217        .lines()
218        .map(|line| line.len())
219        .max()
220        .unwrap_or(0)
221        .max(title.len() + 4);
222
223    let mut output = String::new();
224
225    // Top border
226    writeln!(
227        &mut output,
228        "{}{}",
229        chars.top_left,
230        chars.horizontal.repeat(width + 2)
231    )
232    .unwrap();
233
234    // Title
235    if !title.is_empty() {
236        let (suggestion_start, suggestion_end) = style.suggestion_style();
237        writeln!(
238            &mut output,
239            "{} {} {}{}",
240            chars.vertical, suggestion_start, title, suggestion_end
241        )
242        .unwrap();
243
244        // Separator
245        writeln!(
246            &mut output,
247            "{}{}",
248            chars.vertical,
249            chars.horizontal.repeat(width + 2)
250        )
251        .unwrap();
252    }
253
254    // Content
255    for line in content.lines() {
256        writeln!(
257            &mut output,
258            "{} {:<width$} {}",
259            chars.vertical,
260            line,
261            chars.vertical,
262            width = width
263        )
264        .unwrap();
265    }
266
267    // Bottom border
268    writeln!(
269        &mut output,
270        "{}{}",
271        chars.top_left,
272        chars.horizontal.repeat(width + 2)
273    )
274    .unwrap();
275
276    output
277}