use crate::{CompilerDiagnosticDetail, CompilerErrorOrDiagnostic, CompilerError};
const CODEFRAME_LINES_ABOVE: u32 = 2;
const CODEFRAME_LINES_BELOW: u32 = 3;
const CODEFRAME_MAX_LINES: u32 = 10;
const CODEFRAME_ABBREVIATED_SOURCE_LINES: usize = 5;
fn split_lines(source: &str) -> Vec<&str> {
let mut lines = Vec::new();
let mut start = 0;
let bytes = source.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
let ch = bytes[i];
if ch == b'\r' {
lines.push(&source[start..i]);
if i + 1 < len && bytes[i + 1] == b'\n' {
i += 2;
} else {
i += 1;
}
start = i;
} else if ch == b'\n' {
lines.push(&source[start..i]);
i += 1;
start = i;
} else {
if ch == 0xE2 && i + 2 < len && bytes[i + 1] == 0x80
&& (bytes[i + 2] == 0xA8 || bytes[i + 2] == 0xA9)
{
lines.push(&source[start..i]);
i += 3;
start = i;
} else {
i += 1;
}
}
}
lines.push(&source[start..]);
lines
}
#[derive(Clone, Debug)]
enum MarkerEntry {
WholeLine,
Range(usize, usize), }
fn get_marker_lines(
start_line: u32,
start_column: u32, end_line: u32,
end_column: u32, source_line_count: usize,
lines_above: u32,
lines_below: u32,
) -> (usize, usize, Vec<(usize, MarkerEntry)>) {
let start_line = start_line as usize;
let end_line = end_line as usize;
let start_column = start_column as usize;
let end_column = end_column as usize;
let start = if start_line > (lines_above as usize + 1) {
start_line - (lines_above as usize + 1)
} else {
0
};
let end = std::cmp::min(source_line_count, end_line + lines_below as usize);
let line_diff = end_line - start_line;
let mut marker_lines: Vec<(usize, MarkerEntry)> = Vec::new();
if line_diff > 0 {
for i in 0..=line_diff {
let line_number = i + start_line;
if start_column == 0 {
marker_lines.push((line_number, MarkerEntry::WholeLine));
} else if i == 0 {
marker_lines.push((line_number, MarkerEntry::Range(start_column, 0))); } else if i == line_diff {
marker_lines.push((line_number, MarkerEntry::Range(0, end_column)));
} else {
marker_lines.push((line_number, MarkerEntry::Range(0, 0))); }
}
} else {
if start_column == end_column {
if start_column != 0 {
marker_lines.push((start_line, MarkerEntry::Range(start_column, 0)));
} else {
marker_lines.push((start_line, MarkerEntry::WholeLine));
}
} else {
marker_lines.push((
start_line,
MarkerEntry::Range(start_column, end_column - start_column),
));
}
}
(start, end, marker_lines)
}
pub fn code_frame_columns(
source: &str,
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
message: &str,
) -> String {
let start_column_1 = start_col + 1;
let end_column_1 = end_col + 1;
let lines = split_lines(source);
let source_line_count = lines.len();
let (start, end, marker_lines_raw) = get_marker_lines(
start_line,
start_column_1,
end_line,
end_column_1,
source_line_count,
CODEFRAME_LINES_ABOVE,
CODEFRAME_LINES_BELOW,
);
let has_columns = start_column_1 > 0;
let number_max_width = format!("{}", end).len();
let mut marker_map: std::collections::HashMap<usize, MarkerEntry> = std::collections::HashMap::new();
let line_diff = end_line as usize - start_line as usize;
for (line_number, entry) in marker_lines_raw {
let resolved = match &entry {
MarkerEntry::Range(col, len) => {
if line_diff > 0 {
let i = line_number - start_line as usize;
if i == 0 && *len == 0 {
let source_length = if line_number >= 1 && line_number <= lines.len() {
lines[line_number - 1].len()
} else {
0
};
MarkerEntry::Range(*col, source_length.saturating_sub(*col) + 1)
} else if i > 0 && i < line_diff && *col == 0 && *len == 0 {
let source_length = if (start_line as usize) < lines.len() {
lines[start_line as usize].len()
} else {
0
};
MarkerEntry::Range(0, source_length)
} else {
entry
}
} else {
entry
}
}
_ => entry,
};
marker_map.insert(line_number, resolved);
}
let mut frame_parts: Vec<String> = Vec::new();
let display_lines = &lines[start..end];
for (index, line) in display_lines.iter().enumerate() {
let number = start + 1 + index;
let number_str = format!("{}", number);
let padded_number = if number_str.len() >= number_max_width {
number_str
} else {
let padding = " ".repeat(number_max_width - number_str.len());
format!("{}{}", padding, number_str)
};
let gutter = format!(" {} |", padded_number);
let has_marker = marker_map.get(&number);
let has_next_marker = marker_map.contains_key(&(number + 1));
let last_marker_line = has_marker.is_some() && !has_next_marker;
if let Some(marker_entry) = has_marker {
let line_content = if line.is_empty() {
String::new()
} else {
format!(" {}", line)
};
let marker_line_str = match marker_entry {
MarkerEntry::Range(col, len) => {
let max_col = if *col > 0 { col - 1 } else { 0 };
let byte_end = std::cmp::min(max_col, line.len());
let safe_end = if byte_end < line.len() && !line.is_char_boundary(byte_end) {
line.floor_char_boundary(byte_end)
} else {
byte_end
};
let prefix = &line[..safe_end];
let marker_spacing: String = prefix
.chars()
.map(|c| if c == '\t' { '\t' } else { ' ' })
.collect();
let number_of_markers = if *len == 0 { 1 } else { *len };
let carets = "^".repeat(number_of_markers);
let gutter_spaces = gutter.replace(|c: char| c.is_ascii_digit(), " ");
let mut marker_str = format!(
"\n {} {}{}",
gutter_spaces, marker_spacing, carets
);
if last_marker_line && !message.is_empty() {
marker_str.push(' ');
marker_str.push_str(message);
}
marker_str
}
MarkerEntry::WholeLine => String::new(),
};
frame_parts.push(format!(">{}{}{}", gutter, line_content, marker_line_str));
} else {
let line_content = if line.is_empty() {
String::new()
} else {
format!(" {}", line)
};
frame_parts.push(format!(" {}{}", gutter, line_content));
}
}
let mut frame = frame_parts.join("\n");
if !message.is_empty() && !has_columns {
frame = format!(
"{}{}\n{}",
" ".repeat(number_max_width + 1),
message,
frame
);
}
frame
}
pub fn print_code_frame(
source: &str,
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
message: &str,
) -> String {
let printed = code_frame_columns(source, start_line, start_col, end_line, end_col, message);
if end_line - start_line < CODEFRAME_MAX_LINES {
return printed;
}
let lines: Vec<&str> = printed.split('\n').collect();
let head_count = CODEFRAME_LINES_ABOVE as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES;
let tail_count = CODEFRAME_LINES_BELOW as usize + CODEFRAME_ABBREVIATED_SOURCE_LINES;
if lines.len() <= head_count + tail_count {
return printed;
}
let pipe_index = lines[0].find('|').unwrap_or(0);
let tail_start = lines.len() - tail_count;
let mut parts: Vec<String> = Vec::new();
for line in &lines[..head_count] {
parts.push(line.to_string());
}
parts.push(format!("{}\u{2026}", " ".repeat(pipe_index)));
for line in &lines[tail_start..] {
parts.push(line.to_string());
}
parts.join("\n")
}
use crate::format_category_heading;
pub fn format_compiler_error(
err: &CompilerError,
source: &str,
filename: Option<&str>,
) -> String {
let detail_messages: Vec<String> = err
.details
.iter()
.map(|d| format_error_detail(d, source, filename))
.collect();
let count = err.details.len();
let plural = if count == 1 { "" } else { "s" };
let header = format!("Found {} error{}:\n\n", count, plural);
let trimmed: Vec<String> = detail_messages.iter().map(|m| m.trim().to_string()).collect();
format!("{}{}", header, trimmed.join("\n\n"))
}
fn format_error_detail(
detail: &CompilerErrorOrDiagnostic,
source: &str,
filename: Option<&str>,
) -> String {
match detail {
CompilerErrorOrDiagnostic::Diagnostic(d) => {
let heading = format_category_heading(d.category);
let mut buffer = vec![format!("{}: {}", heading, d.reason)];
if let Some(ref description) = d.description {
buffer.push(format!("\n\n{}.", description));
}
for item in &d.details {
match item {
CompilerDiagnosticDetail::Error { loc, message, .. } => {
if let Some(loc) = loc {
let frame = print_code_frame(
source,
loc.start.line,
loc.start.column,
loc.end.line,
loc.end.column,
message.as_deref().unwrap_or(""),
);
buffer.push("\n\n".to_string());
if let Some(fname) = filename {
buffer.push(format!(
"{}:{}:{}\n",
fname, loc.start.line, loc.start.column
));
}
buffer.push(frame);
}
}
CompilerDiagnosticDetail::Hint { message } => {
buffer.push("\n\n".to_string());
buffer.push(message.clone());
}
}
}
buffer.join("")
}
CompilerErrorOrDiagnostic::ErrorDetail(d) => {
let heading = format_category_heading(d.category);
let mut buffer = vec![format!("{}: {}", heading, d.reason)];
if let Some(ref description) = d.description {
buffer.push(format!("\n\n{}.", description));
if let Some(ref loc) = d.loc {
let frame = print_code_frame(
source,
loc.start.line,
loc.start.column,
loc.end.line,
loc.end.column,
&d.reason,
);
buffer.push("\n\n".to_string());
if let Some(fname) = filename {
buffer.push(format!(
"{}:{}:{}\n",
fname, loc.start.line, loc.start.column
));
}
buffer.push(frame);
buffer.push("\n\n".to_string());
}
} else if let Some(ref loc) = d.loc {
let frame = print_code_frame(
source,
loc.start.line,
loc.start.column,
loc.end.line,
loc.end.column,
&d.reason,
);
buffer.push("\n\n".to_string());
if let Some(fname) = filename {
buffer.push(format!(
"{}:{}:{}\n",
fname, loc.start.line, loc.start.column
));
}
buffer.push(frame);
buffer.push("\n\n".to_string());
}
buffer.join("")
}
}
}