diff --git a/codespan-reporting/src/term/renderer.rs b/codespan-reporting/src/term/renderer.rs index 31c35e83..507e5c2a 100644 --- a/codespan-reporting/src/term/renderer.rs +++ b/codespan-reporting/src/term/renderer.rs @@ -239,6 +239,20 @@ impl<'writer, 'config> Renderer<'writer, 'config> { // FIXME: Use the number of trimmed placeholders when rendering single line carets let source = source.trim_end_matches(['\n', '\r', '\0'].as_ref()); + // get the byte index of the first non-whitespace character + let text_start = source.find(|c| !char::is_whitespace(c)).unwrap_or(0); + + // get the byte index of the first trailing whitespace character + let text_end = source + .char_indices() + .rev() + .take_while(|(_, c)| c.is_whitespace()) + .last() + .map(|(index, _)| index) + .unwrap_or_else(|| source.len()); + + let text_range = text_start..text_end; + // Write source line // // ```text @@ -273,37 +287,43 @@ impl<'writer, 'config> Renderer<'writer, 'config> { // Write source text write!(self, " ")?; - let mut in_primary = false; + let mut highlighted = false; + + // iterate over all characters of source code for (metrics, ch) in self.char_metrics(source.char_indices()) { let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); - // Check if we are overlapping a primary label - let is_primary = single_labels.iter().any(|(ls, range, _)| { - *ls == LabelStyle::Primary && is_overlapping(range, &column_range) - }) || multi_labels.iter().any(|(_, ls, label)| { + /* + A character should be highlighted only if it is part of a primary single label which does not span the whole line + A label already spans the whole line if it does not cover leading/trailing whitespace. + */ + let should_highlight = single_labels.iter().any(|(ls, range, _)| { *ls == LabelStyle::Primary - && match label { - MultiLabel::Top(start) => column_range.start >= *start, - MultiLabel::Left => true, - MultiLabel::Bottom(start, _) => column_range.end <= *start, - } + // is this at the current position + && is_overlapping(range, &column_range) + // is this not a whole line label + && !( + range.start <= text_range.start + && range.end >= text_range.end + ) }); // Set the source color if we are in a primary label - if is_primary && !in_primary { + if should_highlight && !highlighted { self.set_color(self.styles().label(severity, LabelStyle::Primary))?; - in_primary = true; - } else if !is_primary && in_primary { + highlighted = true; + } else if !should_highlight && highlighted { self.reset()?; - in_primary = false; + highlighted = false; } + // actually write the character match ch { '\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?, _ => write!(self, "{}", ch)?, } } - if in_primary { + if highlighted { self.reset()?; } writeln!(self)?; diff --git a/codespan-reporting/tests/snapshots/term__highlight__rich_color.snap b/codespan-reporting/tests/snapshots/term__highlight__rich_color.snap new file mode 100644 index 00000000..b375f47c --- /dev/null +++ b/codespan-reporting/tests/snapshots/term__highlight__rich_color.snap @@ -0,0 +1,36 @@ +--- +source: codespan-reporting/tests/term.rs +expression: TEST_DATA.emit_color(&config) +--- +{fg:Green bold bright}note{bold bright}: not highlighted{/} + {fg:Blue}┌─{/} test.txt:1:1 + {fg:Blue}│{/} +{fg:Blue}1{/} {fg:Blue}│{/} This is some text. + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^^{/} +{fg:Blue}2{/} {fg:Blue}│{/} This is some indented text. + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^^^^^^^^^^^{/} +{fg:Blue}3{/} {fg:Blue}│{/} This is some indented text with trailing whitespace. + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{/} + +{fg:Green bold bright}note{bold bright}: also not highlighted{/} + {fg:Blue}┌─{/} test.txt:2:1 + {fg:Blue}│{/} +{fg:Blue}2{/} {fg:Blue}│{/} This is some indented text. + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{/} +{fg:Blue}3{/} {fg:Blue}│{/} This is some indented text with trailing whitespace. + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^{/} + +{fg:Green bold bright}note{bold bright}: highlighted{/} + {fg:Blue}┌─{/} test.txt:1:1 + {fg:Blue}│{/} +{fg:Blue}1{/} {fg:Blue}│{/} {fg:Green}This is some text{/}. + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^{/} +{fg:Blue}2{/} {fg:Blue}│{/} T{fg:Green}his is some indented text.{/} + {fg:Blue}│{/} {fg:Green}^^^^^^^^^^^^^^^^^^^^^^^^^^{/} +{fg:Blue}3{/} {fg:Blue}│{/} {fg:Green}This{/} is some indented text with trailing whitespace. + {fg:Blue}│{/} {fg:Green}^^^^{/} +{fg:Blue}4{/} {fg:Blue}│{/} {fg:Green}╭{/} This is some multiline text. +{fg:Blue}5{/} {fg:Blue}│{/} {fg:Green}│{/} This is some more multiline text. + {fg:Blue}│{/} {fg:Green}╰{/}{fg:Green}─────────────────────────────────^{/} + + diff --git a/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap b/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap index 10701fbb..febbc124 100644 --- a/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap +++ b/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap @@ -10,11 +10,11 @@ expression: TEST_DATA.emit_color(&config) {fg:Blue}│{/} {fg:Blue}│{/} {fg:Blue}---------------------------------------------{/} {fg:Blue}this is found to be of type `Result`{/} {fg:Blue}3{/} {fg:Blue}│{/} {fg:Blue}│{/} Ordering::Equal => Ok(self.source_span().end()), {fg:Blue}│{/} {fg:Blue}│{/} {fg:Blue}----------------------------{/} {fg:Blue}this is found to be of type `Result`{/} -{fg:Blue}4{/} {fg:Blue}│{/} {fg:Blue}│{/} Ordering::Greater => {fg:Red}LineIndexOutOfBoundsError {{/} +{fg:Blue}4{/} {fg:Blue}│{/} {fg:Blue}│{/} Ordering::Greater => LineIndexOutOfBoundsError { {fg:Blue}│{/} {fg:Red}╭{/}{fg:Red}─{/}{fg:Blue}│{/}{fg:Red}──────────────────────────────────^{/} -{fg:Blue}5{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} {fg:Red} given: line_index,{/} -{fg:Blue}6{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} {fg:Red} max: self.last_line_index(),{/} -{fg:Blue}7{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} {fg:Red} }{/}, +{fg:Blue}5{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} given: line_index, +{fg:Blue}6{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} max: self.last_line_index(), +{fg:Blue}7{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/} }, {fg:Blue}│{/} {fg:Red}╰{/}{fg:Red}─{/}{fg:Blue}│{/}{fg:Red}─────────────^ expected enum `Result`, found struct `LineIndexOutOfBoundsError`{/} {fg:Blue}8{/} {fg:Blue}│{/} {fg:Blue}│{/} } {fg:Blue}│{/} {fg:Blue}╰{/}{fg:Blue}─────────' `match` arms have incompatible types{/} diff --git a/codespan-reporting/tests/term.rs b/codespan-reporting/tests/term.rs index eb5f7ad1..3a0d7d7f 100644 --- a/codespan-reporting/tests/term.rs +++ b/codespan-reporting/tests/term.rs @@ -940,6 +940,7 @@ mod unicode_spans { } mod position_indicator { + use super::*; lazy_static::lazy_static! { @@ -1052,6 +1053,53 @@ mod multiline_omit { test_emit!(rich_no_color); } +mod highlight { + use super::*; + + lazy_static::lazy_static! { + static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, String>> = { + let file = SimpleFile::new( + "test.txt", + [ + "This is some text.", + "\tThis is some indented text.", + "\tThis is some indented text with trailing whitespace. \t", + "This is some multiline text.", + "This is some more multiline text.", + ] + .join("\n"), + ); + + let diagnostics = vec![ + Diagnostic::note() + .with_message("not highlighted") + .with_labels(vec![ + Label::primary((), 0..18), + Label::primary((), 20..47), + Label::primary((), 49..101), + ]), + Diagnostic::note() + .with_message("also not highlighted") + .with_labels(vec![ + Label::primary((), 19..47), + Label::primary((), 48..103), + ]), + Diagnostic::note() + .with_message("highlighted") + .with_labels(vec![ + Label::primary((), 0..17), + Label::primary((), 21..47), + Label::primary((), 49..53), + Label::primary((), 104..166), + ]), + ]; + TestData{files: file, diagnostics } + }; + } + + test_emit!(rich_color); +} + mod surrounding_lines { use super::*;