Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 181 additions & 16 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,22 +192,62 @@ impl RichError {

impl fmt::Display for RichError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn next_newline(s: &str) -> Option<(usize, usize)> {
let mut it = s.char_indices().peekable();

while let Some((i, ch)) = it.next() {
// Treat CRLF as one logical newline, while still recognizing
// standalone CR/LF and Unicode newline characters.
if ch == '\r' {
let len = if matches!(it.peek(), Some((_, c)) if *c == '\n') {
ch.len_utf8() + '\n'.len_utf8()
} else {
ch.len_utf8()
};
return Some((i, len));
}

if ch.is_newline() {
return Some((i, ch.len_utf8()));
}
}

None
}

fn get_line_col(file: &str, offset: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 0;
let s = file.get(..offset).unwrap_or_default();
let mut line = 1usize;
let mut last_line_start = 0usize;
let mut rest = s;
let mut consumed = 0usize;

while let Some((i, nl_len)) = next_newline(rest) {
line += 1;
consumed += i + nl_len;
last_line_start = consumed;
rest = &rest[i + nl_len..];
}

let slice = file.get(0..offset).unwrap_or_default();
let col = 1 + s[last_line_start..]
.chars()
.map(char::len_utf16)
.sum::<usize>();

for char in slice.chars() {
if char.is_newline() {
line += 1;
col = 0;
} else {
col += char.len_utf16();
}
(line, col)
}

fn split_lines_preserving_crlf(file: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut rest = file;

while let Some((i, nl_len)) = next_newline(rest) {
out.push(&rest[..i]);
rest = &rest[i + nl_len..];
}

(line, col + 1)
out.push(rest);
out
}

match self.file {
Expand All @@ -222,8 +262,12 @@ impl fmt::Display for RichError {

writeln!(f, "{:width$} |", " ", width = line_num_width)?;

let mut lines = file.lines().skip(start_line_index).peekable();
let start_line_len = lines.peek().map_or(0, |l| l.len());
let split_lines = split_lines_preserving_crlf(file);
let mut lines = split_lines.into_iter().skip(start_line_index).peekable();

let start_line_len = lines
.peek()
.map_or(0, |l| l.chars().map(char::len_utf16).sum::<usize>());

for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
let line_num = start_line_index + relative_line_index + 1;
Expand All @@ -233,8 +277,10 @@ impl fmt::Display for RichError {
let is_multiline = end_line > start_line;

let (underline_start, underline_length) = match is_multiline {
true => (0, start_line_len),
false => (start_col, end_col - start_col),
// For multiline spans, preserve the existing display style:
// underline the full first displayed line.
true => (1, start_line_len),
false => (start_col, (end_col - start_col).max(1)),
};
write!(f, "{:width$} |", " ", width = line_num_width)?;
write!(f, "{:width$}", " ", width = underline_start)?;
Expand Down Expand Up @@ -717,7 +763,126 @@ let x: u32 = Left(
1 | /*😀*/ let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;

println!("{error}");
assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_with_windows_crlf_newlines() {
let file = "let a: u8 = 65536;\r\nlet b: u8 = 0;";
let error = Error::CannotParse("number too large to fit in target type".to_string())
.with_span(Span::new(12, 17))
.with_file(Arc::from(file));

let expected = r#"
|
1 | let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_with_unix_lf_newlines() {
let file = "let a: u8 = 65536;\nlet b: u8 = 0;";
let error = Error::CannotParse("number too large to fit in target type".to_string())
.with_span(Span::new(12, 17))
.with_file(Arc::from(file));

let expected = r#"
|
1 | let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_with_cr_only_newlines() {
let file = "let a: u8 = 65536;\rlet b: u8 = 0;";
let error = Error::CannotParse("number too large to fit in target type".to_string())
.with_span(Span::new(12, 17))
.with_file(Arc::from(file));

let expected = r#"
|
1 | let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_with_unicode_newlines() {
let file = "let a: u8 = 65536;\u{2028}let b: u8 = 0;";
let error = Error::CannotParse("number too large to fit in target type".to_string())
.with_span(Span::new(12, 17))
.with_file(Arc::from(file));

let expected = r#"
|
1 | let a: u8 = 65536;
| ^^^^^ Cannot parse: number too large to fit in target type"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_with_mixed_newlines_on_second_line() {
let file = "line1\r\nline2\nline3";
let error = Error::CannotParse("err".to_string())
.with_span(Span::new(7, 12))
.with_file(Arc::from(file));

let expected = r#"
|
2 | line2
| ^^^^^ Cannot parse: err"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_does_not_insert_extra_blank_line_for_crlf() {
let file = "a\r\nb";
let error = Error::CannotParse("err".to_string())
.with_span(Span::new(3, 4))
.with_file(Arc::from(file));

let expected = r#"
|
2 | b
| ^ Cannot parse: err"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_handles_utf16_columns_after_newline() {
let file = "x\r\n😀ab";
let error = Error::CannotParse("err".to_string())
.with_span(Span::new(7, 9))
.with_file(Arc::from(file));

let expected = r#"
|
2 | 😀ab
| ^^ Cannot parse: err"#;

assert_eq!(&expected[1..], &error.to_string());
}

#[test]
fn display_zero_length_span_shows_single_caret() {
let file = "let a: u8 = 1;";
let error = Error::CannotParse("err".to_string())
.with_span(Span::new(12, 12))
.with_file(Arc::from(file));

let expected = r#"
|
1 | let a: u8 = 1;
| ^ Cannot parse: err"#;

assert_eq!(&expected[1..], &error.to_string());
}
}