diff --git a/examples/elide_header.rs b/examples/elide_header.rs index 436bb25..c7deda1 100644 --- a/examples/elide_header.rs +++ b/examples/elide_header.rs @@ -14,7 +14,7 @@ def foobar(door, bar={}): .fold(false) .annotation(AnnotationKind::Primary.span(56..58).label("B006")), ) - .element(Level::HELP.title("Replace with `None`; initialize within function"))]; + .element(Level::HELP.message("Replace with `None`; initialize within function"))]; let renderer = Renderer::styled(); anstream::println!("{}", renderer.render(message)); diff --git a/examples/highlight_title.rs b/examples/highlight_message.rs similarity index 96% rename from examples/highlight_title.rs rename to examples/highlight_message.rs index 1de99cf..aaf4910 100644 --- a/examples/highlight_title.rs +++ b/examples/highlight_message.rs @@ -28,7 +28,7 @@ fn main() { let magenta = annotate_snippets::renderer::AnsiColor::Magenta .on_default() .effects(Effects::BOLD); - let title = format!( + let message = format!( "expected fn pointer `{}for<'a>{} fn(Box<{}(dyn Any + Send + 'a){}>) -> Pin<_>` found fn item `fn(Box<{}(dyn Any + Send + 'static){}>) -> Pin<_> {}{{wrapped_fn}}{}`", magenta.render(), @@ -57,7 +57,7 @@ fn main() { .label("arguments to this function are incorrect"), ), ) - .element(Level::NOTE.pre_styled_title(&title)), + .element(Level::NOTE.message(&message)), Group::with_title(Level::NOTE.title("function defined here")).element( Snippet::source(source) .path("$DIR/highlighting.rs") diff --git a/examples/highlight_title.svg b/examples/highlight_message.svg similarity index 100% rename from examples/highlight_title.svg rename to examples/highlight_message.svg diff --git a/src/level.rs b/src/level.rs index 036cf7d..8eaaa87 100644 --- a/src/level.rs +++ b/src/level.rs @@ -2,7 +2,7 @@ use crate::renderer::stylesheet::Stylesheet; use crate::snippet::{ERROR_TXT, HELP_TXT, INFO_TXT, NOTE_TXT, WARNING_TXT}; -use crate::{OptionCow, Title}; +use crate::{Message, OptionCow, Title}; use anstyle::Style; use std::borrow::Cow; @@ -73,6 +73,10 @@ impl<'a> Level<'a> { } impl<'a> Level<'a> { + /// A text [`Element`][crate::Element] to start a [`Group`][crate::Group] + /// + /// See [`Group::with_title`][crate::Group::with_title] + /// ///
/// /// Text passed to this function is considered "untrusted input", as such @@ -80,15 +84,16 @@ impl<'a> Level<'a> { /// not allowed to be passed to this function. /// ///
- pub fn title(self, title: impl Into>) -> Title<'a> { + pub fn title(self, text: impl Into>) -> Title<'a> { Title { level: self, id: None, - title: title.into(), - is_pre_styled: false, + text: text.into(), } } + /// A text [`Element`][crate::Element] in a [`Group`][crate::Group] + /// ///
/// /// Text passed to this function is allowed to be pre-styled, as such all @@ -97,12 +102,10 @@ impl<'a> Level<'a> { /// used to normalize untrusted text before it is passed to this function. /// ///
- pub fn pre_styled_title(self, title: impl Into>) -> Title<'a> { - Title { + pub fn message(self, text: impl Into>) -> Message<'a> { + Message { level: self, - id: None, - title: title.into(), - is_pre_styled: true, + text: text.into(), } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 608613b..b5eff06 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -48,7 +48,7 @@ use crate::renderer::source_map::{ }; use crate::renderer::styled_buffer::StyledBuffer; use crate::snippet::Id; -use crate::{Annotation, AnnotationKind, Element, Group, Origin, Patch, Snippet, Title}; +use crate::{Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Snippet, Title}; pub use anstyle::*; use margin::Margin; use std::borrow::Cow; @@ -303,7 +303,20 @@ impl Renderer { title, max_line_num_len, title_style, - matches!(peek, Some(Element::Title(_))), + matches!(peek, Some(Element::Title(_) | Element::Message(_))), + buffer_msg_line_offset, + ); + last_was_suggestion = false; + } + Element::Message(title) => { + let title_style = TitleStyle::Secondary; + let buffer_msg_line_offset = buffer.num_lines(); + self.render_title( + &mut buffer, + title, + max_line_num_len, + title_style, + matches!(peek, Some(Element::Title(_) | Element::Message(_))), buffer_msg_line_offset, ); last_was_suggestion = false; @@ -336,6 +349,16 @@ impl Renderer { ); } + Some(Element::Message(level)) + if level.level.name != Some(None) => + { + self.draw_col_separator_no_space( + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + None if group_len > 1 => self.draw_col_separator_end( &mut buffer, current_line, @@ -384,7 +407,8 @@ impl Renderer { if g == 0 && (matches!(section, Element::Origin(_)) || (matches!(section, Element::Title(_)) && i == 0) - || matches!(section, Element::Title(level) if level.level.name == Some(None))) + || matches!(section, Element::Title(level) if level.level.name == Some(None)) + || matches!(section, Element::Message(level) if level.level.name == Some(None))) { let current_line = buffer.num_lines(); if peek.is_none() && group_len > 1 { @@ -394,6 +418,13 @@ impl Renderer { max_line_num_len + 1, ); } else if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None)) + { + self.draw_col_separator_no_space( + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } else if matches!(peek, Some(Element::Message(level)) if level.level.name != Some(None)) { self.draw_col_separator_no_space( &mut buffer, @@ -503,7 +534,7 @@ impl Renderer { fn render_title( &self, buffer: &mut StyledBuffer, - title: &Title<'_>, + title: &dyn MessageOrTitle, max_line_num_len: usize, title_style: TitleStyle, is_cont: bool, @@ -511,7 +542,7 @@ impl Renderer { ) { let (label_style, title_element_style) = match title_style { TitleStyle::MainHeader => ( - ElementStyle::Level(title.level.level), + ElementStyle::Level(title.level().level), if self.short_message { ElementStyle::NoStyle } else { @@ -519,7 +550,7 @@ impl Renderer { }, ), TitleStyle::Header => ( - ElementStyle::Level(title.level.level), + ElementStyle::Level(title.level().level), ElementStyle::HeaderMsg, ), TitleStyle::Secondary => { @@ -538,10 +569,10 @@ impl Renderer { }; let mut label_width = 0; - if title.level.name != Some(None) { - buffer.append(buffer_msg_line_offset, title.level.as_str(), label_style); - label_width += title.level.as_str().len(); - if let Some(Id { id: Some(id), url }) = &title.id { + if title.level().name != Some(None) { + buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style); + label_width += title.level().as_str().len(); + if let Some(Id { id: Some(id), url }) = &title.id() { buffer.append(buffer_msg_line_offset, "[", label_style); if let Some(url) = url.as_ref() { buffer.append( @@ -584,10 +615,10 @@ impl Renderer { label_width }); - let (title_str, style) = if title.is_pre_styled { - (title.title.to_string(), ElementStyle::NoStyle) + let (title_str, style) = if title.is_pre_styled() { + (title.text().to_owned(), ElementStyle::NoStyle) } else { - (normalize_whitespace(&title.title), title_element_style) + (normalize_whitespace(title.text()), title_element_style) }; for (i, text) in title_str.lines().enumerate() { if i != 0 { @@ -2532,6 +2563,43 @@ impl Renderer { } } +trait MessageOrTitle { + fn level(&self) -> &Level<'_>; + fn id(&self) -> Option<&Id<'_>>; + fn text(&self) -> &str; + fn is_pre_styled(&self) -> bool; +} + +impl MessageOrTitle for Title<'_> { + fn level(&self) -> &Level<'_> { + &self.level + } + fn id(&self) -> Option<&Id<'_>> { + self.id.as_ref() + } + fn text(&self) -> &str { + self.text.as_ref() + } + fn is_pre_styled(&self) -> bool { + false + } +} + +impl MessageOrTitle for Message<'_> { + fn level(&self) -> &Level<'_> { + &self.level + } + fn id(&self) -> Option<&Id<'_>> { + None + } + fn text(&self) -> &str { + self.text.as_ref() + } + fn is_pre_styled(&self) -> bool { + true + } +} + // instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until // we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which // is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway. @@ -2846,7 +2914,10 @@ fn max_line_number(groups: &[Group<'_>]) -> usize { v.elements .iter() .map(|s| match s { - Element::Title(_) | Element::Origin(_) | Element::Padding(_) => 0, + Element::Title(_) + | Element::Message(_) + | Element::Origin(_) + | Element::Padding(_) => 0, Element::Cause(cause) => { let end = cause .markers diff --git a/src/snippet.rs b/src/snippet.rs index af06326..ef92ff4 100644 --- a/src/snippet.rs +++ b/src/snippet.rs @@ -63,6 +63,7 @@ impl<'a> Group<'a> { #[non_exhaustive] pub enum Element<'a> { Title(Title<'a>), + Message(Message<'a>), Cause(Snippet<'a, Annotation<'a>>), Suggestion(Snippet<'a, Patch<'a>>), Origin(Origin<'a>), @@ -75,6 +76,12 @@ impl<'a> From> for Element<'a> { } } +impl<'a> From> for Element<'a> { + fn from(value: Message<'a>) -> Self { + Element::Message(value) + } +} + impl<'a> From>> for Element<'a> { fn from(value: Snippet<'a, Annotation<'a>>) -> Self { Element::Cause(value) @@ -103,15 +110,14 @@ impl From for Element<'_> { #[derive(Clone, Debug)] pub struct Padding; -/// A text [`Element`] in a [`Group`] +/// A text [`Element`] to start a [`Group`] /// /// See [`Level::title`] to create this. #[derive(Clone, Debug)] pub struct Title<'a> { pub(crate) level: Level<'a>, pub(crate) id: Option>, - pub(crate) title: Cow<'a, str>, - pub(crate) is_pre_styled: bool, + pub(crate) text: Cow<'a, str>, } impl<'a> Title<'a> { @@ -144,6 +150,15 @@ impl<'a> Title<'a> { } } +/// A text [`Element`] in a [`Group`] +/// +/// See [`Level::message`] to create this. +#[derive(Clone, Debug)] +pub struct Message<'a> { + pub(crate) level: Level<'a>, + pub(crate) text: Cow<'a, str>, +} + /// A source view [`Element`] in a [`Group`] /// /// If you do not have [source][Snippet::source] available, see instead [`Origin`] diff --git a/tests/examples.rs b/tests/examples.rs index db00bc1..226c31f 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -50,9 +50,9 @@ fn highlight_source() { } #[test] -fn highlight_title() { - let target = "highlight_title"; - let expected = snapbox::file!["../examples/highlight_title.svg": TermSvg]; +fn highlight_message() { + let target = "highlight_message"; + let expected = snapbox::file!["../examples/highlight_message.svg": TermSvg]; assert_example(target, expected); } diff --git a/tests/formatter.rs b/tests/formatter.rs index 6ebfd94..fe16ca9 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -199,7 +199,7 @@ error: #[test] fn test_format_footer_title() { let input = &[Group::with_title(Level::ERROR.title("")) - .element(Level::ERROR.title("This __is__ a title"))]; + .element(Level::ERROR.message("This __is__ a title"))]; let expected = str![[r#" error: | @@ -2258,7 +2258,9 @@ fn main() { .label("`+` cannot be used to concatenate two `&str` strings"), ), ) - .element(Level::NOTE.title("string concatenation requires an owned `String` on the left")), + .element( + Level::NOTE.message("string concatenation requires an owned `String` on the left"), + ), Group::with_title(Level::HELP.title("create an owned `String` from a string reference")) .element( Snippet::source(source) @@ -2333,7 +2335,7 @@ fn foo() { .annotation(AnnotationKind::Primary.span(0..0)), ) - .element(Level::NOTE.title("this error originates in the macro `include` (in Nightly builds, run with -Z macro-backtrace for more info)")), + .element(Level::NOTE.message("this error originates in the macro `include` (in Nightly builds, run with -Z macro-backtrace for more info)")), ]; let expected_ascii = str![[r#" diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs index 4b1944a..c9bba62 100644 --- a/tests/rustc_tests.rs +++ b/tests/rustc_tests.rs @@ -1659,8 +1659,8 @@ fn main() {} .annotation(AnnotationKind::Context.span(878..880).label("not covered")) .annotation(AnnotationKind::Context.span(890..892).label("not covered")) ) - .element(Level::NOTE.title("the matched value is of type `NonEmptyEnum5`")) - .element(Level::NOTE.title("match arms with guards don't count towards exhaustivity") + .element(Level::NOTE.message("the matched value is of type `NonEmptyEnum5`")) + .element(Level::NOTE.message("match arms with guards don't count towards exhaustivity") ), Group::with_title( Level::HELP @@ -1749,7 +1749,7 @@ fn main() { .primary(true) ) .element(Padding) - .element(Level::NOTE.title("...because it uses `Self` as a type parameter")) + .element(Level::NOTE.message("...because it uses `Self` as a type parameter")) .element( Snippet::source(source) .line_start(1) @@ -2795,9 +2795,9 @@ fn main() { .path("lint_example.rs") .annotation(AnnotationKind::Primary.span(40..49)), ) - .element(Level::WARNING.title("this changes meaning in Rust 2021")) - .element(Level::NOTE.title(long_title2)) - .element(Level::NOTE.title("`#[warn(array_into_iter)]` on by default")), + .element(Level::WARNING.message("this changes meaning in Rust 2021")) + .element(Level::NOTE.message(long_title2)) + .element(Level::NOTE.message("`#[warn(array_into_iter)]` on by default")), Group::with_title( Level::HELP.title("use `.iter()` instead of `.into_iter()` to avoid ambiguity"), )