Skip to content

Commit ea07842

Browse files
committed
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
1 parent e5296bf commit ea07842

File tree

1 file changed

+148
-19
lines changed

1 file changed

+148
-19
lines changed

src/error.rs

Lines changed: 148 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,57 @@ impl RichError {
192192

193193
impl fmt::Display for RichError {
194194
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;
195+
fn next_newline(s: &str) -> Option<(usize, usize)> {
196+
let mut it = s.char_indices().peekable();
198197

199-
let slice = file.get(0..offset).unwrap_or_default();
198+
while let Some((i, ch)) = it.next() {
199+
// Treat CRLF as one logical newline.
200+
if ch == '\r' && matches!(it.peek(), Some((_, c)) if *c == '\n') {
201+
return Some((i, ch.len_utf8() + '\n'.len_utf8()));
202+
}
200203

201-
for char in slice.chars() {
202-
if char.is_newline() {
203-
line += 1;
204-
col = 0;
205-
} else {
206-
col += char.len_utf16();
204+
// Preserve support for LF and Unicode newline characters.
205+
if ch.is_newline() {
206+
return Some((i, ch.len_utf8()));
207207
}
208208
}
209209

210-
(line, col + 1)
210+
None
211+
}
212+
213+
fn get_line_col(file: &str, offset: usize) -> (usize, usize) {
214+
let s = file.get(..offset).unwrap_or_default();
215+
let mut line = 1usize;
216+
let mut last_line_start = 0usize;
217+
let mut rest = s;
218+
let mut consumed = 0usize;
219+
220+
while let Some((i, nl_len)) = next_newline(rest) {
221+
line += 1;
222+
consumed += i + nl_len;
223+
last_line_start = consumed;
224+
rest = &rest[i + nl_len..];
225+
}
226+
227+
let col = 1 + s[last_line_start..]
228+
.chars()
229+
.map(char::len_utf16)
230+
.sum::<usize>();
231+
232+
(line, col)
233+
}
234+
235+
fn split_lines_preserving_crlf(file: &str) -> Vec<&str> {
236+
let mut out = Vec::new();
237+
let mut rest = file;
238+
239+
while let Some((i, nl_len)) = next_newline(rest) {
240+
out.push(&rest[..i]);
241+
rest = &rest[i + nl_len..];
242+
}
243+
244+
out.push(rest);
245+
out
211246
}
212247

213248
match self.file {
@@ -222,24 +257,28 @@ impl fmt::Display for RichError {
222257

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

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

230263
let start_line_len = lines
231264
.peek()
232-
.map_or(0, |l| l.chars().map(char::len_utf16).sum());
265+
.map_or(0, |l| l.chars().map(char::len_utf16).sum::<usize>());
233266

234267
for (relative_line_index, line_str) in lines.take(n_spanned_lines).enumerate() {
235268
let line_num = start_line_index + relative_line_index + 1;
236-
writeln!(f, "{line_num:line_num_width$} | {line_str}")?;
269+
write!(f, "{line_num:line_num_width$} |")?;
270+
if !line_str.is_empty() {
271+
write!(f, " {line_str}")?;
272+
}
273+
writeln!(f)?;
237274
}
238275

239276
let is_multiline = end_line > start_line;
240277

241278
let (underline_start, underline_length) = match is_multiline {
242-
true => (0, start_line_len),
279+
// For multiline spans, preserve the existing display style:
280+
// underline the full first displayed line.
281+
true => (1, start_line_len),
243282
false => (start_col, (end_col - start_col).max(1)),
244283
};
245284
write!(f, "{:width$} |", " ", width = line_num_width)?;
@@ -764,6 +803,81 @@ let x: u32 = Left(
764803
assert_eq!(&expected[1..], &error.to_string());
765804
}
766805

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

788902
let expected = r#"
789903
|
790-
3 |
904+
3 |
791905
| ^ Cannot parse: eof"#;
792906

793907
assert_eq!(&expected[1..], &error.to_string());
794908
}
909+
910+
#[test]
911+
fn display_zero_length_span_shows_single_caret() {
912+
let file = "let a: u8 = 1;";
913+
let error = Error::CannotParse("err".to_string())
914+
.with_span(Span::new(12, 12))
915+
.with_file(Arc::from(file));
916+
917+
let expected = r#"
918+
|
919+
1 | let a: u8 = 1;
920+
| ^ Cannot parse: err"#;
921+
922+
assert_eq!(&expected[1..], &error.to_string());
923+
}
795924
}

0 commit comments

Comments
 (0)