Skip to content

Commit b5a0af2

Browse files
authoredJun 14, 2024··
Merge pull request #131 from Muscraft/fix-no-highlght-line-end
Fix annotating line endings
2 parents ca313bf + c68600d commit b5a0af2

File tree

5 files changed

+514
-36
lines changed

5 files changed

+514
-36
lines changed
 

‎src/renderer/display_list.rs

+68-19
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ pub(crate) enum DisplaySourceLine<'a> {
524524
Content {
525525
text: &'a str,
526526
range: (usize, usize), // meta information for annotation placement.
527+
end_line: EndLine,
527528
},
528529
/// An empty source line.
529530
Empty,
@@ -658,7 +659,8 @@ impl<'a> CursorLines<'a> {
658659
}
659660
}
660661

661-
enum EndLine {
662+
#[derive(Copy, Clone, Debug, PartialEq)]
663+
pub(crate) enum EndLine {
662664
Eof = 0,
663665
Crlf = 1,
664666
Lf = 2,
@@ -847,13 +849,20 @@ fn format_header<'a>(
847849

848850
for item in body {
849851
if let DisplayLine::Source {
850-
line: DisplaySourceLine::Content { text, range },
852+
line:
853+
DisplaySourceLine::Content {
854+
text,
855+
range,
856+
end_line,
857+
},
851858
lineno,
852859
..
853860
} = item
854861
{
855-
if main_range >= range.0 && main_range <= range.1 {
856-
let char_column = text[0..(main_range - range.0)].chars().count();
862+
if main_range >= range.0 && main_range <= range.1 + *end_line as usize {
863+
let char_column = text[0..(main_range - range.0).min(text.len())]
864+
.chars()
865+
.count();
857866
col = char_column + 1;
858867
line_offset = lineno.unwrap_or(1);
859868
break;
@@ -927,8 +936,18 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
927936
let mut unhighlighed_lines = vec![];
928937
for line in body {
929938
match &line {
930-
DisplayLine::Source { annotations, .. } => {
931-
if annotations.is_empty() {
939+
DisplayLine::Source {
940+
annotations,
941+
inline_marks,
942+
..
943+
} => {
944+
if annotations.is_empty()
945+
// A multiline start mark (`/`) needs be treated as an
946+
// annotation or the line could get folded.
947+
&& inline_marks
948+
.iter()
949+
.all(|m| m.mark_type != DisplayMarkType::AnnotationStart)
950+
{
932951
unhighlighed_lines.push(line);
933952
} else {
934953
if lines.is_empty() {
@@ -1016,12 +1035,14 @@ fn format_body(
10161035
for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() {
10171036
let line_length: usize = line.len();
10181037
let line_range = (current_index, current_index + line_length);
1038+
let end_line_size = end_line as usize;
10191039
body.push(DisplayLine::Source {
10201040
lineno: Some(current_line),
10211041
inline_marks: vec![],
10221042
line: DisplaySourceLine::Content {
10231043
text: line,
10241044
range: line_range,
1045+
end_line,
10251046
},
10261047
annotations: vec![],
10271048
});
@@ -1045,7 +1066,7 @@ fn format_body(
10451066
let line_start_index = line_range.0;
10461067
let line_end_index = line_range.1;
10471068
current_line += 1;
1048-
current_index += line_length + end_line as usize;
1069+
current_index += line_length + end_line_size;
10491070

10501071
// It would be nice to use filter_drain here once it's stable.
10511072
annotations.retain(|annotation| {
@@ -1057,18 +1078,24 @@ fn format_body(
10571078
};
10581079
let label_right = annotation.label.map_or(0, |label| label.len() + 1);
10591080
match annotation.range {
1060-
Range { start, .. } if start > line_end_index => true,
1081+
// This handles if the annotation is on the next line. We add
1082+
// the `end_line_size` to account for annotating the line end.
1083+
Range { start, .. } if start > line_end_index + end_line_size => true,
1084+
// This handles the case where an annotation is contained
1085+
// within the current line including any line-end characters.
10611086
Range { start, end }
1062-
if start >= line_start_index && end <= line_end_index
1063-
// Allow annotating eof or stripped eol
1064-
|| start == line_end_index && end - start <= 1 =>
1087+
if start >= line_start_index
1088+
// We add at least one to `line_end_index` to allow
1089+
// highlighting the end of a file
1090+
&& end <= line_end_index + max(end_line_size, 1) =>
10651091
{
10661092
if let DisplayLine::Source {
10671093
ref mut annotations,
10681094
..
10691095
} = body[body_idx]
10701096
{
1071-
let annotation_start_col = line[0..(start - line_start_index)]
1097+
let annotation_start_col = line
1098+
[0..(start - line_start_index).min(line_length)]
10721099
.chars()
10731100
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
10741101
.sum::<usize>();
@@ -1101,11 +1128,16 @@ fn format_body(
11011128
}
11021129
false
11031130
}
1131+
// This handles the case where a multiline annotation starts
1132+
// somewhere on the current line, including any line-end chars
11041133
Range { start, end }
11051134
if start >= line_start_index
1106-
&& start <= line_end_index
1135+
// The annotation can start on a line ending
1136+
&& start <= line_end_index + end_line_size.saturating_sub(1)
11071137
&& end > line_end_index =>
11081138
{
1139+
// Special case for multiline annotations that start at the
1140+
// beginning of a line, which requires a special mark (`/`)
11091141
if start - line_start_index == 0 {
11101142
if let DisplayLine::Source {
11111143
ref mut inline_marks,
@@ -1122,7 +1154,8 @@ fn format_body(
11221154
..
11231155
} = body[body_idx]
11241156
{
1125-
let annotation_start_col = line[0..(start - line_start_index)]
1157+
let annotation_start_col = line
1158+
[0..(start - line_start_index).min(line_length)]
11261159
.chars()
11271160
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
11281161
.sum::<usize>();
@@ -1147,7 +1180,11 @@ fn format_body(
11471180
}
11481181
true
11491182
}
1150-
Range { start, end } if start < line_start_index && end > line_end_index => {
1183+
// This handles the case where a multiline annotation starts
1184+
// somewhere before this line and ends after it as well
1185+
Range { start, end }
1186+
if start < line_start_index && end > line_end_index + max(end_line_size, 1) =>
1187+
{
11511188
if let DisplayLine::Source {
11521189
ref mut inline_marks,
11531190
..
@@ -1160,10 +1197,14 @@ fn format_body(
11601197
}
11611198
true
11621199
}
1200+
// This handles the case where a multiline annotation ends
1201+
// somewhere on the current line, including any line-end chars
11631202
Range { start, end }
11641203
if start < line_start_index
11651204
&& end >= line_start_index
1166-
&& end <= line_end_index =>
1205+
// We add at least one to `line_end_index` to allow
1206+
// highlighting the end of a file
1207+
&& end <= line_end_index + max(end_line_size, 1) =>
11671208
{
11681209
if let DisplayLine::Source {
11691210
ref mut inline_marks,
@@ -1175,13 +1216,21 @@ fn format_body(
11751216
mark_type: DisplayMarkType::AnnotationThrough,
11761217
annotation_type: DisplayAnnotationType::from(annotation.level),
11771218
});
1178-
let end_mark = line[0..(end - line_start_index)]
1219+
let end_mark = line[0..(end - line_start_index).min(line_length)]
11791220
.chars()
11801221
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
11811222
.sum::<usize>()
11821223
.saturating_sub(1);
1183-
1184-
let end_plus_one = end_mark + 1;
1224+
// If the annotation ends on a line-end character, we
1225+
// need to annotate one past the end of the line
1226+
let (end_mark, end_plus_one) = if end > line_end_index
1227+
// Special case for highlighting the end of a file
1228+
|| (end == line_end_index + 1 && end_line_size == 0)
1229+
{
1230+
(end_mark + 1, end_mark + 2)
1231+
} else {
1232+
(end_mark, end_mark + 1)
1233+
};
11851234

11861235
span_left_margin = min(span_left_margin, end_mark);
11871236
span_right_margin = max(span_right_margin, end_plus_one);

‎tests/fixtures/no-color/ann_multiline2.svg

+6-8
Loading

‎tests/fixtures/no-color/ann_multiline2.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ title = "spacing error found"
55

66
[[message.snippets]]
77
source = """
8-
This is an exampl
9-
e of an edge case of an annotation overflowing
8+
This is an example
9+
of an edge case of an annotation overflowing
1010
to exactly one character on next line.
1111
"""
1212
line_start = 26
@@ -15,4 +15,4 @@ fold = false
1515
[[message.snippets.annotations]]
1616
label = "this should not be on separate lines"
1717
level = "Error"
18-
range = [11, 18]
18+
range = [11, 19]

‎tests/fixtures/no-color/fold_ann_multiline.svg

+8-6
Loading

‎tests/formatter.rs

+429
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,432 @@ LL | abc
303303
let renderer = Renderer::plain().anonymized_line_numbers(true);
304304
assert_data_eq!(renderer.render(input).to_string(), expected);
305305
}
306+
307+
#[test]
308+
fn issue_130() {
309+
let input = Level::Error.title("dummy").snippet(
310+
Snippet::source("foo\nbar\nbaz")
311+
.origin("file/path")
312+
.line_start(3)
313+
.fold(true)
314+
.annotation(Level::Error.span(4..11)), // bar\nbaz
315+
);
316+
317+
let expected = str![[r#"
318+
error: dummy
319+
--> file/path:4:1
320+
|
321+
4 | / bar
322+
5 | | baz
323+
| |___^
324+
|
325+
"#]];
326+
let renderer = Renderer::plain();
327+
assert_data_eq!(renderer.render(input).to_string(), expected);
328+
}
329+
330+
#[test]
331+
fn unterminated_string_multiline() {
332+
let source = "\
333+
a\"
334+
// ...
335+
";
336+
let input = Level::Error.title("").snippet(
337+
Snippet::source(source)
338+
.origin("file/path")
339+
.line_start(3)
340+
.fold(true)
341+
.annotation(Level::Error.span(0..10)), // 1..10 works
342+
);
343+
let expected = str![[r#"
344+
error
345+
--> file/path:3:1
346+
|
347+
3 | / a"
348+
4 | | // ...
349+
| |_______^
350+
|
351+
"#]];
352+
let renderer = Renderer::plain().anonymized_line_numbers(false);
353+
assert_data_eq!(renderer.render(input).to_string(), expected);
354+
}
355+
356+
#[test]
357+
fn char_and_nl_annotate_char() {
358+
let source = "a\r\nb";
359+
let input = Level::Error.title("").snippet(
360+
Snippet::source(source)
361+
.origin("file/path")
362+
.line_start(3)
363+
.annotation(Level::Error.span(0..2)), // a\r
364+
);
365+
let expected = str![[r#"
366+
error
367+
--> file/path:3:1
368+
|
369+
3 | a
370+
| ^
371+
4 | b
372+
|"#]];
373+
let renderer = Renderer::plain().anonymized_line_numbers(false);
374+
assert_data_eq!(renderer.render(input).to_string(), expected);
375+
}
376+
377+
#[test]
378+
fn char_eol_annotate_char() {
379+
let source = "a\r\nb";
380+
let input = Level::Error.title("").snippet(
381+
Snippet::source(source)
382+
.origin("file/path")
383+
.line_start(3)
384+
.annotation(Level::Error.span(0..3)), // a\r\n
385+
);
386+
let expected = str![[r#"
387+
error
388+
--> file/path:3:1
389+
|
390+
3 | a
391+
| ^
392+
4 | b
393+
|"#]];
394+
let renderer = Renderer::plain().anonymized_line_numbers(false);
395+
assert_data_eq!(renderer.render(input).to_string(), expected);
396+
}
397+
398+
#[test]
399+
fn char_eol_annotate_char_double_width() {
400+
let snippets = Level::Error.title("").snippet(
401+
Snippet::source("こん\r\nにちは\r\n世界")
402+
.origin("<current file>")
403+
.annotation(Level::Error.span(3..8)), // ん\r\n
404+
);
405+
406+
let expected = str![[r#"
407+
error
408+
--> <current file>:1:2
409+
|
410+
1 | こん
411+
| ^^
412+
2 | にちは
413+
3 | 世界
414+
|
415+
"#]];
416+
417+
let renderer = Renderer::plain();
418+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
419+
}
420+
421+
#[test]
422+
fn annotate_eol() {
423+
let source = "a\r\nb";
424+
let input = Level::Error.title("").snippet(
425+
Snippet::source(source)
426+
.origin("file/path")
427+
.line_start(3)
428+
.annotation(Level::Error.span(1..2)), // \r
429+
);
430+
let expected = str![[r#"
431+
error
432+
--> file/path:3:2
433+
|
434+
3 | a
435+
| ^
436+
4 | b
437+
|"#]];
438+
let renderer = Renderer::plain().anonymized_line_numbers(false);
439+
assert_data_eq!(renderer.render(input).to_string(), expected);
440+
}
441+
442+
#[test]
443+
fn annotate_eol2() {
444+
let source = "a\r\nb";
445+
let input = Level::Error.title("").snippet(
446+
Snippet::source(source)
447+
.origin("file/path")
448+
.line_start(3)
449+
.annotation(Level::Error.span(1..3)), // \r\n
450+
);
451+
let expected = str![[r#"
452+
error
453+
--> file/path:3:2
454+
|
455+
3 | a
456+
| ^
457+
4 | b
458+
|"#]];
459+
let renderer = Renderer::plain().anonymized_line_numbers(false);
460+
assert_data_eq!(renderer.render(input).to_string(), expected);
461+
}
462+
463+
#[test]
464+
fn annotate_eol3() {
465+
let source = "a\r\nb";
466+
let input = Level::Error.title("").snippet(
467+
Snippet::source(source)
468+
.origin("file/path")
469+
.line_start(3)
470+
.annotation(Level::Error.span(2..3)), // \n
471+
);
472+
let expected = str![[r#"
473+
error
474+
--> file/path:3:2
475+
|
476+
3 | a
477+
| ^
478+
4 | b
479+
|"#]];
480+
let renderer = Renderer::plain().anonymized_line_numbers(false);
481+
assert_data_eq!(renderer.render(input).to_string(), expected);
482+
}
483+
484+
#[test]
485+
fn annotate_eol4() {
486+
let source = "a\r\nb";
487+
let input = Level::Error.title("").snippet(
488+
Snippet::source(source)
489+
.origin("file/path")
490+
.line_start(3)
491+
.annotation(Level::Error.span(2..2)), // \n
492+
);
493+
let expected = str![[r#"
494+
error
495+
--> file/path:3:2
496+
|
497+
3 | a
498+
| ^
499+
4 | b
500+
|"#]];
501+
let renderer = Renderer::plain().anonymized_line_numbers(false);
502+
assert_data_eq!(renderer.render(input).to_string(), expected);
503+
}
504+
505+
#[test]
506+
fn annotate_eol_double_width() {
507+
let snippets = Level::Error.title("").snippet(
508+
Snippet::source("こん\r\nにちは\r\n世界")
509+
.origin("<current file>")
510+
.annotation(Level::Error.span(7..8)), // \n
511+
);
512+
513+
let expected = str![[r#"
514+
error
515+
--> <current file>:1:3
516+
|
517+
1 | こん
518+
| ^
519+
2 | にちは
520+
3 | 世界
521+
|
522+
"#]];
523+
524+
let renderer = Renderer::plain();
525+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
526+
}
527+
528+
#[test]
529+
fn multiline_eol_start() {
530+
let source = "a\r\nb";
531+
let input = Level::Error.title("").snippet(
532+
Snippet::source(source)
533+
.origin("file/path")
534+
.line_start(3)
535+
.annotation(Level::Error.span(1..4)), // \r\nb
536+
);
537+
let expected = str![[r#"
538+
error
539+
--> file/path:3:2
540+
|
541+
3 | a
542+
| __^
543+
4 | | b
544+
| |_^
545+
|"#]];
546+
let renderer = Renderer::plain().anonymized_line_numbers(false);
547+
assert_data_eq!(renderer.render(input).to_string(), expected);
548+
}
549+
550+
#[test]
551+
fn multiline_eol_start2() {
552+
let source = "a\r\nb";
553+
let input = Level::Error.title("").snippet(
554+
Snippet::source(source)
555+
.origin("file/path")
556+
.line_start(3)
557+
.annotation(Level::Error.span(2..4)), // \nb
558+
);
559+
let expected = str![[r#"
560+
error
561+
--> file/path:3:2
562+
|
563+
3 | a
564+
| __^
565+
4 | | b
566+
| |_^
567+
|"#]];
568+
let renderer = Renderer::plain().anonymized_line_numbers(false);
569+
assert_data_eq!(renderer.render(input).to_string(), expected);
570+
}
571+
572+
#[test]
573+
fn multiline_eol_start3() {
574+
let source = "a\nb";
575+
let input = Level::Error.title("").snippet(
576+
Snippet::source(source)
577+
.origin("file/path")
578+
.line_start(3)
579+
.annotation(Level::Error.span(1..3)), // \nb
580+
);
581+
let expected = str![[r#"
582+
error
583+
--> file/path:3:2
584+
|
585+
3 | a
586+
| __^
587+
4 | | b
588+
| |_^
589+
|"#]];
590+
let renderer = Renderer::plain().anonymized_line_numbers(false);
591+
assert_data_eq!(renderer.render(input).to_string(), expected);
592+
}
593+
594+
#[test]
595+
fn multiline_eol_start_double_width() {
596+
let snippets = Level::Error.title("").snippet(
597+
Snippet::source("こん\r\nにちは\r\n世界")
598+
.origin("<current file>")
599+
.annotation(Level::Error.span(7..11)), // \r\nに
600+
);
601+
602+
let expected = str![[r#"
603+
error
604+
--> <current file>:1:3
605+
|
606+
1 | こん
607+
| _____^
608+
2 | | にちは
609+
| |__^
610+
3 | 世界
611+
|
612+
"#]];
613+
614+
let renderer = Renderer::plain();
615+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
616+
}
617+
618+
#[test]
619+
fn multiline_eol_start_eol_end() {
620+
let source = "a\nb\nc";
621+
let input = Level::Error.title("").snippet(
622+
Snippet::source(source)
623+
.origin("file/path")
624+
.line_start(3)
625+
.annotation(Level::Error.span(1..4)), // \nb\n
626+
);
627+
let expected = str![[r#"
628+
error
629+
--> file/path:3:2
630+
|
631+
3 | a
632+
| __^
633+
4 | | b
634+
| |__^
635+
5 | c
636+
|
637+
"#]];
638+
let renderer = Renderer::plain().anonymized_line_numbers(false);
639+
assert_data_eq!(renderer.render(input).to_string(), expected);
640+
}
641+
642+
#[test]
643+
fn multiline_eol_start_eol_end2() {
644+
let source = "a\r\nb\r\nc";
645+
let input = Level::Error.title("").snippet(
646+
Snippet::source(source)
647+
.origin("file/path")
648+
.line_start(3)
649+
.annotation(Level::Error.span(2..5)), // \nb\r
650+
);
651+
let expected = str![[r#"
652+
error
653+
--> file/path:3:2
654+
|
655+
3 | a
656+
| __^
657+
4 | | b
658+
| |__^
659+
5 | c
660+
|
661+
"#]];
662+
let renderer = Renderer::plain().anonymized_line_numbers(false);
663+
assert_data_eq!(renderer.render(input).to_string(), expected);
664+
}
665+
666+
#[test]
667+
fn multiline_eol_start_eol_end3() {
668+
let source = "a\r\nb\r\nc";
669+
let input = Level::Error.title("").snippet(
670+
Snippet::source(source)
671+
.origin("file/path")
672+
.line_start(3)
673+
.annotation(Level::Error.span(2..6)), // \nb\r\n
674+
);
675+
let expected = str![[r#"
676+
error
677+
--> file/path:3:2
678+
|
679+
3 | a
680+
| __^
681+
4 | | b
682+
| |__^
683+
5 | c
684+
|
685+
"#]];
686+
let renderer = Renderer::plain().anonymized_line_numbers(false);
687+
assert_data_eq!(renderer.render(input).to_string(), expected);
688+
}
689+
690+
#[test]
691+
fn multiline_eol_start_eof_end() {
692+
let source = "a\r\nb";
693+
let input = Level::Error.title("").snippet(
694+
Snippet::source(source)
695+
.origin("file/path")
696+
.line_start(3)
697+
.annotation(Level::Error.span(1..5)), // \r\nb(EOF)
698+
);
699+
let expected = str![[r#"
700+
error
701+
--> file/path:3:2
702+
|
703+
3 | a
704+
| __^
705+
4 | | b
706+
| |__^
707+
|
708+
"#]];
709+
let renderer = Renderer::plain().anonymized_line_numbers(false);
710+
assert_data_eq!(renderer.render(input).to_string(), expected);
711+
}
712+
713+
#[test]
714+
fn multiline_eol_start_eof_end_double_width() {
715+
let source = "ん\r\nに";
716+
let input = Level::Error.title("").snippet(
717+
Snippet::source(source)
718+
.origin("file/path")
719+
.line_start(3)
720+
.annotation(Level::Error.span(3..9)), // \r\nに(EOF)
721+
);
722+
let expected = str![[r#"
723+
error
724+
--> file/path:3:2
725+
|
726+
3 | ん
727+
| ___^
728+
4 | | に
729+
| |___^
730+
|
731+
"#]];
732+
let renderer = Renderer::plain().anonymized_line_numbers(false);
733+
assert_data_eq!(renderer.render(input).to_string(), expected);
734+
}

0 commit comments

Comments
 (0)
Please sign in to comment.