@@ -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,61 @@ 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 ( ) ;
198196
199- let slice = file. get ( 0 ..offset) . unwrap_or_default ( ) ;
197+ while let Some ( ( i, ch) ) = it. next ( ) {
198+ // Treat CRLF as one logical newline.
199+ if ch == '\r' && matches ! ( it. peek( ) , Some ( ( _, c) ) if * c == '\n' ) {
200+ return Some ( ( i, ch. len_utf8 ( ) + '\n' . len_utf8 ( ) ) ) ;
201+ }
200202
201- for char in slice. chars ( ) {
202- if char. is_newline ( ) {
203- line += 1 ;
204- col = 0 ;
205- } else {
206- col += char. len_utf16 ( ) ;
203+ // Support LF.
204+ if ch == '\n' {
205+ return Some ( ( i, ch. len_utf8 ( ) ) ) ;
207206 }
207+
208+ // Unicode separator support
209+ if ch == '\u{2028}' || ch == '\u{2029}' {
210+ return Some ( ( i, ch. len_utf8 ( ) ) ) ;
211+ }
212+ }
213+
214+ None
215+ }
216+ fn get_line_col ( file : & str , offset : usize ) -> ( usize , usize ) {
217+ let s = file. get ( ..offset) . unwrap_or_default ( ) ;
218+ let mut line = 1usize ;
219+ let mut last_line_start = 0usize ;
220+ let mut rest = s;
221+ let mut consumed = 0usize ;
222+
223+ while let Some ( ( i, nl_len) ) = next_newline ( rest) {
224+ line += 1 ;
225+ consumed += i + nl_len;
226+ last_line_start = consumed;
227+ rest = & rest[ i + nl_len..] ;
228+ }
229+
230+ let col = 1 + s[ last_line_start..]
231+ . chars ( )
232+ . map ( char:: len_utf16)
233+ . sum :: < usize > ( ) ;
234+
235+ ( line, col)
236+ }
237+
238+ fn split_lines_preserving_crlf ( file : & str ) -> Vec < & str > {
239+ let mut out = Vec :: new ( ) ;
240+ let mut rest = file;
241+
242+ while let Some ( ( i, nl_len) ) = next_newline ( rest) {
243+ out. push ( & rest[ ..i] ) ;
244+ rest = & rest[ i + nl_len..] ;
208245 }
209246
210- ( line, col + 1 )
247+ out. push ( rest) ;
248+ out
211249 }
212250
213251 match self . file {
@@ -222,24 +260,28 @@ impl fmt::Display for RichError {
222260
223261 writeln ! ( f, "{:width$} |" , " " , width = line_num_width) ?;
224262
225- let mut lines = file
226- . split ( |c : char | c. is_newline ( ) )
227- . skip ( start_line_index)
228- . peekable ( ) ;
263+ let split_lines = split_lines_preserving_crlf ( file) ;
264+ let mut lines = split_lines. into_iter ( ) . skip ( start_line_index) . peekable ( ) ;
229265
230266 let start_line_len = lines
231267 . peek ( )
232- . map_or ( 0 , |l| l. chars ( ) . map ( char:: len_utf16) . sum ( ) ) ;
268+ . map_or ( 0 , |l| l. chars ( ) . map ( char:: len_utf16) . sum :: < usize > ( ) ) ;
233269
234270 for ( relative_line_index, line_str) in lines. take ( n_spanned_lines) . enumerate ( ) {
235271 let line_num = start_line_index + relative_line_index + 1 ;
236- writeln ! ( f, "{line_num:line_num_width$} | {line_str}" ) ?;
272+ write ! ( f, "{line_num:line_num_width$} |" ) ?;
273+ if !line_str. is_empty ( ) {
274+ write ! ( f, " {line_str}" ) ?;
275+ }
276+ writeln ! ( f) ?;
237277 }
238278
239279 let is_multiline = end_line > start_line;
240280
241281 let ( underline_start, underline_length) = match is_multiline {
242- true => ( 0 , start_line_len) ,
282+ // For multiline spans, preserve the existing display style:
283+ // underline the full first displayed line.
284+ true => ( 1 , start_line_len) ,
243285 false => ( start_col, ( end_col - start_col) . max ( 1 ) ) ,
244286 } ;
245287 write ! ( f, "{:width$} |" , " " , width = line_num_width) ?;
@@ -764,6 +806,81 @@ let x: u32 = Left(
764806 assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
765807 }
766808
809+ #[ test]
810+ fn display_with_windows_crlf_newlines ( ) {
811+ let file = "let a: u8 = 65536;\r \n let b: u8 = 0;" ;
812+ let error = Error :: CannotParse ( "number too large to fit in target type" . to_string ( ) )
813+ . with_span ( Span :: new ( 12 , 17 ) )
814+ . with_file ( Arc :: from ( file) ) ;
815+
816+ let expected = r#"
817+ |
818+ 1 | let a: u8 = 65536;
819+ | ^^^^^ Cannot parse: number too large to fit in target type"# ;
820+
821+ assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
822+ }
823+
824+ #[ test]
825+ fn display_with_unix_lf_newlines ( ) {
826+ let file = "let a: u8 = 65536;\n let b: u8 = 0;" ;
827+ let error = Error :: CannotParse ( "number too large to fit in target type" . to_string ( ) )
828+ . with_span ( Span :: new ( 12 , 17 ) )
829+ . with_file ( Arc :: from ( file) ) ;
830+
831+ let expected = r#"
832+ |
833+ 1 | let a: u8 = 65536;
834+ | ^^^^^ Cannot parse: number too large to fit in target type"# ;
835+
836+ assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
837+ }
838+
839+ #[ test]
840+ fn display_with_mixed_newlines_on_second_line ( ) {
841+ let file = "line1\r \n line2\n line3" ;
842+ let error = Error :: CannotParse ( "err" . to_string ( ) )
843+ . with_span ( Span :: new ( 7 , 12 ) )
844+ . with_file ( Arc :: from ( file) ) ;
845+
846+ let expected = r#"
847+ |
848+ 2 | line2
849+ | ^^^^^ Cannot parse: err"# ;
850+
851+ assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
852+ }
853+
854+ #[ test]
855+ fn display_does_not_insert_extra_blank_line_for_crlf ( ) {
856+ let file = "a\r \n b" ;
857+ let error = Error :: CannotParse ( "err" . to_string ( ) )
858+ . with_span ( Span :: new ( 3 , 4 ) )
859+ . with_file ( Arc :: from ( file) ) ;
860+
861+ let expected = r#"
862+ |
863+ 2 | b
864+ | ^ Cannot parse: err"# ;
865+
866+ assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
867+ }
868+
869+ #[ test]
870+ fn display_handles_utf16_columns_after_newline ( ) {
871+ let file = "x\r \n 😀ab" ;
872+ let error = Error :: CannotParse ( "err" . to_string ( ) )
873+ . with_span ( Span :: new ( 7 , 9 ) )
874+ . with_file ( Arc :: from ( file) ) ;
875+
876+ let expected = r#"
877+ |
878+ 2 | 😀ab
879+ | ^^ Cannot parse: err"# ;
880+
881+ assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
882+ }
883+
767884 #[ test]
768885 fn display_span_as_point ( ) {
769886 let file = "fn main()" ;
@@ -787,9 +904,24 @@ let x: u32 = Left(
787904
788905 let expected = r#"
789906 |
790- 3 |
907+ 3 |
791908 | ^ Cannot parse: eof"# ;
792909
793910 assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
794911 }
912+
913+ #[ test]
914+ fn display_zero_length_span_shows_single_caret ( ) {
915+ let file = "let a: u8 = 1;" ;
916+ let error = Error :: CannotParse ( "err" . to_string ( ) )
917+ . with_span ( Span :: new ( 12 , 12 ) )
918+ . with_file ( Arc :: from ( file) ) ;
919+
920+ let expected = r#"
921+ |
922+ 1 | let a: u8 = 1;
923+ | ^ Cannot parse: err"# ;
924+
925+ assert_eq ! ( & expected[ 1 ..] , & error. to_string( ) ) ;
926+ }
795927}
0 commit comments