rott/rottlib/src/diagnostics/render.rs

733 lines
24 KiB
Rust

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<RangeInclusive<usize>>,
secondary_ranges: Vec<RangeInclusive<usize>>,
}
impl RangeSet {
/*fn get(&self, index: usize) -> Option<&RangeInclusive<usize>> {
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<Item = &RangeInclusive<usize>> {
self.primary_range
.iter()
.chain(self.secondary_ranges.iter())
}
fn iter_labeled(&self) -> impl Iterator<Item = (LabelType, &RangeInclusive<usize>)> {
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<usize>) -> Option<usize> {
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<RangeInclusive<usize>> {
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<String>) {
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<LabelType>],
) {
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<LabelType>],
) {
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<LabelType>],
) {
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<LabelType>],
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<LabelType>],
) -> 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<LabelType>],
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<LabelType>],
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}");
}
}