1use crate::components::ComponentDimensions;
2use crate::draw_utils::print_with_color_and_background_at;
3use crate::model::common::{Bounds, ScreenBuffer};
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum ErrorSeverity {
8 Error,
10 Warning,
12 Info,
14 Hint,
16}
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum SyntaxToken {
21 Keyword,
23 String,
25 Number,
27 Comment,
29 Function,
31 Type,
33 Variable,
35 Operator,
37 Punctuation,
39 Text,
41}
42
43#[derive(Debug, Clone)]
45pub struct SyntaxHighlightConfig {
46 pub enabled: bool,
48 pub keyword_color: String,
50 pub string_color: String,
52 pub number_color: String,
54 pub comment_color: String,
56 pub function_color: String,
58 pub type_color: String,
60 pub variable_color: String,
62 pub operator_color: String,
64 pub punctuation_color: String,
66 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#[derive(Debug, Clone)]
90pub struct ErrorDisplayConfig {
91 pub error_color: String,
93 pub warning_color: String,
95 pub info_color: String,
97 pub hint_color: String,
99 pub line_number_color: String,
101 pub caret_color: String,
103 pub file_path_color: String,
105 pub border_color: String,
107 pub background_color: String,
109 pub show_context: bool,
111 pub context_lines: usize,
113 pub caret_char: char,
115 pub continuation_char: char,
117 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 pub fn terminal() -> Self {
145 Self {
146 show_context: false,
147 context_lines: 1,
148 ..Default::default()
149 }
150 }
151
152 pub fn detailed() -> Self {
154 Self {
155 show_context: true,
156 context_lines: 3,
157 ..Default::default()
158 }
159 }
160
161 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#[derive(Debug, Clone)]
174pub struct ErrorInfo {
175 pub message: String,
177 pub file_path: String,
179 pub line_number: usize,
181 pub column_number: usize,
183 pub severity: ErrorSeverity,
185 pub help: Option<String>,
187 pub note: Option<String>,
189 pub caret_positioning: Option<CaretPositioning>,
191}
192
193#[derive(Debug, Clone)]
195pub struct ErrorSpan {
196 pub start_line: usize,
198 pub start_column: usize,
200 pub end_line: usize,
202 pub end_column: usize,
204 pub message: String,
206}
207
208#[derive(Debug, Clone)]
210pub struct CaretPositioning {
211 pub primary_span: ErrorSpan,
213 pub secondary_spans: Vec<ErrorSpan>,
215 pub show_multi_line_carets: bool,
217 pub show_line_continuations: bool,
219 pub multi_line_start_char: char,
221 pub multi_line_middle_char: char,
223 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
247pub struct ErrorDisplay {
249 pub id: String,
251 pub config: ErrorDisplayConfig,
253}
254
255impl ErrorDisplay {
256 pub fn new(id: String, config: ErrorDisplayConfig) -> Self {
258 Self { id, config }
259 }
260
261 pub fn with_defaults(id: String) -> Self {
263 Self::new(id, ErrorDisplayConfig::default())
264 }
265
266 pub fn with_terminal_config(id: String) -> Self {
268 Self::new(id, ErrorDisplayConfig::terminal())
269 }
270
271 pub fn with_detailed_config(id: String) -> Self {
273 Self::new(id, ErrorDisplayConfig::detailed())
274 }
275
276 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 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 pub fn format_error(&self, error: &ErrorInfo, content: &str) -> String {
294 let lines: Vec<&str> = content.lines().collect();
295
296 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 result.push_str(&format!(
308 "{}: {}\n",
309 self.severity_text(&error.severity),
310 error.message
311 ));
312
313 result.push_str(&format!(
315 " --> {}:{}:{}\n",
316 error.file_path, error.line_number, error.column_number
317 ));
318
319 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 result.push_str(&format!("{}|\n", " ".repeat(line_num_width + 1)));
338
339 result.push_str(&format!(
341 "{:width$} | {}\n",
342 error.line_number,
343 error_line,
344 width = line_num_width
345 ));
346
347 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 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 if let Some(help) = &error.help {
372 result.push_str(&format!("\nhelp: {}\n", help));
373 }
374
375 if let Some(note) = &error.note {
377 result.push_str(&format!("\nnote: {}\n", note));
378 }
379
380 result
381 }
382
383 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 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 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 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 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 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 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 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 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 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 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 if remaining.starts_with("//") || remaining.starts_with("#") {
507 tokens.push((SyntaxToken::Comment, &line[current_pos..]));
508 break;
509 }
510
511 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 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 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 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 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 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", }
589 }
590
591 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 fn find_string_end(&self, chars: &[char], start: usize) -> usize {
602 let mut pos = start + 1; 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() }
611
612 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 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 fn find_operator_end(&self, chars: &[char], start: usize) -> usize {
634 let pos = start + 1;
635 let first_char = chars[start];
636
637 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 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, _ => SyntaxToken::Variable,
666 }
667 }
668
669 fn classify_rust_word(&self, word: &str) -> SyntaxToken {
671 match word {
672 "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 "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 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 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 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 pub fn format_error_with_highlighting(&self, error: &ErrorInfo, content: &str) -> String {
753 let lines: Vec<&str> = content.lines().collect();
754
755 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 result.push_str(&format!(
767 "{}: {}\n",
768 self.severity_text(&error.severity),
769 error.message
770 ));
771
772 result.push_str(&format!(
774 " --> {}:{}:{}\n",
775 error.file_path, error.line_number, error.column_number
776 ));
777
778 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 result.push_str(&format!("{}|\n", " ".repeat(line_num_width + 1)));
798
799 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 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 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 if let Some(help) = &error.help {
834 result.push_str(&format!("\nhelp: {}\n", help));
835 }
836
837 if let Some(note) = &error.note {
839 result.push_str(&format!("\nnote: {}\n", note));
840 }
841
842 result
843 }
844
845 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 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 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 self.config.line_number_color.clone()
887 }
888 } else {
889 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 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 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 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 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 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 self.render(error, content, bounds, buffer);
975 }
976 }
977
978 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 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 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 self.render_multi_line_span(
1013 &mut result,
1014 &lines,
1015 &caret_positioning.primary_span,
1016 line_number_width,
1017 true, );
1019
1020 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, );
1030 }
1031
1032 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 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 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 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 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 if span.start_line == span.end_line {
1092 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 let spaces_before_pipe = " ".repeat(line_number_width);
1108
1109 if line_idx == start_line {
1110 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 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 result.push_str(&format!("{} | │\n", spaces_before_pipe));
1141 }
1142 }
1143 } else {
1144 result.push_str(&format!("{} | {}\n", line_num_str, highlighted_content));
1146 }
1147 }
1148 }
1149
1150 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 fn get_enhanced_line_color(
1162 &self,
1163 line: &str,
1164 severity: &ErrorSeverity,
1165 caret_positioning: &CaretPositioning,
1166 ) -> String {
1167 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 self.get_line_color(line, severity)
1177 }
1178
1179 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 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 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 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 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 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"); }
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); }
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[")); assert!(highlighted.contains("fn")); }
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 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); }
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()); }
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); }
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); }
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); }
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); }
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 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 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 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; 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("^^^^^")); }
1659
1660 #[test]
1661 fn test_enhanced_caret_formatting_multi_line() {
1662 let mut config = ErrorDisplayConfig::default();
1663 config.syntax_highlighting.enabled = false; 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("┌")); assert!(formatted.contains("│")); assert!(formatted.contains("└")); }
1689
1690 #[test]
1691 fn test_enhanced_caret_with_secondary_spans() {
1692 let mut config = ErrorDisplayConfig::default();
1693 config.syntax_highlighting.enabled = false; 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 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"); 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"); }
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 display.render_with_enhanced_carets(&error, content, &bounds, &mut buffer);
1761
1762 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 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("┌")); assert!(result.contains("│")); assert!(result.contains("└")); }
1818}