use crate::error::terminal::{ColorScheme, TerminalFormatter};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Span {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LineCol {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnhancedSpan {
pub span: Span,
pub start_pos: LineCol,
pub end_pos: LineCol,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextWindow {
pub lines: Vec<String>,
pub error_line: usize,
pub error_column: usize,
pub error_length: usize,
}
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Span { start, end }
}
pub fn single(position: usize) -> Self {
Span {
start: position,
end: position + 1,
}
}
#[inline(always)]
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.start >= self.end
}
pub fn contains(&self, position: usize) -> bool {
position >= self.start && position < self.end
}
pub fn merge(&self, other: &Span) -> Span {
Span {
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
pub fn to_enhanced(&self, input: &str) -> EnhancedSpan {
let start_pos = byte_to_line_col(input, self.start);
let end_pos = byte_to_line_col(input, self.end);
EnhancedSpan {
span: *self,
start_pos,
end_pos,
}
}
pub fn extract<'a>(&self, input: &'a str) -> &'a str {
let start = self.start.min(input.len());
let end = self.end.min(input.len());
&input[start..end]
}
pub fn context_window(&self, input: &str, context_size: usize) -> ContextWindow {
let start_line = byte_to_line_col(input, self.start);
let end_line = byte_to_line_col(input, self.end);
let lines: Vec<&str> = input.lines().collect();
let context_start = start_line.line.saturating_sub(context_size + 1);
let context_end = (end_line.line + context_size).min(lines.len());
let context_lines: Vec<String> = lines[context_start..context_end]
.iter()
.map(|s| s.to_string())
.collect();
ContextWindow {
lines: context_lines,
error_line: start_line.line - context_start - 1,
error_column: start_line.column,
error_length: self.len(),
}
}
}
impl LineCol {
pub fn new(line: usize, column: usize) -> Self {
LineCol { line, column }
}
pub fn start() -> Self {
LineCol { line: 1, column: 1 }
}
}
impl EnhancedSpan {
pub fn new(span: Span, start_pos: LineCol, end_pos: LineCol) -> Self {
EnhancedSpan {
span,
start_pos,
end_pos,
}
}
pub fn location_string(&self) -> String {
if self.start_pos.line == self.end_pos.line {
format!("{}:{}", self.start_pos.line, self.start_pos.column)
} else {
format!(
"{}:{}-{}:{}",
self.start_pos.line, self.start_pos.column, self.end_pos.line, self.end_pos.column
)
}
}
}
impl ContextWindow {
pub fn format_with_highlight(&self) -> String {
let mut result = String::new();
for (i, line) in self.lines.iter().enumerate() {
result.push_str(&format!("{:4} | ", i + 1));
if i == self.error_line {
let before = &line[..self.error_column.saturating_sub(1)];
let error_start = self.error_column.saturating_sub(1);
let error_end = (error_start + self.error_length).min(line.len());
let error_text = &line[error_start..error_end];
let after = &line[error_end..];
result.push_str(before);
result.push_str(&format!(">>{error_text}<<")); result.push_str(after);
result.push('\n');
result.push_str(" | ");
result.push_str(&" ".repeat(self.error_column.saturating_sub(1)));
result.push_str(&"^".repeat(self.error_length.max(1)));
result.push('\n');
} else {
result.push_str(line);
result.push('\n');
}
}
result
}
pub fn format_with_formatter(&self, formatter: &TerminalFormatter) -> String {
let mut result = String::new();
for (i, line) in self.lines.iter().enumerate() {
let line_num = i + 1;
if i == self.error_line {
let before = &line[..self.error_column.saturating_sub(1)];
let error_start = self.error_column.saturating_sub(1);
let error_end = (error_start + self.error_length).min(line.len());
let error_text = &line[error_start..error_end];
let after = &line[error_end..];
result.push_str(&formatter.format_line_number(line_num));
result.push_str(before);
result.push_str(&formatter.colorize(error_text, ColorScheme::Error));
result.push_str(after);
result.push('\n');
result.push_str(
&formatter.format_error_pointer(self.error_column, self.error_length.max(1)),
);
result.push('\n');
} else {
result.push_str(&formatter.format_line_number(line_num));
result.push_str(line);
result.push('\n');
}
}
result
}
pub fn format_colored(&self) -> String {
let formatter = TerminalFormatter::new();
self.format_with_formatter(&formatter)
}
pub fn format_plain(&self) -> String {
let formatter = TerminalFormatter::without_colors();
self.format_with_formatter(&formatter)
}
}
fn byte_to_line_col(input: &str, byte_pos: usize) -> LineCol {
let mut line = 1;
let mut column = 1;
let mut current_pos = 0;
for ch in input.chars() {
if current_pos >= byte_pos {
break;
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
current_pos += ch.len_utf8();
}
LineCol { line, column }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_creation() {
let span = Span::new(5, 10);
assert_eq!(span.start, 5);
assert_eq!(span.end, 10);
assert_eq!(span.len(), 5);
assert!(!span.is_empty());
}
#[test]
fn test_span_single() {
let span = Span::single(5);
assert_eq!(span.start, 5);
assert_eq!(span.end, 6);
assert_eq!(span.len(), 1);
}
#[test]
fn test_span_contains() {
let span = Span::new(5, 10);
assert!(span.contains(5));
assert!(span.contains(7));
assert!(!span.contains(10));
assert!(!span.contains(3));
}
#[test]
fn test_span_merge() {
let span1 = Span::new(5, 10);
let span2 = Span::new(8, 15);
let merged = span1.merge(&span2);
assert_eq!(merged.start, 5);
assert_eq!(merged.end, 15);
}
#[test]
fn test_byte_to_line_col() {
let input = "hello\nworld\nfoo";
assert_eq!(byte_to_line_col(input, 0), LineCol::new(1, 1));
assert_eq!(byte_to_line_col(input, 5), LineCol::new(1, 6)); assert_eq!(byte_to_line_col(input, 6), LineCol::new(2, 1)); assert_eq!(byte_to_line_col(input, 12), LineCol::new(3, 1)); }
#[test]
fn test_enhanced_span() {
let input = "hello\nworld\nfoo";
let span = Span::new(6, 11); let enhanced = span.to_enhanced(input);
assert_eq!(enhanced.start_pos, LineCol::new(2, 1));
assert_eq!(enhanced.end_pos, LineCol::new(2, 6));
assert_eq!(enhanced.location_string(), "2:1");
}
#[test]
fn test_span_extract() {
let input = "hello world";
let span = Span::new(6, 11); assert_eq!(span.extract(input), "world");
}
}