From 8cd2f36c779657ba1ed4fdad5ae59d3a63a0ad7f Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:31:24 -0500 Subject: [PATCH 1/7] refactor: Generalize Title::title to Title::text --- src/level.rs | 8 ++++---- src/renderer/mod.rs | 4 ++-- src/snippet.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/level.rs b/src/level.rs index 036cf7d..4fec995 100644 --- a/src/level.rs +++ b/src/level.rs @@ -80,11 +80,11 @@ 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(), + text: text.into(), is_pre_styled: false, } } @@ -97,11 +97,11 @@ 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> { + pub fn pre_styled_title(self, text: impl Into>) -> Title<'a> { Title { level: self, id: None, - title: title.into(), + text: text.into(), is_pre_styled: true, } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 608613b..0970c74 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -585,9 +585,9 @@ impl Renderer { }); let (title_str, style) = if title.is_pre_styled { - (title.title.to_string(), ElementStyle::NoStyle) + (title.text.to_string(), 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 { diff --git a/src/snippet.rs b/src/snippet.rs index af06326..4ea0ed7 100644 --- a/src/snippet.rs +++ b/src/snippet.rs @@ -110,7 +110,7 @@ pub struct Padding; pub struct Title<'a> { pub(crate) level: Level<'a>, pub(crate) id: Option>, - pub(crate) title: Cow<'a, str>, + pub(crate) text: Cow<'a, str>, pub(crate) is_pre_styled: bool, } From 6cefcbb243ff9d7e3e58f8a3faa77642cb38c54d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:36:20 -0500 Subject: [PATCH 2/7] docs(examples): Clarify we are highlighting a message, not a title --- examples/highlight_title.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/highlight_title.rs b/examples/highlight_title.rs index 1de99cf..5357558 100644 --- a/examples/highlight_title.rs +++ b/examples/highlight_title.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.pre_styled_title(&message)), Group::with_title(Level::NOTE.title("function defined here")).element( Snippet::source(source) .path("$DIR/highlighting.rs") From 3a598feee1376ccacc1d76d7388c0426249f2f9e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:37:01 -0500 Subject: [PATCH 3/7] docs(examples): Clarify we are highlighting a message, not a title --- examples/{highlight_title.rs => highlight_message.rs} | 0 examples/{highlight_title.svg => highlight_message.svg} | 0 tests/examples.rs | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) rename examples/{highlight_title.rs => highlight_message.rs} (100%) rename examples/{highlight_title.svg => highlight_message.svg} (100%) diff --git a/examples/highlight_title.rs b/examples/highlight_message.rs similarity index 100% rename from examples/highlight_title.rs rename to examples/highlight_message.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/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); } From ea73333d9d33b5f1218e1a0cded09d9e0cde9965 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:38:16 -0500 Subject: [PATCH 4/7] fix: Rename Level::pre_styled_title to Level::message --- examples/highlight_message.rs | 2 +- src/level.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/highlight_message.rs b/examples/highlight_message.rs index 5357558..aaf4910 100644 --- a/examples/highlight_message.rs +++ b/examples/highlight_message.rs @@ -57,7 +57,7 @@ fn main() { .label("arguments to this function are incorrect"), ), ) - .element(Level::NOTE.pre_styled_title(&message)), + .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/src/level.rs b/src/level.rs index 4fec995..972a2dd 100644 --- a/src/level.rs +++ b/src/level.rs @@ -97,7 +97,7 @@ impl<'a> Level<'a> { /// used to normalize untrusted text before it is passed to this function. /// /// - pub fn pre_styled_title(self, text: impl Into>) -> Title<'a> { + pub fn message(self, text: impl Into>) -> Title<'a> { Title { level: self, id: None, From 7ec47b1e2c0a7c0e06b7bc39c1a19cc2d706e976 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:40:13 -0500 Subject: [PATCH 5/7] docs: Switch all messages to Level::message --- examples/elide_header.rs | 2 +- tests/formatter.rs | 8 +++++--- tests/rustc_tests.rs | 12 ++++++------ 3 files changed, 12 insertions(+), 10 deletions(-) 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/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"), ) From 169d8e231926dd7cc5d30ffc5f040d4e73fd840f Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:51:28 -0500 Subject: [PATCH 6/7] docs: Clarify Level::title vs Level::message --- src/level.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/level.rs b/src/level.rs index 972a2dd..7439523 100644 --- a/src/level.rs +++ b/src/level.rs @@ -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 @@ -89,6 +93,8 @@ impl<'a> Level<'a> { } } + /// A text [`Element`][crate::Element] in a [`Group`][crate::Group] + /// ///
/// /// Text passed to this function is allowed to be pre-styled, as such all From 4373542d19cc4363f14ba40f32084c46a6961c07 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 2 Jul 2025 10:47:43 -0500 Subject: [PATCH 7/7] fix: Make Message a distinct type This makes it clearer that we shouldn't set `id` on this. If someone wants to set an `id`, they should create a new `Group` which will have a `Title`. --- src/level.rs | 9 ++--- src/renderer/mod.rs | 99 ++++++++++++++++++++++++++++++++++++++------- src/snippet.rs | 19 ++++++++- 3 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/level.rs b/src/level.rs index 7439523..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; @@ -89,7 +89,6 @@ impl<'a> Level<'a> { level: self, id: None, text: text.into(), - is_pre_styled: false, } } @@ -103,12 +102,10 @@ impl<'a> Level<'a> { /// used to normalize untrusted text before it is passed to this function. /// ///
- pub fn message(self, text: impl Into>) -> Title<'a> { - Title { + pub fn message(self, text: impl Into>) -> Message<'a> { + Message { level: self, - id: None, text: text.into(), - is_pre_styled: true, } } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 0970c74..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.text.to_string(), ElementStyle::NoStyle) + let (title_str, style) = if title.is_pre_styled() { + (title.text().to_owned(), ElementStyle::NoStyle) } else { - (normalize_whitespace(&title.text), 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 4ea0ed7..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,7 +110,7 @@ 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)] @@ -111,7 +118,6 @@ pub struct Title<'a> { pub(crate) level: Level<'a>, pub(crate) id: Option>, pub(crate) text: Cow<'a, str>, - pub(crate) is_pre_styled: bool, } 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`]