Skip to main content

boxmux_lib/components/
error_display.rs

1use crate::components::ComponentDimensions;
2use crate::draw_utils::print_with_color_and_background_at;
3use crate::model::common::{Bounds, ScreenBuffer};
4
5/// Error severity levels for display styling
6#[derive(Debug, Clone, PartialEq)]
7pub enum ErrorSeverity {
8    /// Critical error that stops execution
9    Error,
10    /// Warning that should be addressed
11    Warning,
12    /// Information for debugging
13    Info,
14    /// Hint for user guidance
15    Hint,
16}
17
18/// Syntax highlighting token types for error context
19#[derive(Debug, Clone, PartialEq)]
20pub enum SyntaxToken {
21    /// Language keywords (fn, let, impl, etc.)
22    Keyword,
23    /// String literals
24    String,
25    /// Numeric literals
26    Number,
27    /// Comments
28    Comment,
29    /// Function and method names
30    Function,
31    /// Type names
32    Type,
33    /// Variable names
34    Variable,
35    /// Operators (+, -, =, etc.)
36    Operator,
37    /// Punctuation and delimiters
38    Punctuation,
39    /// Regular text
40    Text,
41}
42
43/// Syntax highlighting configuration
44#[derive(Debug, Clone)]
45pub struct SyntaxHighlightConfig {
46    /// Enable syntax highlighting
47    pub enabled: bool,
48    /// Color for keywords
49    pub keyword_color: String,
50    /// Color for string literals
51    pub string_color: String,
52    /// Color for numeric literals
53    pub number_color: String,
54    /// Color for comments
55    pub comment_color: String,
56    /// Color for function names
57    pub function_color: String,
58    /// Color for type names
59    pub type_color: String,
60    /// Color for variable names
61    pub variable_color: String,
62    /// Color for operators
63    pub operator_color: String,
64    /// Color for punctuation
65    pub punctuation_color: String,
66    /// Language for syntax highlighting (rust, yaml, json, etc.)
67    pub language: String,
68}
69
70impl Default for SyntaxHighlightConfig {
71    fn default() -> Self {
72        Self {
73            enabled: true,
74            keyword_color: "magenta".to_string(),
75            string_color: "green".to_string(),
76            number_color: "yellow".to_string(),
77            comment_color: "bright_black".to_string(),
78            function_color: "blue".to_string(),
79            type_color: "cyan".to_string(),
80            variable_color: "white".to_string(),
81            operator_color: "red".to_string(),
82            punctuation_color: "bright_black".to_string(),
83            language: "rust".to_string(),
84        }
85    }
86}
87
88/// Error display style configuration
89#[derive(Debug, Clone)]
90pub struct ErrorDisplayConfig {
91    /// Color for error severity indicators
92    pub error_color: String,
93    /// Color for warning severity indicators
94    pub warning_color: String,
95    /// Color for info severity indicators
96    pub info_color: String,
97    /// Color for hint severity indicators
98    pub hint_color: String,
99    /// Color for line numbers
100    pub line_number_color: String,
101    /// Color for caret indicators (^^^)
102    pub caret_color: String,
103    /// Color for file path information
104    pub file_path_color: String,
105    /// Color for pipe characters and borders
106    pub border_color: String,
107    /// Background color for error display
108    pub background_color: String,
109    /// Whether to show context lines around error
110    pub show_context: bool,
111    /// Number of context lines to show before and after error
112    pub context_lines: usize,
113    /// Character to use for caret indicators
114    pub caret_char: char,
115    /// Character to use for line continuation
116    pub continuation_char: char,
117    /// Syntax highlighting configuration
118    pub syntax_highlighting: SyntaxHighlightConfig,
119}
120
121impl Default for ErrorDisplayConfig {
122    fn default() -> Self {
123        Self {
124            error_color: "bright_red".to_string(),
125            warning_color: "bright_yellow".to_string(),
126            info_color: "bright_blue".to_string(),
127            hint_color: "bright_green".to_string(),
128            line_number_color: "bright_blue".to_string(),
129            caret_color: "bright_red".to_string(),
130            file_path_color: "bright_blue".to_string(),
131            border_color: "bright_blue".to_string(),
132            background_color: "black".to_string(),
133            show_context: true,
134            context_lines: 2,
135            caret_char: '^',
136            continuation_char: '.',
137            syntax_highlighting: SyntaxHighlightConfig::default(),
138        }
139    }
140}
141
142impl ErrorDisplayConfig {
143    /// Create config optimized for terminal display
144    pub fn terminal() -> Self {
145        Self {
146            show_context: false,
147            context_lines: 1,
148            ..Default::default()
149        }
150    }
151
152    /// Create config optimized for detailed debugging
153    pub fn detailed() -> Self {
154        Self {
155            show_context: true,
156            context_lines: 3,
157            ..Default::default()
158        }
159    }
160
161    /// Create config with custom colors
162    pub fn with_colors(error_color: String, warning_color: String, info_color: String) -> Self {
163        Self {
164            error_color,
165            warning_color,
166            info_color,
167            ..Default::default()
168        }
169    }
170}
171
172/// Error information for display
173#[derive(Debug, Clone)]
174pub struct ErrorInfo {
175    /// Error message
176    pub message: String,
177    /// File path where error occurred
178    pub file_path: String,
179    /// Line number (1-based)
180    pub line_number: usize,
181    /// Column number (1-based)
182    pub column_number: usize,
183    /// Error severity level
184    pub severity: ErrorSeverity,
185    /// Optional help text
186    pub help: Option<String>,
187    /// Optional note text
188    pub note: Option<String>,
189    /// Enhanced caret positioning configuration
190    pub caret_positioning: Option<CaretPositioning>,
191}
192
193/// Multi-line error span for precise highlighting
194#[derive(Debug, Clone)]
195pub struct ErrorSpan {
196    /// Start line number (1-indexed)
197    pub start_line: usize,
198    /// Start column number (1-indexed)
199    pub start_column: usize,
200    /// End line number (1-indexed, can be same as start_line for single-line errors)
201    pub end_line: usize,
202    /// End column number (1-indexed)
203    pub end_column: usize,
204    /// Message for this span
205    pub message: String,
206}
207
208/// Enhanced caret positioning configuration
209#[derive(Debug, Clone)]
210pub struct CaretPositioning {
211    /// Primary error span (required)
212    pub primary_span: ErrorSpan,
213    /// Secondary spans for additional context (optional)
214    pub secondary_spans: Vec<ErrorSpan>,
215    /// Whether to show multi-line carets
216    pub show_multi_line_carets: bool,
217    /// Whether to show line continuation indicators
218    pub show_line_continuations: bool,
219    /// Character for multi-line caret start
220    pub multi_line_start_char: char,
221    /// Character for multi-line caret middle
222    pub multi_line_middle_char: char,
223    /// Character for multi-line caret end
224    pub multi_line_end_char: char,
225}
226
227impl Default for CaretPositioning {
228    fn default() -> Self {
229        Self {
230            primary_span: ErrorSpan {
231                start_line: 1,
232                start_column: 1,
233                end_line: 1,
234                end_column: 1,
235                message: String::new(),
236            },
237            secondary_spans: Vec::new(),
238            show_multi_line_carets: true,
239            show_line_continuations: true,
240            multi_line_start_char: '┌',
241            multi_line_middle_char: '│',
242            multi_line_end_char: '└',
243        }
244    }
245}
246
247/// Rust-style error message rendering component with line indicators and caret positioning
248pub struct ErrorDisplay {
249    /// Unique identifier for this error display instance
250    pub id: String,
251    /// Configuration for error display styling
252    pub config: ErrorDisplayConfig,
253}
254
255impl ErrorDisplay {
256    /// Create a new error display with specified ID and config
257    pub fn new(id: String, config: ErrorDisplayConfig) -> Self {
258        Self { id, config }
259    }
260
261    /// Create error display with default configuration
262    pub fn with_defaults(id: String) -> Self {
263        Self::new(id, ErrorDisplayConfig::default())
264    }
265
266    /// Create error display optimized for terminal
267    pub fn with_terminal_config(id: String) -> Self {
268        Self::new(id, ErrorDisplayConfig::terminal())
269    }
270
271    /// Create error display optimized for detailed debugging
272    pub fn with_detailed_config(id: String) -> Self {
273        Self::new(id, ErrorDisplayConfig::detailed())
274    }
275
276    /// Create error display with syntax highlighting for specific language
277    pub fn with_syntax_highlighting(id: String, language: String) -> Self {
278        let mut config = ErrorDisplayConfig::default();
279        config.syntax_highlighting.language = language;
280        Self::new(id, config)
281    }
282
283    /// Create error display with custom syntax highlighting config
284    pub fn with_custom_syntax_config(id: String, syntax_config: SyntaxHighlightConfig) -> Self {
285        let config = ErrorDisplayConfig {
286            syntax_highlighting: syntax_config,
287            ..Default::default()
288        };
289        Self::new(id, config)
290    }
291
292    /// Generate Rust-style error display text
293    pub fn format_error(&self, error: &ErrorInfo, content: &str) -> String {
294        let lines: Vec<&str> = content.lines().collect();
295
296        // Ensure we have valid line numbers (using 1-based indexing)
297        if error.line_number == 0 || error.line_number > lines.len() {
298            return format!("{}: {}", self.severity_text(&error.severity), error.message);
299        }
300
301        let error_line = lines[error.line_number - 1];
302        let line_num_width = self.calculate_line_number_width(error, &lines);
303
304        let mut result = String::new();
305
306        // Error header with severity
307        result.push_str(&format!(
308            "{}: {}\n",
309            self.severity_text(&error.severity),
310            error.message
311        ));
312
313        // File location
314        result.push_str(&format!(
315            " --> {}:{}:{}\n",
316            error.file_path, error.line_number, error.column_number
317        ));
318
319        // Context lines before error (if enabled)
320        if self.config.show_context {
321            let start_line = error
322                .line_number
323                .saturating_sub(self.config.context_lines + 1);
324            for line_idx in start_line..(error.line_number - 1) {
325                if line_idx < lines.len() {
326                    result.push_str(&format!(
327                        "{:width$} | {}\n",
328                        line_idx + 1,
329                        lines[line_idx],
330                        width = line_num_width
331                    ));
332                }
333            }
334        }
335
336        // Separator
337        result.push_str(&format!("{}|\n", " ".repeat(line_num_width + 1)));
338
339        // Error line
340        result.push_str(&format!(
341            "{:width$} | {}\n",
342            error.line_number,
343            error_line,
344            width = line_num_width
345        ));
346
347        // Column indicator with caret
348        if error.column_number > 0 && error.column_number <= error_line.len() + 1 {
349            let spaces_before_pipe = " ".repeat(line_num_width);
350            let spaces_before_caret = " ".repeat(error.column_number.saturating_sub(1));
351            result.push_str(&format!(
352                "{} | {}{}\n",
353                spaces_before_pipe, spaces_before_caret, self.config.caret_char
354            ));
355        }
356
357        // Context lines after error (if enabled)
358        if self.config.show_context {
359            let end_line = (error.line_number + self.config.context_lines).min(lines.len());
360            for line_idx in error.line_number..end_line {
361                result.push_str(&format!(
362                    "{:width$} | {}\n",
363                    line_idx + 1,
364                    lines[line_idx],
365                    width = line_num_width
366                ));
367            }
368        }
369
370        // Help text
371        if let Some(help) = &error.help {
372            result.push_str(&format!("\nhelp: {}\n", help));
373        }
374
375        // Note text
376        if let Some(note) = &error.note {
377            result.push_str(&format!("\nnote: {}\n", note));
378        }
379
380        result
381    }
382
383    /// Render error display within bounds
384    pub fn render(
385        &self,
386        error: &ErrorInfo,
387        content: &str,
388        bounds: &Bounds,
389        buffer: &mut ScreenBuffer,
390    ) {
391        let formatted_error = self.format_error(error, content);
392        let error_lines: Vec<&str> = formatted_error.lines().collect();
393
394        let viewable_height = ComponentDimensions::new(*bounds).content_bounds().height();
395        let start_y = ComponentDimensions::new(*bounds).content_bounds().top();
396
397        // Render error lines within bounds
398        for (line_idx, &line) in error_lines.iter().take(viewable_height).enumerate() {
399            let y_position = start_y + line_idx;
400            if y_position > ComponentDimensions::new(*bounds).content_bounds().bottom() {
401                break;
402            }
403
404            // Determine color based on line content
405            let text_color = self.get_line_color(line, &error.severity);
406
407            print_with_color_and_background_at(
408                y_position,
409                ComponentDimensions::new(*bounds).content_bounds().left(),
410                &Some(text_color),
411                &Some(self.config.background_color.clone()),
412                line,
413                buffer,
414            );
415        }
416    }
417
418    /// Render multiple errors in sequence
419    pub fn render_multiple(
420        &self,
421        errors: &[ErrorInfo],
422        content: &str,
423        bounds: &Bounds,
424        buffer: &mut ScreenBuffer,
425    ) {
426        let mut combined_output = String::new();
427
428        for (idx, error) in errors.iter().enumerate() {
429            combined_output.push_str(&self.format_error(error, content));
430
431            // Add separator between errors
432            if idx < errors.len() - 1 {
433                combined_output.push_str(&format!("\n{}\n\n", "-".repeat(50)));
434            }
435        }
436
437        let error_lines: Vec<&str> = combined_output.lines().collect();
438        let viewable_height = ComponentDimensions::new(*bounds).content_bounds().height();
439        let start_y = ComponentDimensions::new(*bounds).content_bounds().top();
440
441        // Render combined error lines within bounds
442        for (line_idx, &line) in error_lines.iter().take(viewable_height).enumerate() {
443            let y_position = start_y + line_idx;
444            if y_position > ComponentDimensions::new(*bounds).content_bounds().bottom() {
445                break;
446            }
447
448            // Determine color based on line content
449            let text_color = self.get_line_color(line, &ErrorSeverity::Error);
450
451            print_with_color_and_background_at(
452                y_position,
453                ComponentDimensions::new(*bounds).content_bounds().left(),
454                &Some(text_color),
455                &Some(self.config.background_color.clone()),
456                line,
457                buffer,
458            );
459        }
460    }
461
462    /// Apply syntax highlighting to a line of code
463    pub fn apply_syntax_highlighting(&self, line: &str) -> String {
464        if !self.config.syntax_highlighting.enabled {
465            return line.to_string();
466        }
467
468        let tokens = self.tokenize_line(line);
469        let mut result = String::new();
470
471        for (token_type, text) in tokens {
472            let color = self.get_token_color(&token_type);
473            if color != "white" && !text.trim().is_empty() {
474                // Add ANSI color code for non-default colors
475                result.push_str(&format!(
476                    "\x1b[{}m{}\x1b[0m",
477                    self.color_to_ansi(&color),
478                    text
479                ));
480            } else {
481                result.push_str(text);
482            }
483        }
484
485        result
486    }
487
488    /// Tokenize a line of code for syntax highlighting
489    fn tokenize_line<'a>(&self, line: &'a str) -> Vec<(SyntaxToken, &'a str)> {
490        let mut tokens = Vec::new();
491        let mut current_pos = 0;
492        let chars: Vec<char> = line.chars().collect();
493
494        while current_pos < chars.len() {
495            let remaining = &line[current_pos..];
496
497            // Skip whitespace
498            if chars[current_pos].is_whitespace() {
499                let whitespace_end = self.find_whitespace_end(&chars, current_pos);
500                tokens.push((SyntaxToken::Text, &line[current_pos..whitespace_end]));
501                current_pos = whitespace_end;
502                continue;
503            }
504
505            // Comments
506            if remaining.starts_with("//") || remaining.starts_with("#") {
507                tokens.push((SyntaxToken::Comment, &line[current_pos..]));
508                break;
509            }
510
511            // String literals
512            if chars[current_pos] == '"' {
513                let string_end = self.find_string_end(&chars, current_pos);
514                tokens.push((SyntaxToken::String, &line[current_pos..string_end]));
515                current_pos = string_end;
516                continue;
517            }
518
519            // Numbers
520            if chars[current_pos].is_ascii_digit() {
521                let number_end = self.find_number_end(&chars, current_pos);
522                tokens.push((SyntaxToken::Number, &line[current_pos..number_end]));
523                current_pos = number_end;
524                continue;
525            }
526
527            // Keywords and identifiers
528            if chars[current_pos].is_alphabetic() || chars[current_pos] == '_' {
529                let word_end = self.find_word_end(&chars, current_pos);
530                let word = &line[current_pos..word_end];
531                let token_type = self.classify_word(word);
532                tokens.push((token_type, word));
533                current_pos = word_end;
534                continue;
535            }
536
537            // Operators and punctuation
538            let operator_end = self.find_operator_end(&chars, current_pos);
539            let operator = &line[current_pos..operator_end];
540            let token_type = if self.is_operator(operator) {
541                SyntaxToken::Operator
542            } else {
543                SyntaxToken::Punctuation
544            };
545            tokens.push((token_type, operator));
546            current_pos = operator_end;
547        }
548
549        tokens
550    }
551
552    /// Get color for a specific token type
553    fn get_token_color(&self, token_type: &SyntaxToken) -> String {
554        match token_type {
555            SyntaxToken::Keyword => self.config.syntax_highlighting.keyword_color.clone(),
556            SyntaxToken::String => self.config.syntax_highlighting.string_color.clone(),
557            SyntaxToken::Number => self.config.syntax_highlighting.number_color.clone(),
558            SyntaxToken::Comment => self.config.syntax_highlighting.comment_color.clone(),
559            SyntaxToken::Function => self.config.syntax_highlighting.function_color.clone(),
560            SyntaxToken::Type => self.config.syntax_highlighting.type_color.clone(),
561            SyntaxToken::Variable => self.config.syntax_highlighting.variable_color.clone(),
562            SyntaxToken::Operator => self.config.syntax_highlighting.operator_color.clone(),
563            SyntaxToken::Punctuation => self.config.syntax_highlighting.punctuation_color.clone(),
564            SyntaxToken::Text => "white".to_string(),
565        }
566    }
567
568    /// Convert color name to ANSI escape code
569    fn color_to_ansi(&self, color: &str) -> &'static str {
570        match color {
571            "black" => "30",
572            "red" => "31",
573            "green" => "32",
574            "yellow" => "33",
575            "blue" => "34",
576            "magenta" => "35",
577            "cyan" => "36",
578            "white" => "37",
579            "bright_black" => "90",
580            "bright_red" => "91",
581            "bright_green" => "92",
582            "bright_yellow" => "93",
583            "bright_blue" => "94",
584            "bright_magenta" => "95",
585            "bright_cyan" => "96",
586            "bright_white" => "97",
587            _ => "37", // Default to white
588        }
589    }
590
591    /// Find end of whitespace sequence
592    fn find_whitespace_end(&self, chars: &[char], start: usize) -> usize {
593        let mut pos = start;
594        while pos < chars.len() && chars[pos].is_whitespace() {
595            pos += 1;
596        }
597        pos
598    }
599
600    /// Find end of string literal
601    fn find_string_end(&self, chars: &[char], start: usize) -> usize {
602        let mut pos = start + 1; // Skip opening quote
603        while pos < chars.len() {
604            if chars[pos] == '"' && (pos == 0 || chars[pos - 1] != '\\') {
605                return pos + 1;
606            }
607            pos += 1;
608        }
609        chars.len() // Unclosed string
610    }
611
612    /// Find end of number literal
613    fn find_number_end(&self, chars: &[char], start: usize) -> usize {
614        let mut pos = start;
615        while pos < chars.len()
616            && (chars[pos].is_ascii_digit() || chars[pos] == '.' || chars[pos] == '_')
617        {
618            pos += 1;
619        }
620        pos
621    }
622
623    /// Find end of word (identifier/keyword)
624    fn find_word_end(&self, chars: &[char], start: usize) -> usize {
625        let mut pos = start;
626        while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
627            pos += 1;
628        }
629        pos
630    }
631
632    /// Find end of operator sequence
633    fn find_operator_end(&self, chars: &[char], start: usize) -> usize {
634        let pos = start + 1;
635        let first_char = chars[start];
636
637        // Handle multi-character operators
638        if pos < chars.len() {
639            let second_char = chars[pos];
640            match (first_char, second_char) {
641                ('=', '=')
642                | ('!', '=')
643                | ('<', '=')
644                | ('>', '=')
645                | ('+', '=')
646                | ('-', '=')
647                | ('*', '=')
648                | ('/', '=')
649                | ('-', '>')
650                | (':', ':')
651                | ('.', '.') => pos + 1,
652                _ => pos,
653            }
654        } else {
655            pos
656        }
657    }
658
659    /// Classify a word as keyword, type, function, or variable
660    fn classify_word(&self, word: &str) -> SyntaxToken {
661        match self.config.syntax_highlighting.language.as_str() {
662            "rust" => self.classify_rust_word(word),
663            "yaml" => self.classify_yaml_word(word),
664            "json" => SyntaxToken::Variable, // JSON has no keywords
665            _ => SyntaxToken::Variable,
666        }
667    }
668
669    /// Classify Rust language keywords and identifiers
670    fn classify_rust_word(&self, word: &str) -> SyntaxToken {
671        match word {
672            // Rust keywords
673            "fn" | "let" | "mut" | "const" | "static" | "impl" | "struct" | "enum" | "trait"
674            | "pub" | "use" | "mod" | "crate" | "super" | "self" | "Self" | "match" | "if"
675            | "else" | "while" | "for" | "loop" | "break" | "continue" | "return" | "async"
676            | "await" | "move" | "ref" | "as" | "in" | "where" | "unsafe" | "extern" => {
677                SyntaxToken::Keyword
678            }
679
680            // Common types
681            "String" | "str" | "Vec" | "Option" | "Result" | "Box" | "Rc" | "Arc" | "i8"
682            | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
683            | "usize" | "f32" | "f64" | "bool" | "char" => SyntaxToken::Type,
684
685            _ => {
686                // Function detection (simple heuristic)
687                if word.chars().next().is_some_and(|c| c.is_lowercase())
688                    && word.chars().all(|c| c.is_alphanumeric() || c == '_')
689                {
690                    SyntaxToken::Function
691                } else if word.chars().next().is_some_and(|c| c.is_uppercase()) {
692                    SyntaxToken::Type
693                } else {
694                    SyntaxToken::Variable
695                }
696            }
697        }
698    }
699
700    /// Classify YAML language keywords
701    fn classify_yaml_word(&self, word: &str) -> SyntaxToken {
702        match word {
703            "true" | "false" | "null" | "yes" | "no" | "on" | "off" => SyntaxToken::Keyword,
704            _ => SyntaxToken::Variable,
705        }
706    }
707
708    /// Check if text is an operator
709    fn is_operator(&self, text: &str) -> bool {
710        matches!(
711            text,
712            "+" | "-"
713                | "*"
714                | "/"
715                | "%"
716                | "="
717                | "=="
718                | "!="
719                | "<"
720                | ">"
721                | "<="
722                | ">="
723                | "&&"
724                | "||"
725                | "!"
726                | "&"
727                | "|"
728                | "^"
729                | "<<"
730                | ">>"
731                | "+="
732                | "-="
733                | "*="
734                | "/="
735                | "%="
736                | "^="
737                | "&="
738                | "|="
739                | "<<="
740                | ">>="
741                | "->"
742                | "::"
743                | ".."
744                | "..."
745                | "?"
746                | "@"
747                | "#"
748        )
749    }
750
751    /// Generate syntax-highlighted error display text
752    pub fn format_error_with_highlighting(&self, error: &ErrorInfo, content: &str) -> String {
753        let lines: Vec<&str> = content.lines().collect();
754
755        // Ensure we have valid line numbers (using 1-based indexing)
756        if error.line_number == 0 || error.line_number > lines.len() {
757            return format!("{}: {}", self.severity_text(&error.severity), error.message);
758        }
759
760        let error_line = lines[error.line_number - 1];
761        let line_num_width = self.calculate_line_number_width(error, &lines);
762
763        let mut result = String::new();
764
765        // Error header with severity
766        result.push_str(&format!(
767            "{}: {}\n",
768            self.severity_text(&error.severity),
769            error.message
770        ));
771
772        // File location
773        result.push_str(&format!(
774            " --> {}:{}:{}\n",
775            error.file_path, error.line_number, error.column_number
776        ));
777
778        // Context lines before error (if enabled)
779        if self.config.show_context {
780            let start_line = error
781                .line_number
782                .saturating_sub(self.config.context_lines + 1);
783            for line_idx in start_line..(error.line_number - 1) {
784                if line_idx < lines.len() {
785                    let highlighted_line = self.apply_syntax_highlighting(lines[line_idx]);
786                    result.push_str(&format!(
787                        "{:width$} | {}\n",
788                        line_idx + 1,
789                        highlighted_line,
790                        width = line_num_width
791                    ));
792                }
793            }
794        }
795
796        // Separator
797        result.push_str(&format!("{}|\n", " ".repeat(line_num_width + 1)));
798
799        // Error line with syntax highlighting
800        let highlighted_error_line = self.apply_syntax_highlighting(error_line);
801        result.push_str(&format!(
802            "{:width$} | {}\n",
803            error.line_number,
804            highlighted_error_line,
805            width = line_num_width
806        ));
807
808        // Column indicator with caret
809        if error.column_number > 0 && error.column_number <= error_line.len() + 1 {
810            let spaces_before_pipe = " ".repeat(line_num_width);
811            let spaces_before_caret = " ".repeat(error.column_number.saturating_sub(1));
812            result.push_str(&format!(
813                "{} | {}{}\n",
814                spaces_before_pipe, spaces_before_caret, self.config.caret_char
815            ));
816        }
817
818        // Context lines after error (if enabled)
819        if self.config.show_context {
820            let end_line = (error.line_number + self.config.context_lines).min(lines.len());
821            for line_idx in error.line_number..end_line {
822                let highlighted_line = self.apply_syntax_highlighting(lines[line_idx]);
823                result.push_str(&format!(
824                    "{:width$} | {}\n",
825                    line_idx + 1,
826                    highlighted_line,
827                    width = line_num_width
828                ));
829            }
830        }
831
832        // Help text
833        if let Some(help) = &error.help {
834            result.push_str(&format!("\nhelp: {}\n", help));
835        }
836
837        // Note text
838        if let Some(note) = &error.note {
839            result.push_str(&format!("\nnote: {}\n", note));
840        }
841
842        result
843    }
844
845    /// Get severity text for display
846    fn severity_text(&self, severity: &ErrorSeverity) -> &str {
847        match severity {
848            ErrorSeverity::Error => "error",
849            ErrorSeverity::Warning => "warning",
850            ErrorSeverity::Info => "info",
851            ErrorSeverity::Hint => "hint",
852        }
853    }
854
855    /// Calculate appropriate width for line numbers
856    fn calculate_line_number_width(&self, error: &ErrorInfo, lines: &[&str]) -> usize {
857        let max_line = if self.config.show_context {
858            (error.line_number + self.config.context_lines).min(lines.len())
859        } else {
860            error.line_number
861        };
862        format!("{}", max_line).len().max(3)
863    }
864
865    /// Determine color for a specific line based on content
866    fn get_line_color(&self, line: &str, severity: &ErrorSeverity) -> String {
867        if line.starts_with("error:") {
868            self.config.error_color.clone()
869        } else if line.starts_with("warning:") {
870            self.config.warning_color.clone()
871        } else if line.starts_with("info:") {
872            self.config.info_color.clone()
873        } else if line.starts_with("hint:") || line.starts_with("help:") {
874            self.config.hint_color.clone()
875        } else if line.starts_with("note:") {
876            self.config.info_color.clone()
877        } else if line.contains(" --> ") {
878            self.config.file_path_color.clone()
879        } else if line.contains(self.config.caret_char) {
880            self.config.caret_color.clone()
881        } else if line.contains(" | ") {
882            if line.trim_start().starts_with("|") {
883                self.config.border_color.clone()
884            } else {
885                // Line with line number
886                self.config.line_number_color.clone()
887            }
888        } else {
889            // Default text color based on severity
890            match severity {
891                ErrorSeverity::Error => self.config.error_color.clone(),
892                ErrorSeverity::Warning => self.config.warning_color.clone(),
893                ErrorSeverity::Info => self.config.info_color.clone(),
894                ErrorSeverity::Hint => self.config.hint_color.clone(),
895            }
896        }
897    }
898
899    /// Create ErrorInfo from serde_yaml::Error
900    pub fn from_yaml_error(
901        yaml_error: &serde_yaml::Error,
902        file_path: &str,
903        message: String,
904    ) -> Option<ErrorInfo> {
905        yaml_error.location().map(|location| ErrorInfo {
906            message,
907            file_path: file_path.to_string(),
908            line_number: location.line(),
909            column_number: location.column(),
910            severity: ErrorSeverity::Error,
911            help: Some("Check YAML syntax and structure".to_string()),
912            note: None,
913            caret_positioning: None,
914        })
915    }
916
917    /// Create ErrorInfo for configuration validation
918    pub fn validation_error(
919        file_path: &str,
920        line_number: usize,
921        column_number: usize,
922        message: String,
923    ) -> ErrorInfo {
924        ErrorInfo {
925            message,
926            file_path: file_path.to_string(),
927            line_number,
928            column_number,
929            severity: ErrorSeverity::Error,
930            help: Some("Verify configuration values and structure".to_string()),
931            note: None,
932            caret_positioning: None,
933        }
934    }
935
936    /// Render error with enhanced multi-line caret positioning
937    pub fn render_with_enhanced_carets(
938        &self,
939        error: &ErrorInfo,
940        content: &str,
941        bounds: &Bounds,
942        buffer: &mut ScreenBuffer,
943    ) {
944        if let Some(caret_positioning) = &error.caret_positioning {
945            let formatted_error =
946                self.format_error_with_multi_line_carets(error, content, caret_positioning);
947            let error_lines: Vec<&str> = formatted_error.lines().collect();
948
949            let viewable_height = ComponentDimensions::new(*bounds).content_bounds().height();
950            let start_y = ComponentDimensions::new(*bounds).content_bounds().top();
951
952            // Render error lines within bounds
953            for (line_idx, &line) in error_lines.iter().take(viewable_height).enumerate() {
954                let y_position = start_y + line_idx;
955                if y_position > ComponentDimensions::new(*bounds).content_bounds().bottom() {
956                    break;
957                }
958
959                // Enhanced color determination for multi-line carets
960                let text_color =
961                    self.get_enhanced_line_color(line, &error.severity, caret_positioning);
962
963                print_with_color_and_background_at(
964                    y_position,
965                    ComponentDimensions::new(*bounds).content_bounds().left(),
966                    &Some(text_color),
967                    &Some(self.config.background_color.clone()),
968                    line,
969                    buffer,
970                );
971            }
972        } else {
973            // Fallback to standard rendering
974            self.render(error, content, bounds, buffer);
975        }
976    }
977
978    /// Format error message with enhanced multi-line caret positioning
979    pub fn format_error_with_multi_line_carets(
980        &self,
981        error: &ErrorInfo,
982        content: &str,
983        caret_positioning: &CaretPositioning,
984    ) -> String {
985        let mut result = String::new();
986
987        // Header with severity and message
988        let _severity_color = self.get_severity_color(&error.severity);
989        result.push_str(&format!(
990            "{}: {}\n",
991            match error.severity {
992                ErrorSeverity::Error => "error",
993                ErrorSeverity::Warning => "warning",
994                ErrorSeverity::Info => "info",
995                ErrorSeverity::Hint => "hint",
996            },
997            error.message
998        ));
999
1000        // File location
1001        result.push_str(&format!(
1002            " --> {}:{}:{}\n",
1003            error.file_path,
1004            caret_positioning.primary_span.start_line,
1005            caret_positioning.primary_span.start_column
1006        ));
1007
1008        let lines: Vec<&str> = content.lines().collect();
1009        let line_number_width = self.calculate_line_number_width(error, &lines);
1010
1011        // Render multi-line error span
1012        self.render_multi_line_span(
1013            &mut result,
1014            &lines,
1015            &caret_positioning.primary_span,
1016            line_number_width,
1017            true, // is_primary
1018        );
1019
1020        // Render secondary spans
1021        for secondary_span in &caret_positioning.secondary_spans {
1022            result.push_str(&format!("   {}\n", secondary_span.message));
1023            self.render_multi_line_span(
1024                &mut result,
1025                &lines,
1026                secondary_span,
1027                line_number_width,
1028                false, // is_primary
1029            );
1030        }
1031
1032        // Add help and note
1033        if let Some(help) = &error.help {
1034            result.push_str(&format!("   = help: {}\n", help));
1035        }
1036        if let Some(note) = &error.note {
1037            result.push_str(&format!("   = note: {}\n", note));
1038        }
1039
1040        result
1041    }
1042
1043    /// Render a multi-line error span with proper caret indicators
1044    fn render_multi_line_span(
1045        &self,
1046        result: &mut String,
1047        lines: &[&str],
1048        span: &ErrorSpan,
1049        line_number_width: usize,
1050        _is_primary: bool,
1051    ) {
1052        let start_line = span.start_line.saturating_sub(1);
1053        let end_line = span.end_line.saturating_sub(1);
1054
1055        // Show context lines before if enabled
1056        let context_start = if self.config.show_context {
1057            start_line.saturating_sub(self.config.context_lines)
1058        } else {
1059            start_line
1060        };
1061
1062        let context_end = if self.config.show_context {
1063            (end_line + self.config.context_lines + 1).min(lines.len())
1064        } else {
1065            end_line + 1
1066        };
1067
1068        for line_idx in context_start..context_end {
1069            if line_idx >= lines.len() {
1070                break;
1071            }
1072
1073            let line_num = line_idx + 1;
1074            let is_error_line = line_idx >= start_line && line_idx <= end_line;
1075            let line_content = lines[line_idx];
1076
1077            // Format line number with proper padding
1078            let line_num_str = if is_error_line {
1079                format!("{:width$}", line_num, width = line_number_width)
1080            } else {
1081                " ".repeat(line_number_width)
1082            };
1083
1084            // Apply syntax highlighting
1085            let highlighted_content = self.apply_syntax_highlighting(line_content);
1086
1087            if is_error_line {
1088                result.push_str(&format!("{} | {}\n", line_num_str, highlighted_content));
1089
1090                // Add caret indicators for error lines
1091                if span.start_line == span.end_line {
1092                    // Single line error - traditional caret
1093                    let spaces_before_pipe = " ".repeat(line_number_width);
1094                    let spaces_before_caret = " ".repeat(span.start_column.saturating_sub(1));
1095                    let caret_length = if span.end_column > span.start_column {
1096                        span.end_column - span.start_column
1097                    } else {
1098                        1
1099                    };
1100                    let carets = self.config.caret_char.to_string().repeat(caret_length);
1101                    result.push_str(&format!(
1102                        "{} | {}{}\n",
1103                        spaces_before_pipe, spaces_before_caret, carets
1104                    ));
1105                } else {
1106                    // Multi-line error - enhanced carets
1107                    let spaces_before_pipe = " ".repeat(line_number_width);
1108
1109                    if line_idx == start_line {
1110                        // Start line - show start character and line continuation
1111                        let spaces_before_caret = " ".repeat(span.start_column.saturating_sub(1));
1112                        let continuation_length = line_content
1113                            .len()
1114                            .saturating_sub(span.start_column.saturating_sub(1));
1115                        let continuation = if continuation_length > 0 {
1116                            format!("┌{}", "─".repeat(continuation_length.saturating_sub(1)))
1117                        } else {
1118                            "┌".to_string()
1119                        };
1120                        result.push_str(&format!(
1121                            "{} | {}{}\n",
1122                            spaces_before_pipe, spaces_before_caret, continuation
1123                        ));
1124                    } else if line_idx == end_line {
1125                        // End line - show end character and end caret
1126                        let end_caret_length = span.end_column.saturating_sub(1);
1127                        let end_continuation = if end_caret_length > 0 {
1128                            format!(
1129                                "{}└{}",
1130                                "─".repeat(end_caret_length.saturating_sub(1)),
1131                                self.config.caret_char
1132                            )
1133                        } else {
1134                            format!("└{}", self.config.caret_char)
1135                        };
1136                        result
1137                            .push_str(&format!("{} | {}\n", spaces_before_pipe, end_continuation));
1138                    } else {
1139                        // Middle line - show continuation character
1140                        result.push_str(&format!("{} | │\n", spaces_before_pipe));
1141                    }
1142                }
1143            } else {
1144                // Context line
1145                result.push_str(&format!("{} | {}\n", line_num_str, highlighted_content));
1146            }
1147        }
1148    }
1149
1150    /// Get color for error severity level
1151    fn get_severity_color(&self, severity: &ErrorSeverity) -> String {
1152        match severity {
1153            ErrorSeverity::Error => self.config.error_color.clone(),
1154            ErrorSeverity::Warning => self.config.warning_color.clone(),
1155            ErrorSeverity::Info => self.config.info_color.clone(),
1156            ErrorSeverity::Hint => self.config.hint_color.clone(),
1157        }
1158    }
1159
1160    /// Get enhanced line color for multi-line caret positioning
1161    fn get_enhanced_line_color(
1162        &self,
1163        line: &str,
1164        severity: &ErrorSeverity,
1165        caret_positioning: &CaretPositioning,
1166    ) -> String {
1167        // Check for multi-line caret indicators
1168        if line.contains(caret_positioning.multi_line_start_char)
1169            || line.contains(caret_positioning.multi_line_middle_char)
1170            || line.contains(caret_positioning.multi_line_end_char)
1171        {
1172            return self.config.caret_color.clone();
1173        }
1174
1175        // Standard line color determination
1176        self.get_line_color(line, severity)
1177    }
1178
1179    /// Create ErrorInfo with enhanced caret positioning for multi-line errors
1180    pub fn create_multi_line_error(
1181        message: String,
1182        file_path: String,
1183        start_line: usize,
1184        start_column: usize,
1185        end_line: usize,
1186        end_column: usize,
1187        severity: ErrorSeverity,
1188    ) -> ErrorInfo {
1189        let primary_span = ErrorSpan {
1190            start_line,
1191            start_column,
1192            end_line,
1193            end_column,
1194            message: message.clone(),
1195        };
1196
1197        ErrorInfo {
1198            message,
1199            file_path,
1200            line_number: start_line,
1201            column_number: start_column,
1202            severity,
1203            help: None,
1204            note: None,
1205            caret_positioning: Some(CaretPositioning {
1206                primary_span,
1207                ..Default::default()
1208            }),
1209        }
1210    }
1211
1212    /// Add secondary span to existing error for additional context
1213    pub fn add_secondary_span(
1214        &self,
1215        error: &mut ErrorInfo,
1216        start_line: usize,
1217        start_column: usize,
1218        end_line: usize,
1219        end_column: usize,
1220        message: String,
1221    ) {
1222        let secondary_span = ErrorSpan {
1223            start_line,
1224            start_column,
1225            end_line,
1226            end_column,
1227            message,
1228        };
1229
1230        if let Some(caret_positioning) = &mut error.caret_positioning {
1231            caret_positioning.secondary_spans.push(secondary_span);
1232        } else {
1233            error.caret_positioning = Some(CaretPositioning {
1234                secondary_spans: vec![secondary_span],
1235                ..Default::default()
1236            });
1237        }
1238    }
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243    use super::*;
1244
1245    #[test]
1246    fn test_syntax_highlight_config_creation() {
1247        let config = SyntaxHighlightConfig::default();
1248        assert!(config.enabled);
1249        assert_eq!(config.language, "rust");
1250        assert_eq!(config.keyword_color, "magenta");
1251        assert_eq!(config.string_color, "green");
1252    }
1253
1254    #[test]
1255    fn test_error_display_with_syntax_highlighting() {
1256        let display =
1257            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1258        assert!(display.config.syntax_highlighting.enabled);
1259        assert_eq!(display.config.syntax_highlighting.language, "rust");
1260    }
1261
1262    #[test]
1263    fn test_error_display_with_custom_syntax_config() {
1264        let mut syntax_config = SyntaxHighlightConfig::default();
1265        syntax_config.enabled = false;
1266        syntax_config.language = "yaml".to_string();
1267
1268        let display = ErrorDisplay::with_custom_syntax_config("test".to_string(), syntax_config);
1269        assert!(!display.config.syntax_highlighting.enabled);
1270        assert_eq!(display.config.syntax_highlighting.language, "yaml");
1271    }
1272
1273    #[test]
1274    fn test_tokenize_rust_line() {
1275        let display =
1276            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1277        let tokens = display.tokenize_line("fn main() {");
1278
1279        assert!(tokens.len() >= 4);
1280        // Find keyword token
1281        let keyword_token = tokens
1282            .iter()
1283            .find(|(token_type, text)| *token_type == SyntaxToken::Keyword && *text == "fn");
1284        assert!(keyword_token.is_some());
1285    }
1286
1287    #[test]
1288    fn test_tokenize_string_literal() {
1289        let display =
1290            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1291        let tokens = display.tokenize_line("let msg = \"hello\";");
1292
1293        // Find string token
1294        let string_token = tokens
1295            .iter()
1296            .find(|(token_type, _)| *token_type == SyntaxToken::String);
1297        assert!(string_token.is_some());
1298    }
1299
1300    #[test]
1301    fn test_tokenize_number_literal() {
1302        let display =
1303            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1304        let tokens = display.tokenize_line("let x = 42;");
1305
1306        // Find number token
1307        let number_token = tokens
1308            .iter()
1309            .find(|(token_type, text)| *token_type == SyntaxToken::Number && *text == "42");
1310        assert!(number_token.is_some());
1311    }
1312
1313    #[test]
1314    fn test_tokenize_comment() {
1315        let display =
1316            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1317        let tokens = display.tokenize_line("// This is a comment");
1318
1319        // Should have comment token for the entire line
1320        let comment_token = tokens
1321            .iter()
1322            .find(|(token_type, _)| *token_type == SyntaxToken::Comment);
1323        assert!(comment_token.is_some());
1324    }
1325
1326    #[test]
1327    fn test_classify_rust_keywords() {
1328        let display =
1329            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1330
1331        assert_eq!(display.classify_rust_word("fn"), SyntaxToken::Keyword);
1332        assert_eq!(display.classify_rust_word("let"), SyntaxToken::Keyword);
1333        assert_eq!(display.classify_rust_word("impl"), SyntaxToken::Keyword);
1334        assert_eq!(display.classify_rust_word("struct"), SyntaxToken::Keyword);
1335    }
1336
1337    #[test]
1338    fn test_classify_rust_types() {
1339        let display =
1340            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1341
1342        assert_eq!(display.classify_rust_word("String"), SyntaxToken::Type);
1343        assert_eq!(display.classify_rust_word("Vec"), SyntaxToken::Type);
1344        assert_eq!(display.classify_rust_word("i32"), SyntaxToken::Type);
1345        assert_eq!(display.classify_rust_word("bool"), SyntaxToken::Type);
1346    }
1347
1348    #[test]
1349    fn test_classify_yaml_keywords() {
1350        let display =
1351            ErrorDisplay::with_syntax_highlighting("test".to_string(), "yaml".to_string());
1352
1353        assert_eq!(display.classify_yaml_word("true"), SyntaxToken::Keyword);
1354        assert_eq!(display.classify_yaml_word("false"), SyntaxToken::Keyword);
1355        assert_eq!(display.classify_yaml_word("null"), SyntaxToken::Keyword);
1356        assert_eq!(display.classify_yaml_word("yes"), SyntaxToken::Keyword);
1357    }
1358
1359    #[test]
1360    fn test_is_operator() {
1361        let display =
1362            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1363
1364        assert!(display.is_operator("="));
1365        assert!(display.is_operator("=="));
1366        assert!(display.is_operator("!="));
1367        assert!(display.is_operator("->"));
1368        assert!(display.is_operator("::"));
1369        assert!(!display.is_operator("abc"));
1370    }
1371
1372    #[test]
1373    fn test_color_to_ansi() {
1374        let display =
1375            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1376
1377        assert_eq!(display.color_to_ansi("red"), "31");
1378        assert_eq!(display.color_to_ansi("green"), "32");
1379        assert_eq!(display.color_to_ansi("bright_red"), "91");
1380        assert_eq!(display.color_to_ansi("unknown"), "37"); // Default to white
1381    }
1382
1383    #[test]
1384    fn test_apply_syntax_highlighting_disabled() {
1385        let mut config = ErrorDisplayConfig::default();
1386        config.syntax_highlighting.enabled = false;
1387        let display = ErrorDisplay::new("test".to_string(), config);
1388
1389        let line = "fn main() {}";
1390        let highlighted = display.apply_syntax_highlighting(line);
1391        assert_eq!(highlighted, line); // Should be unchanged when disabled
1392    }
1393
1394    #[test]
1395    fn test_apply_syntax_highlighting_enabled() {
1396        let display =
1397            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1398
1399        let line = "fn main() {}";
1400        let highlighted = display.apply_syntax_highlighting(line);
1401        assert!(highlighted.contains("\x1b[")); // Should contain ANSI codes
1402        assert!(highlighted.contains("fn")); // Should still contain the text
1403    }
1404
1405    #[test]
1406    fn test_format_error_with_highlighting() {
1407        let display =
1408            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1409
1410        let error = ErrorInfo {
1411            message: "expected `;`".to_string(),
1412            file_path: "test.rs".to_string(),
1413            line_number: 2,
1414            column_number: 15,
1415            severity: ErrorSeverity::Error,
1416            help: Some("add semicolon".to_string()),
1417            note: Some("syntax error".to_string()),
1418            caret_positioning: None,
1419        };
1420
1421        let content = "fn main() {\n    let x = 42\n}";
1422        let formatted = display.format_error_with_highlighting(&error, content);
1423
1424        assert!(formatted.contains("error: expected `;`"));
1425        assert!(formatted.contains(" --> test.rs:2:15"));
1426        assert!(formatted.contains("help: add semicolon"));
1427        assert!(formatted.contains("note: syntax error"));
1428        // Should contain syntax highlighting for the error line
1429        assert!(formatted.contains("let"));
1430    }
1431
1432    #[test]
1433    fn test_find_string_end() {
1434        let display =
1435            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1436        let chars: Vec<char> = "\"hello world\"".chars().collect();
1437
1438        let end = display.find_string_end(&chars, 0);
1439        assert_eq!(end, 13); // Position after closing quote
1440    }
1441
1442    #[test]
1443    fn test_find_string_end_unclosed() {
1444        let display =
1445            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1446        let chars: Vec<char> = "\"hello world".chars().collect();
1447
1448        let end = display.find_string_end(&chars, 0);
1449        assert_eq!(end, chars.len()); // Should return length for unclosed string
1450    }
1451
1452    #[test]
1453    fn test_find_number_end() {
1454        let display =
1455            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1456        let chars: Vec<char> = "123.45_u32".chars().collect();
1457
1458        let end = display.find_number_end(&chars, 0);
1459        assert_eq!(end, 7); // Should include digits, dots, and underscores but not letters
1460    }
1461
1462    #[test]
1463    fn test_find_word_end() {
1464        let display =
1465            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1466        let chars: Vec<char> = "hello_world123 ".chars().collect();
1467
1468        let end = display.find_word_end(&chars, 0);
1469        assert_eq!(end, 14); // Should include alphanumeric and underscores
1470    }
1471
1472    #[test]
1473    fn test_find_operator_end_single_char() {
1474        let display =
1475            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1476        let chars: Vec<char> = "+ 1".chars().collect();
1477
1478        let end = display.find_operator_end(&chars, 0);
1479        assert_eq!(end, 1); // Single character operator
1480    }
1481
1482    #[test]
1483    fn test_find_operator_end_multi_char() {
1484        let display =
1485            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1486        let chars: Vec<char> = "== 1".chars().collect();
1487
1488        let end = display.find_operator_end(&chars, 0);
1489        assert_eq!(end, 2); // Multi-character operator
1490    }
1491
1492    #[test]
1493    fn test_get_token_color() {
1494        let display =
1495            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1496
1497        assert_eq!(display.get_token_color(&SyntaxToken::Keyword), "magenta");
1498        assert_eq!(display.get_token_color(&SyntaxToken::String), "green");
1499        assert_eq!(display.get_token_color(&SyntaxToken::Number), "yellow");
1500        assert_eq!(
1501            display.get_token_color(&SyntaxToken::Comment),
1502            "bright_black"
1503        );
1504        assert_eq!(display.get_token_color(&SyntaxToken::Text), "white");
1505    }
1506
1507    fn create_test_buffer() -> ScreenBuffer {
1508        ScreenBuffer::new()
1509    }
1510
1511    #[test]
1512    fn test_multi_language_support() {
1513        // Test Rust highlighting
1514        let rust_display =
1515            ErrorDisplay::with_syntax_highlighting("rust_test".to_string(), "rust".to_string());
1516        assert_eq!(rust_display.config.syntax_highlighting.language, "rust");
1517        assert_eq!(rust_display.classify_rust_word("fn"), SyntaxToken::Keyword);
1518
1519        // Test YAML highlighting
1520        let yaml_display =
1521            ErrorDisplay::with_syntax_highlighting("yaml_test".to_string(), "yaml".to_string());
1522        assert_eq!(yaml_display.config.syntax_highlighting.language, "yaml");
1523        assert_eq!(
1524            yaml_display.classify_yaml_word("true"),
1525            SyntaxToken::Keyword
1526        );
1527    }
1528
1529    #[test]
1530    fn test_complex_tokenization() {
1531        let display =
1532            ErrorDisplay::with_syntax_highlighting("test".to_string(), "rust".to_string());
1533        let tokens = display
1534            .tokenize_line("let result: Result<String, Error> = Ok(\"success\".to_string());");
1535
1536        // Verify we have various token types
1537        let has_keyword = tokens.iter().any(|(t, _)| *t == SyntaxToken::Keyword);
1538        let has_type = tokens.iter().any(|(t, _)| *t == SyntaxToken::Type);
1539        let has_string = tokens.iter().any(|(t, _)| *t == SyntaxToken::String);
1540        let has_operator = tokens.iter().any(|(t, _)| *t == SyntaxToken::Operator);
1541        let has_punctuation = tokens.iter().any(|(t, _)| *t == SyntaxToken::Punctuation);
1542
1543        assert!(has_keyword);
1544        assert!(has_type);
1545        assert!(has_string);
1546        assert!(has_operator);
1547        assert!(has_punctuation);
1548    }
1549
1550    #[test]
1551    fn test_error_span_creation() {
1552        let span = ErrorSpan {
1553            start_line: 5,
1554            start_column: 10,
1555            end_line: 7,
1556            end_column: 15,
1557            message: "Multi-line error span".to_string(),
1558        };
1559
1560        assert_eq!(span.start_line, 5);
1561        assert_eq!(span.start_column, 10);
1562        assert_eq!(span.end_line, 7);
1563        assert_eq!(span.end_column, 15);
1564        assert_eq!(span.message, "Multi-line error span");
1565    }
1566
1567    #[test]
1568    fn test_caret_positioning_default() {
1569        let caret_positioning = CaretPositioning::default();
1570
1571        assert_eq!(caret_positioning.primary_span.start_line, 1);
1572        assert_eq!(caret_positioning.primary_span.start_column, 1);
1573        assert_eq!(caret_positioning.primary_span.end_line, 1);
1574        assert_eq!(caret_positioning.primary_span.end_column, 1);
1575        assert!(caret_positioning.secondary_spans.is_empty());
1576        assert!(caret_positioning.show_multi_line_carets);
1577        assert!(caret_positioning.show_line_continuations);
1578        assert_eq!(caret_positioning.multi_line_start_char, '┌');
1579        assert_eq!(caret_positioning.multi_line_middle_char, '│');
1580        assert_eq!(caret_positioning.multi_line_end_char, '└');
1581    }
1582
1583    #[test]
1584    fn test_create_multi_line_error() {
1585        let error = ErrorDisplay::create_multi_line_error(
1586            "Multi-line syntax error".to_string(),
1587            "test.rs".to_string(),
1588            10,
1589            5,
1590            12,
1591            8,
1592            ErrorSeverity::Error,
1593        );
1594
1595        assert_eq!(error.message, "Multi-line syntax error");
1596        assert_eq!(error.file_path, "test.rs");
1597        assert_eq!(error.line_number, 10);
1598        assert_eq!(error.column_number, 5);
1599        assert_eq!(error.severity, ErrorSeverity::Error);
1600
1601        let caret_positioning = error.caret_positioning.unwrap();
1602        assert_eq!(caret_positioning.primary_span.start_line, 10);
1603        assert_eq!(caret_positioning.primary_span.start_column, 5);
1604        assert_eq!(caret_positioning.primary_span.end_line, 12);
1605        assert_eq!(caret_positioning.primary_span.end_column, 8);
1606    }
1607
1608    #[test]
1609    fn test_add_secondary_span() {
1610        let display = ErrorDisplay::with_defaults("test".to_string());
1611        let mut error = ErrorDisplay::create_multi_line_error(
1612            "Primary error".to_string(),
1613            "main.rs".to_string(),
1614            5,
1615            10,
1616            5,
1617            20,
1618            ErrorSeverity::Error,
1619        );
1620
1621        display.add_secondary_span(&mut error, 8, 5, 8, 15, "Related issue here".to_string());
1622
1623        let caret_positioning = error.caret_positioning.unwrap();
1624        assert_eq!(caret_positioning.secondary_spans.len(), 1);
1625
1626        let secondary = &caret_positioning.secondary_spans[0];
1627        assert_eq!(secondary.start_line, 8);
1628        assert_eq!(secondary.start_column, 5);
1629        assert_eq!(secondary.end_line, 8);
1630        assert_eq!(secondary.end_column, 15);
1631        assert_eq!(secondary.message, "Related issue here");
1632    }
1633
1634    #[test]
1635    fn test_enhanced_caret_formatting_single_line() {
1636        let mut config = ErrorDisplayConfig::default();
1637        config.syntax_highlighting.enabled = false; // Disable syntax highlighting for cleaner test assertions
1638        let display = ErrorDisplay::new("test".to_string(), config);
1639        let error = ErrorDisplay::create_multi_line_error(
1640            "Single line error".to_string(),
1641            "test.rs".to_string(),
1642            3,
1643            10,
1644            3,
1645            15,
1646            ErrorSeverity::Error,
1647        );
1648
1649        let content = "line 1\nline 2\nthis is line 3 with error\nline 4";
1650        let caret_positioning = error.caret_positioning.as_ref().unwrap();
1651        let formatted =
1652            display.format_error_with_multi_line_carets(&error, &content, caret_positioning);
1653
1654        assert!(formatted.contains("error: Single line error"));
1655        assert!(formatted.contains("--> test.rs:3:10"));
1656        assert!(formatted.contains("this is line 3 with error"));
1657        assert!(formatted.contains("^^^^^")); // 5 caret characters for positions 10-15
1658    }
1659
1660    #[test]
1661    fn test_enhanced_caret_formatting_multi_line() {
1662        let mut config = ErrorDisplayConfig::default();
1663        config.syntax_highlighting.enabled = false; // Disable syntax highlighting for cleaner test assertions
1664        let display = ErrorDisplay::new("test".to_string(), config);
1665        let error = ErrorDisplay::create_multi_line_error(
1666            "Multi-line error".to_string(),
1667            "test.rs".to_string(),
1668            2,
1669            5,
1670            4,
1671            10,
1672            ErrorSeverity::Warning,
1673        );
1674
1675        let content = "line 1\nstart error here\nmiddle line\nend error here\nline 5";
1676        let caret_positioning = error.caret_positioning.as_ref().unwrap();
1677        let formatted =
1678            display.format_error_with_multi_line_carets(&error, &content, caret_positioning);
1679
1680        assert!(formatted.contains("warning: Multi-line error"));
1681        assert!(formatted.contains("--> test.rs:2:5"));
1682        assert!(formatted.contains("start error here"));
1683        assert!(formatted.contains("middle line"));
1684        assert!(formatted.contains("end error here"));
1685        assert!(formatted.contains("┌")); // Multi-line start
1686        assert!(formatted.contains("│")); // Multi-line middle
1687        assert!(formatted.contains("└")); // Multi-line end
1688    }
1689
1690    #[test]
1691    fn test_enhanced_caret_with_secondary_spans() {
1692        let mut config = ErrorDisplayConfig::default();
1693        config.syntax_highlighting.enabled = false; // Disable syntax highlighting for cleaner test assertions
1694        let display = ErrorDisplay::new("test".to_string(), config);
1695        let mut error = ErrorDisplay::create_multi_line_error(
1696            "Primary error with context".to_string(),
1697            "lib.rs".to_string(),
1698            10,
1699            1,
1700            10,
1701            20,
1702            ErrorSeverity::Error,
1703        );
1704
1705        display.add_secondary_span(&mut error, 15, 5, 15, 12, "Related definition".to_string());
1706
1707        let content = (1..=20)
1708            .map(|i| format!("line {}", i))
1709            .collect::<Vec<_>>()
1710            .join("\n");
1711        let caret_positioning = error.caret_positioning.as_ref().unwrap();
1712        let formatted =
1713            display.format_error_with_multi_line_carets(&error, &content, caret_positioning);
1714
1715        assert!(formatted.contains("Primary error with context"));
1716        assert!(formatted.contains("line 10"));
1717        assert!(formatted.contains("Related definition"));
1718        assert!(formatted.contains("line 15"));
1719    }
1720
1721    #[test]
1722    fn test_enhanced_line_color_detection() {
1723        let display = ErrorDisplay::with_defaults("test".to_string());
1724        let caret_positioning = CaretPositioning::default();
1725
1726        // Test caret line detection
1727        let caret_line = format!("   | {}", caret_positioning.multi_line_start_char);
1728        let color =
1729            display.get_enhanced_line_color(&caret_line, &ErrorSeverity::Error, &caret_positioning);
1730        assert_eq!(color, "bright_red"); // Should use caret color
1731
1732        // Test normal line with line number format (contains " | ")
1733        let normal_line = "   | regular code line";
1734        let color = display.get_enhanced_line_color(
1735            &normal_line,
1736            &ErrorSeverity::Error,
1737            &caret_positioning,
1738        );
1739        assert_eq!(color, "bright_blue"); // Should use line_number_color for lines with " | "
1740    }
1741
1742    #[test]
1743    fn test_render_with_enhanced_carets() {
1744        let display = ErrorDisplay::with_defaults("test".to_string());
1745        let error = ErrorDisplay::create_multi_line_error(
1746            "Render test error".to_string(),
1747            "render.rs".to_string(),
1748            1,
1749            1,
1750            2,
1751            5,
1752            ErrorSeverity::Info,
1753        );
1754
1755        let content = "first line\nsecond line";
1756        let bounds = Bounds::new(0, 0, 80, 10);
1757        let mut buffer = create_test_buffer();
1758
1759        // Should not panic and should handle rendering gracefully
1760        display.render_with_enhanced_carets(&error, content, &bounds, &mut buffer);
1761
1762        // Test fallback to standard rendering when no caret positioning
1763        let standard_error = ErrorInfo {
1764            message: "Standard error".to_string(),
1765            file_path: "std.rs".to_string(),
1766            line_number: 1,
1767            column_number: 1,
1768            severity: ErrorSeverity::Error,
1769            help: None,
1770            note: None,
1771            caret_positioning: None,
1772        };
1773
1774        display.render_with_enhanced_carets(&standard_error, content, &bounds, &mut buffer);
1775    }
1776
1777    #[test]
1778    fn test_multi_line_span_rendering() {
1779        let display = ErrorDisplay::with_detailed_config("detailed_test".to_string());
1780        let span = ErrorSpan {
1781            start_line: 2,
1782            start_column: 10,
1783            end_line: 4,
1784            end_column: 5,
1785            message: "Test span".to_string(),
1786        };
1787
1788        let lines = vec![
1789            "line 1",
1790            "line 2 with error start",
1791            "line 3 middle",
1792            "line 4 error end",
1793            "line 5",
1794        ];
1795        let mut result = String::new();
1796
1797        display.render_multi_line_span(&mut result, &lines, &span, 3, true);
1798
1799        // Test content is present (may contain ANSI codes)
1800        assert!(
1801            result.contains("line")
1802                && result.contains("2")
1803                && result.contains("with")
1804                && result.contains("error")
1805                && result.contains("start")
1806        );
1807        assert!(result.contains("line") && result.contains("3") && result.contains("middle"));
1808        assert!(
1809            result.contains("line")
1810                && result.contains("4")
1811                && result.contains("error")
1812                && result.contains("end")
1813        );
1814        assert!(result.contains("┌")); // Start indicator
1815        assert!(result.contains("│")); // Middle indicator
1816        assert!(result.contains("└")); // End indicator
1817    }
1818}