use crate::diagnostics::{Diagnostic, Severity}; use crate::lexer::{TokenSpan, TokenizedFile}; use core::convert::Into; use crossterm::style::Stylize; use std::cmp::max; use std::collections::HashMap; use std::ops::RangeInclusive; const INDENT: &str = " "; /* error[P0034]: missing `{` to start `switch` body in file: files/P0034_04.uc 1 | ╭switch | │------ `switch` starts here 2 | ╭ │( 3 | │ │ A 4 | │ │) | │ ╰─^ expected `{` before end of file | ╰ ^ switch selector is here Outmost guideline isn't reaching ^!. And should it be `^` even? */ /* error: expected one of `,`, `:`, or `}`, found `token_to` --> rottlib/src/ast/mod.rs:80:13 | 78 | Self { | ---- while parsing this struct 79 | token_from: self.token_from,scd | --- while parsing this struct field 80 | token_to: std::cmp::max(self.token_to, right_most_index), | ^^^^^^^^ expected one of `,`, `:`, or `}` */ /* | 76 | / "asdasdas 77 | | asd1 78 | | asd2 79 | | asdasd" | |___________________^ expected `()`, found `&str` */ /* 1. Get each span's range and total lines covered by spans as ranges; 2. We need `+N` more lines for `N` labels; 3. */ // TODO: check if blue guidelines are sometimes red or vice versa // TODO: tabs needs to be replaced with 1-width character // These are abstract rendering events, not self-contained draw commands. // They are emitted in increasing order of "significant lines" (range starts/ends). // The actual source span for a label is recovered later from its LabelType. #[derive(PartialEq, Eq, Clone, Copy)] enum RendererCommands { StartRange { label_type: LabelType, column: usize, }, FinishRange { label_type: LabelType, column: usize, }, SingleRange { label_type: LabelType, }, } enum LineIndexType { Normal(usize), Missing, Ellipsis, } // Label ordering is semantic: primary first, then secondaries in diagnostic order. // That order is also used to break visual ties when multiple labels would otherwise // start or end on the same source line. #[derive(PartialEq, Eq, Hash, Clone, Copy)] enum LabelType { Primary, Secondary(usize), } struct RangeSet { primary_range: Option>, secondary_ranges: Vec>, } impl RangeSet { /*fn get(&self, index: usize) -> Option<&RangeInclusive> { if self.primary_range.is_some() { if index == 0 { return self.primary_range.as_ref(); } else { self.secondary_ranges.get(index - 1) } } else { self.secondary_ranges.get(index) } } fn len(&self) -> usize { self.secondary_ranges.len() + if self.primary_range.is_some() { 1 } else { 0 } }*/ fn iter(&self) -> impl Iterator> { self.primary_range .iter() .chain(self.secondary_ranges.iter()) } fn iter_labeled(&self) -> impl Iterator)> { self.primary_range .iter() .map(|range| (LabelType::Primary, range)) .chain( self.secondary_ranges .iter() .enumerate() .map(|(index, range)| (LabelType::Secondary(index), range)), ) } fn get_first_bound_above(&self, line_number: Option) -> Option { self.iter() .filter_map(|range| { let start = *range.start(); let end = *range.end(); let start_ok = line_number.is_none_or(|n| start > n).then_some(start); let end_ok = line_number.is_none_or(|n| end > n).then_some(end); match (start_ok, end_ok) { (Some(a), Some(b)) => Some(a.min(b)), (Some(a), None) => Some(a), (None, Some(b)) => Some(b), (None, None) => None, } }) .min() } } // Converts labeled line ranges into an ordered stream of renderer events. // // Important invariants: // // 1. Commands are ordered by increasing significant line. // A significant line is any line on which some label starts or ends. // // 2. If multiple labels would visually terminate on the same source line, // the renderer treats them as ending on distinct phantom rows, ordered by // diagnostic priority (primary/secondary order). This prevents intersections // and means that same-line closings are intentionally linearized rather than // treated as a geometric tie. // // 3. RendererCommands do not store source line numbers directly. // Later rendering recovers the underlying span from LabelType and uses the // event order to know when labels become active/inactive. // // 4. When a label starts on the same significant line where another label ends, // starts are processed first. This is intentional: longer-lived/opening labels // must occupy earlier columns so that shorter-lived/closing labels bend around // them without intersecting. fn make_renderer_commands(ranges: RangeSet) -> Vec<(usize, RendererCommands)> { // Maps currently-open labels to the index of their StartRange command so that // we can patch in the final column once the label closes. let mut open_ranges = HashMap::new(); let mut commands = Vec::new(); let mut current_line = None; while let Some(next_significant_line) = ranges.get_first_bound_above(current_line) { current_line = Some(next_significant_line); // First process all new ranges because they'll live longer and have // to have earlier columns for (label, range) in ranges.iter_labeled() { if *range.start() == next_significant_line { if range.start() != range.end() { commands.push(( *range.start(), RendererCommands::StartRange { label_type: label, column: 0, }, )); open_ranges.insert(label, commands.len() - 1); } else { commands.push(( *range.start(), RendererCommands::SingleRange { label_type: label }, )); } } } // Closing pass. // The assigned column is the number of ranges that remain open after removing // this label. Because same-line visual ties are already linearized by label // priority / phantom rows, processing labels in iter_labeled() order is // intentional here. for (label, range) in ranges.iter_labeled() { if *range.end() == next_significant_line { if let Some(index) = open_ranges.remove(&label) { // Column meaning: // 0 = outermost / earliest lane // larger values = further inward lanes // // We assign the column at close time, not at open time, because the final lane // depends on which other ranges outlive this one. let column = open_ranges.len(); if let Some((line_number, RendererCommands::StartRange { .. })) = commands.get(index) { commands[index] = ( *line_number, RendererCommands::StartRange { label_type: label, column, }, ); } commands.push(( *range.end(), RendererCommands::FinishRange { label_type: label, column, }, )); } } } } commands } fn max_line_number_width(ranges: &RangeSet) -> usize { let max_line = ranges.iter().map(|range| *range.end()).max().unwrap_or(0); if max_line == 0 { 1 } else { max_line.ilog10() as usize + 1 } } fn span_to_range<'src>( span: TokenSpan, file: &TokenizedFile<'src>, ) -> Option> { let start_line = file.token_line(span.start)?; let end_line = file.token_line(span.end)?; if start_line <= end_line { Some(start_line..=end_line) } else { None } } fn make_ranges<'src>(file: &TokenizedFile<'src>, diagnostic: &Diagnostic) -> RangeSet { let mut result = RangeSet { primary_range: None, secondary_ranges: Vec::new(), }; result.primary_range = diagnostic .primary_label() .and_then(|label| span_to_range(label.span, file)); for secondary in diagnostic.secondary_labels() { if let Some(range) = span_to_range(secondary.span, file) { result.secondary_ranges.push(range); } } result } impl Diagnostic { pub fn render<'src>(&self, file: &TokenizedFile<'src>, file_path: impl Into) { self.render_header(); println!("{INDENT}{}: {}", "in file".blue().bold(), file_path.into()); self.render_lines(file); self.render_help_and_notes(file); } /*StartRange { label_type: LabelType, column: usize, }, FinishRange { label_type: LabelType, }, SingleRange { label_type: LabelType, }, */ fn label_data(&self, label_type: LabelType) -> Option<(TokenSpan, String)> { match label_type { LabelType::Primary => self .primary_label() .map(|label| (label.span, label.message.clone())), LabelType::Secondary(id) => Some(( self.secondary_labels()[id].span, self.secondary_labels()[id].message.clone(), )), } } fn render_lines<'src>(&self, file: &TokenizedFile<'src>) { let ranges = make_ranges(file, &self); let max_line_number_width = max(max_line_number_width(&ranges), 3); let commands = make_renderer_commands(ranges); let mut max_column = 0; for command in &commands { if let (_, RendererCommands::StartRange { column, .. }) = command { max_column = max(max_column, *column); } } let mut vertical_stack = Vec::new(); vertical_stack.resize(max_column + 1, None); let mut i = 0; while i < commands.len() { let mut current_line = commands[i].0; let mut single_commands = Vec::new(); let mut start_commands = Vec::new(); let mut finish_commands = Vec::new(); while i < commands.len() && current_line == commands[i].0 { match commands[i].1 { RendererCommands::SingleRange { label_type } => { single_commands.push(label_type) } RendererCommands::StartRange { label_type, column } => { start_commands.push((label_type, column)); } RendererCommands::FinishRange { label_type, column } => { finish_commands.push((label_type, column)) } } i += 1; } // !!!!!!!!!!!!!!!! // First - update line drawing stack // First - update line drawing stack for &(label_type, column) in &start_commands { vertical_stack[column] = Some(label_type); } // Next - draw the line self.draw_line_with_starts( current_line, max_line_number_width, file, &vertical_stack, &start_commands, ); for label_type in single_commands { self.render_single_command( label_type, max_line_number_width, file, &vertical_stack, ); } // Next - render finish commands (drop for now) for (label_type, column) in finish_commands { self.render_finish_command( label_type, max_line_number_width, file, &vertical_stack, ); vertical_stack[column] = None; } // !!!!!!!!!!!!!!!! // Render some more lines let mut countdown = 3; current_line += 1; while i < commands.len() && current_line < commands[i].0 { if countdown == 0 { if current_line + 1 == commands[i].0 { self.draw_line(current_line, max_line_number_width, file, &vertical_stack); } else { println!( "{}", self.make_line_prefix( LineIndexType::Ellipsis, max_line_number_width, &vertical_stack ) ); } break; } else { self.draw_line(current_line, max_line_number_width, file, &vertical_stack); } current_line += 1; countdown -= 1; } } } fn render_single_command<'src>( &self, label_type: LabelType, max_line_number_width: usize, file: &TokenizedFile<'src>, vertical_stack: &[Option], ) { let Some((span, message)) = self.label_data(label_type) else { return; }; let Some(visible) = file.span_visible_on_line(span) else { return; }; let mut builder = self.make_line_prefix( LineIndexType::Missing, max_line_number_width, vertical_stack, ); builder.push_str(&" ".repeat(visible.columns.start)); let underline_width = (visible.columns.end - visible.columns.start).max(1); let mut underline_label = if label_type == LabelType::Primary { "^".repeat(underline_width) } else { "-".repeat(underline_width) }; underline_label.push_str(&format!(" {}", message)); match label_type { LabelType::Primary => { if self.severity == Severity::Error { builder.push_str(&underline_label.red().bold().to_string()); } else { builder.push_str(&underline_label.yellow().bold().to_string()); } } LabelType::Secondary(_) => { builder.push_str(&underline_label.blue().bold().to_string()); } } println!("{builder}"); } fn render_finish_command<'src>( &self, label_type: LabelType, max_line_number_width: usize, file: &TokenizedFile<'src>, vertical_stack: &[Option], ) { let Some((span, message)) = self.label_data(label_type) else { return; }; let Some(visible) = file .token_visible_spans(span.end) .and_then(|spans| spans.into_iter().last()) else { return; }; let mut builder = self.make_finish_prefix(max_line_number_width, vertical_stack, label_type); builder.push_str(&"─".repeat(visible.columns.start).red().to_string()); let underline_width = (visible.columns.end - visible.columns.start).max(1); let mut underline_label = "^".repeat(underline_width); underline_label.push_str(&format!(" {}", message)); match label_type { LabelType::Primary => { if self.severity == Severity::Error { builder.push_str(&underline_label.red().bold().to_string()); } else { builder.push_str(&underline_label.yellow().bold().to_string()); } } LabelType::Secondary(_) => { builder.push_str(&underline_label.blue().bold().to_string()); } } println!("{builder}"); } fn draw_line<'src>( &self, current_line: usize, max_line_number_width: usize, file: &TokenizedFile<'src>, vertical_stack: &[Option], ) { println!( "{}{}", self.make_line_prefix( LineIndexType::Normal(current_line), max_line_number_width, vertical_stack ), file.line_text(current_line).unwrap_or_default() ); } fn draw_line_with_starts<'src>( &self, current_line: usize, max_line_number_width: usize, file: &TokenizedFile<'src>, vertical_stack: &[Option], start_commands: &[(LabelType, usize)], ) { println!( "{}{}", self.make_start_prefix( LineIndexType::Normal(current_line), max_line_number_width, vertical_stack, start_commands, ), file.line_text(current_line).unwrap_or_default() ); } fn make_line_prefix<'src>( &self, current_line: LineIndexType, max_line_number_width: usize, vertical_stack: &[Option], ) -> String { let line_text = match current_line { LineIndexType::Normal(current_line) => (current_line + 1).to_string(), LineIndexType::Missing => "".to_string(), LineIndexType::Ellipsis => "...".to_string(), }; let line_padding = " ".repeat(max_line_number_width - line_text.len()); let mut builder = format!(" {}{} | ", line_padding, line_text) .blue() .bold() .to_string(); for vertical_line in vertical_stack { if let Some(label) = vertical_line { let piece = match label { LabelType::Primary => { if self.severity == Severity::Error { " │".red() } else { " │".yellow() } } LabelType::Secondary(_) => " │".blue(), } .to_string(); builder.push_str(&piece); } else { builder.push_str(" "); } } builder } fn make_start_prefix( &self, current_line: LineIndexType, max_line_number_width: usize, vertical_stack: &[Option], start_commands: &[(LabelType, usize)], ) -> String { let line_text = match current_line { LineIndexType::Normal(current_line) => (current_line + 1).to_string(), LineIndexType::Missing => "".to_string(), LineIndexType::Ellipsis => "...".to_string(), }; let line_padding = " ".repeat(max_line_number_width - line_text.len()); let mut builder = format!(" {}{} | ", line_padding, line_text) .blue() .bold() .to_string(); for (column, vertical_line) in vertical_stack.iter().enumerate() { let piece = match vertical_line { Some(label) => { let starts_here = start_commands.iter().any(|(start_label, start_column)| { *start_label == *label && *start_column == column }); match label { LabelType::Primary => { if self.severity == Severity::Error { if starts_here { " ╭".red() } else { " │".red() } } else { if starts_here { " ╭".yellow() } else { " │".yellow() } } } LabelType::Secondary(_) => { if starts_here { " ╭".blue() } else { " │".blue() } } } .to_string() } None => " ".to_string(), }; builder.push_str(&piece); } builder } fn make_finish_prefix( &self, max_line_number_width: usize, vertical_stack: &[Option], finishing_label: LabelType, ) -> String { let line_text = ""; let line_padding = " ".repeat(max_line_number_width - line_text.len()); let mut builder = format!(" {}{} | ", line_padding, line_text) .blue() .bold() .to_string(); for vertical_line in vertical_stack { let piece = match vertical_line { Some(label) if *label == finishing_label => match label { LabelType::Primary => { if self.severity == Severity::Error { " ╰".red() } else { " ╰".yellow() } } LabelType::Secondary(_) => " ╰".blue(), } .to_string(), Some(label) => match label { LabelType::Primary => { if self.severity == Severity::Error { " │".red() } else { " │".yellow() } } LabelType::Secondary(_) => " │".blue(), } .to_string(), None => " ".to_string(), }; builder.push_str(&piece); } builder } fn render_header(&self) { let severity_label = match self.severity { Severity::Error => "error".red(), Severity::Warning => "warning".yellow(), }; if let Some(ref code) = self.code { println!( "{}", format!("{}[{}]: {}", severity_label, code, self.headline).bold() ); } else { println!( "{}", format!("{}: {}", severity_label, self.headline).bold() ); } } fn render_help_and_notes<'src>(&self, file: &TokenizedFile<'src>) { if self.help().is_none() && self.notes().is_empty() { return; } let ranges = make_ranges(file, self); let max_line_number_width = max(max_line_number_width(&ranges), 3); // Blank gutter separator, like rustc's trailing `|` println!( "{}", format!(" {} |", " ".repeat(max_line_number_width)) .blue() .bold() ); if let Some(help) = self.help() { self.render_trailer_line("help", help, max_line_number_width); } for note in self.notes() { self.render_trailer_line("note", note, max_line_number_width); } } fn render_trailer_line(&self, kind: &str, message: &str, max_line_number_width: usize) { let prefix = format!(" {} = ", " ".repeat(max_line_number_width)) .blue() .bold() .to_string(); let kind = match kind { "help" => "help".green().bold().to_string(), "note" => "note".blue().bold().to_string(), _ => kind.bold().to_string(), }; println!("{prefix}{kind}: {message}"); } }