@@ -5,7 +5,6 @@ use std::sync::Arc;
55use chumsky:: error:: Error as ChumskyError ;
66use chumsky:: input:: ValueInput ;
77use chumsky:: label:: LabelError ;
8- use chumsky:: text:: Char ;
98use chumsky:: util:: MaybeRef ;
109use chumsky:: DefaultExpected ;
1110
@@ -192,22 +191,67 @@ impl RichError {
192191
193192impl 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 \n let 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;\n let 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 \n line2\n line3" ;
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 \n b" ;
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;\r let 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