Skip to content

Commit 079afeb

Browse files
committed
Update error.rs
fix: CRLF newline handling and error span rendering Previously, CRLF was treated as two newlines, causing incorrect line numbers, extra blank lines, and misaligned spans on Windows. - treat CRLF as a single newline - use consistent newline handling for line/column calculation and display - fix trailing space rendering on empty lines - preserve UTF-16 column alignment and span rendering - Handle lone CR for current lexer behavior.
1 parent e5296bf commit 079afeb

1 file changed

Lines changed: 188 additions & 20 deletions

File tree

src/error.rs

Lines changed: 188 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use std::sync::Arc;
55
use chumsky::error::Error as ChumskyError;
66
use chumsky::input::ValueInput;
77
use chumsky::label::LabelError;
8-
use chumsky::text::Char;
98
use chumsky::util::MaybeRef;
109
use chumsky::DefaultExpected;
1110

@@ -192,22 +191,67 @@ impl RichError {
192191

193192
impl fmt::Display for RichError {
194193
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195-
fn get_line_col(file: &str, offset: usize) -> (usize, usize) {
196-
let mut line = 1;
197-
let mut col = 0;
194+
fn next_newline(s: &str) -> Option<(usize, usize)> {
195+
let mut it = s.char_indices().peekable();
196+
197+
while let Some((i, ch)) = it.next() {
198+
if ch == '\r' {
199+
// Treat CRLF as one logical newline.
200+
if matches!(it.peek(), Some((_, c)) if *c == '\n') {
201+
it.next();
202+
return Some((i, '\r'.len_utf8() + '\n'.len_utf8()));
203+
}
198204

199-
let slice = file.get(0..offset).unwrap_or_default();
205+
// Support lone CR for compatibility with current lexer behavior.
206+
return Some((i, ch.len_utf8()));
207+
}
200208

201-
for char in slice.chars() {
202-
if char.is_newline() {
203-
line += 1;
204-
col = 0;
205-
} else {
206-
col += char.len_utf16();
209+
// Support LF newline.
210+
if ch == '\n' {
211+
return Some((i, ch.len_utf8()));
207212
}
213+
214+
// Unicode newline support.
215+
if ch == '\u{2028}' || ch == '\u{2029}' {
216+
return Some((i, ch.len_utf8()));
217+
}
218+
}
219+
220+
None
221+
}
222+
fn get_line_col(file: &str, offset: usize) -> (usize, usize) {
223+
let s = file.get(..offset).unwrap_or_default();
224+
let mut line = 1usize;
225+
let mut last_line_start = 0usize;
226+
let mut rest = s;
227+
let mut consumed = 0usize;
228+
229+
while let Some((i, nl_len)) = next_newline(rest) {
230+
line += 1;
231+
consumed += i + nl_len;
232+
last_line_start = consumed;
233+
rest = &rest[i + nl_len..];
234+
}
235+
236+
let col = 1 + s[last_line_start..]
237+
.chars()
238+
.map(char::len_utf16)
239+
.sum::<usize>();
240+
241+
(line, col)
242+
}
243+
244+
fn split_lines_preserving_crlf(file: &str) -> Vec<&str> {
245+
let mut out = Vec::new();
246+
let mut rest = file;
247+
248+
while let Some((i, nl_len)) = next_newline(rest) {
249+
out.push(&rest[..i]);
250+
rest = &rest[i + nl_len..];
208251
}
209252

210-
(line, col + 1)
253+
out.push(rest);
254+
out
211255
}
212256

213257
match self.file {
@@ -222,24 +266,28 @@ impl fmt::Display for RichError {
222266

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

225-
let mut lines = file
226-
.split(|c: char| c.is_newline())
227-
.skip(start_line_index)
228-
.peekable();
269+
let split_lines = split_lines_preserving_crlf(file);
270+
let mut lines = split_lines.into_iter().skip(start_line_index).peekable();
229271

230272
let start_line_len = lines
231273
.peek()
232-
.map_or(0, |l| l.chars().map(char::len_utf16).sum());
274+
.map_or(0, |l| l.chars().map(char::len_utf16).sum::<usize>());
233275

234276
for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
235277
let line_num = start_line_index + relative_line_index + 1;
236-
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
278+
write!(f, "{line_num:line_num_width$} |")?;
279+
if !line_str.is_empty() {
280+
write!(f, " {line_str}")?;
281+
}
282+
writeln!(f)?;
237283
}
238284

239285
let is_multiline = end_line > start_line;
240286

241287
let (underline_start, underline_length) = match is_multiline {
242-
true => (0, start_line_len),
288+
// For multiline spans, preserve the existing display style:
289+
// underline the full first displayed line.
290+
true => (1, start_line_len),
243291
false => (start_col, (end_col - start_col).max(1)),
244292
};
245293
write!(f, "{:width$} |", " ", width = line_num_width)?;
@@ -764,6 +812,81 @@ let x: u32 = Left(
764812
assert_eq!(&expected[1..], &error.to_string());
765813
}
766814

815+
#[test]
816+
fn display_with_windows_crlf_newlines() {
817+
let file = "let a: u8 = 65536;\r\nlet b: u8 = 0;";
818+
let error = Error::CannotParse("number too large to fit in target type".to_string())
819+
.with_span(Span::new(12, 17))
820+
.with_file(Arc::from(file));
821+
822+
let expected = r#"
823+
|
824+
1 | let a: u8 = 65536;
825+
| ^^^^^ Cannot parse: number too large to fit in target type"#;
826+
827+
assert_eq!(&expected[1..], &error.to_string());
828+
}
829+
830+
#[test]
831+
fn display_with_unix_lf_newlines() {
832+
let file = "let a: u8 = 65536;\nlet b: u8 = 0;";
833+
let error = Error::CannotParse("number too large to fit in target type".to_string())
834+
.with_span(Span::new(12, 17))
835+
.with_file(Arc::from(file));
836+
837+
let expected = r#"
838+
|
839+
1 | let a: u8 = 65536;
840+
| ^^^^^ Cannot parse: number too large to fit in target type"#;
841+
842+
assert_eq!(&expected[1..], &error.to_string());
843+
}
844+
845+
#[test]
846+
fn display_with_mixed_newlines_on_second_line() {
847+
let file = "line1\r\nline2\nline3";
848+
let error = Error::CannotParse("err".to_string())
849+
.with_span(Span::new(7, 12))
850+
.with_file(Arc::from(file));
851+
852+
let expected = r#"
853+
|
854+
2 | line2
855+
| ^^^^^ Cannot parse: err"#;
856+
857+
assert_eq!(&expected[1..], &error.to_string());
858+
}
859+
860+
#[test]
861+
fn display_does_not_insert_extra_blank_line_for_crlf() {
862+
let file = "a\r\nb";
863+
let error = Error::CannotParse("err".to_string())
864+
.with_span(Span::new(3, 4))
865+
.with_file(Arc::from(file));
866+
867+
let expected = r#"
868+
|
869+
2 | b
870+
| ^ Cannot parse: err"#;
871+
872+
assert_eq!(&expected[1..], &error.to_string());
873+
}
874+
875+
#[test]
876+
fn display_handles_utf16_columns_after_newline() {
877+
let file = "x\r\n😀ab";
878+
let error = Error::CannotParse("err".to_string())
879+
.with_span(Span::new(7, 9))
880+
.with_file(Arc::from(file));
881+
882+
let expected = r#"
883+
|
884+
2 | 😀ab
885+
| ^^ Cannot parse: err"#;
886+
887+
assert_eq!(&expected[1..], &error.to_string());
888+
}
889+
767890
#[test]
768891
fn display_span_as_point() {
769892
let file = "fn main()";
@@ -787,7 +910,52 @@ let x: u32 = Left(
787910

788911
let expected = r#"
789912
|
790-
3 |
913+
3 |
914+
| ^ Cannot parse: eof"#;
915+
916+
assert_eq!(&expected[1..], &error.to_string());
917+
}
918+
919+
#[test]
920+
fn display_zero_length_span_shows_single_caret() {
921+
let file = "let a: u8 = 1;";
922+
let error = Error::CannotParse("err".to_string())
923+
.with_span(Span::new(12, 12))
924+
.with_file(Arc::from(file));
925+
926+
let expected = r#"
927+
|
928+
1 | let a: u8 = 1;
929+
| ^ Cannot parse: err"#;
930+
931+
assert_eq!(&expected[1..], &error.to_string());
932+
}
933+
934+
#[test]
935+
fn display_with_cr_only_newlines() {
936+
let file = "let a: u8 = 0;\rlet b: u8 = 65536;";
937+
let error = Error::CannotParse("number too large to fit in target type".to_string())
938+
.with_span(Span::new(27, 32))
939+
.with_file(Arc::from(file));
940+
941+
let expected = r#"
942+
|
943+
2 | let b: u8 = 65536;
944+
| ^^^^^ Cannot parse: number too large to fit in target type"#;
945+
946+
assert_eq!(&expected[1..], &error.to_string());
947+
}
948+
949+
#[test]
950+
fn display_span_as_point_on_trailing_cr_only_empty_line() {
951+
let file = "fn main(){\r let a:\r";
952+
let error = Error::CannotParse("eof".to_string())
953+
.with_span(Span::new(file.len(), file.len()))
954+
.with_file(Arc::from(file));
955+
956+
let expected = r#"
957+
|
958+
3 |
791959
| ^ Cannot parse: eof"#;
792960

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

0 commit comments

Comments
 (0)