From 359a6ed10e5f2f065f8cd51a5f5f4164e6291d0b Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Tue, 10 Jun 2025 04:20:52 -0600 Subject: [PATCH 1/2] test: Add hyperlink test --- examples/id_hyperlink.rs | 32 +++++++++++++++++++++++++++++++ examples/id_hyperlink.svg | 40 +++++++++++++++++++++++++++++++++++++++ tests/examples.rs | 7 +++++++ 3 files changed, 79 insertions(+) create mode 100644 examples/id_hyperlink.rs create mode 100644 examples/id_hyperlink.svg diff --git a/examples/id_hyperlink.rs b/examples/id_hyperlink.rs new file mode 100644 index 00000000..7c3ace1e --- /dev/null +++ b/examples/id_hyperlink.rs @@ -0,0 +1,32 @@ +use annotate_snippets::renderer::OutputTheme; +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; + +fn main() { + let source = r#"//@ compile-flags: -Zterminal-urls=yes +fn main() { + let () = 4; //~ ERROR +} +"#; + + let message = Level::ERROR.header("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .path("$DIR/terminal_urls.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(59..61) + .label("expected integer, found `()`"), + ) + .annotation( + AnnotationKind::Context + .span(64..65) + .label("this expression has type `{integer}`"), + ), + ), + ); + + let renderer = Renderer::styled().theme(OutputTheme::Unicode); + anstream::println!("{}", renderer.render(message)); +} diff --git a/examples/id_hyperlink.svg b/examples/id_hyperlink.svg new file mode 100644 index 00000000..64dbfe18 --- /dev/null +++ b/examples/id_hyperlink.svg @@ -0,0 +1,40 @@ + + + + + + + error[E0308]: mismatched types + + ╭▸ $DIR/terminal_urls.rs:3:9 + + + + 3 let () = 4; //~ ERROR + + ┯━ this expression has type `{integer}` + + + + ╰╴ expected integer, found `()` + + + + + + diff --git a/tests/examples.rs b/tests/examples.rs index 02e961c8..ec2643e9 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -49,6 +49,13 @@ fn highlight_title() { assert_example(target, expected); } +#[test] +fn id_hyperlink() { + let target = "id_hyperlink"; + let expected = snapbox::file!["../examples/id_hyperlink.svg": TermSvg]; + assert_example(target, expected); +} + #[test] fn multislice() { let target = "multislice"; From bff9dd5f88bcba80e8c4aa7ecc60a8f178e46137 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Thu, 5 Jun 2025 20:14:19 -0600 Subject: [PATCH 2/2] feat: Add support for ID hyperlinks --- Cargo.lock | 20 +++++++------------ examples/id_hyperlink.rs | 41 +++++++++++++++++++++------------------ examples/id_hyperlink.svg | 2 +- src/renderer/mod.rs | 19 ++++++++++++++++-- src/snippet.rs | 20 +++++++++++++++++-- 5 files changed, 65 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 532bb57a..8cfa6638 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,7 +12,7 @@ dependencies = [ "divan", "memchr", "snapbox", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -47,9 +47,9 @@ dependencies = [ [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] @@ -65,15 +65,15 @@ dependencies = [ [[package]] name = "anstyle-svg" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbf0bf947d663010f0b4132f28ca08da9151f3b9035fa7578a38de521c1d1aa" +checksum = "0a43964079ef399480603125d5afae2b219aceffb77478956e25f17b9bc3435c" dependencies = [ - "anstream", "anstyle", "anstyle-lossy", + "anstyle-parse", "html-escape", - "unicode-width 0.1.13", + "unicode-width", ] [[package]] @@ -418,12 +418,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - [[package]] name = "unicode-width" version = "0.2.0" diff --git a/examples/id_hyperlink.rs b/examples/id_hyperlink.rs index 7c3ace1e..209fc15b 100644 --- a/examples/id_hyperlink.rs +++ b/examples/id_hyperlink.rs @@ -7,25 +7,28 @@ fn main() { let () = 4; //~ ERROR } "#; - - let message = Level::ERROR.header("mismatched types").id("E0308").group( - Group::new().element( - Snippet::source(source) - .line_start(1) - .path("$DIR/terminal_urls.rs") - .fold(true) - .annotation( - AnnotationKind::Primary - .span(59..61) - .label("expected integer, found `()`"), - ) - .annotation( - AnnotationKind::Context - .span(64..65) - .label("this expression has type `{integer}`"), - ), - ), - ); + let message = Level::ERROR + .header("mismatched types") + .id("E0308") + .id_url("https://doc.rust-lang.org/error_codes/E0308.html") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .path("$DIR/terminal_urls.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(59..61) + .label("expected integer, found `()`"), + ) + .annotation( + AnnotationKind::Context + .span(64..65) + .label("this expression has type `{integer}`"), + ), + ), + ); let renderer = Renderer::styled().theme(OutputTheme::Unicode); anstream::println!("{}", renderer.render(message)); diff --git a/examples/id_hyperlink.svg b/examples/id_hyperlink.svg index 64dbfe18..5caa4114 100644 --- a/examples/id_hyperlink.svg +++ b/examples/id_hyperlink.svg @@ -19,7 +19,7 @@ - error[E0308]: mismatched types + error[E0308]: mismatched types ╭▸ $DIR/terminal_urls.rs:3:9 diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 39c805b7..a5459734 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -46,6 +46,7 @@ use crate::renderer::source_map::{ AnnotatedLineInfo, LineInfo, Loc, SourceMap, SubstitutionHighlight, }; use crate::renderer::styled_buffer::StyledBuffer; +use crate::snippet::Id; use crate::{Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Snippet, Title}; pub use anstyle::*; use margin::Margin; @@ -545,7 +546,7 @@ impl Renderer { title: &Title<'_>, max_line_num_len: usize, title_style: TitleStyle, - id: Option<&&str>, + id: Option<&Id<'_>>, is_cont: bool, buffer_msg_line_offset: usize, ) { @@ -620,17 +621,31 @@ impl Renderer { ); } label_width += title.level.as_str().len(); - if let Some(id) = id { + if let Some(Id { id: Some(id), url }) = id { buffer.append( buffer_msg_line_offset, "[", ElementStyle::Level(title.level.level), ); + if let Some(url) = url.as_ref() { + buffer.append( + buffer_msg_line_offset, + &format!("\x1B]8;;{url}\x1B\\"), + ElementStyle::Level(title.level.level), + ); + } buffer.append( buffer_msg_line_offset, id, ElementStyle::Level(title.level.level), ); + if url.is_some() { + buffer.append( + buffer_msg_line_offset, + "\x1B]8;;\x1B\\", + ElementStyle::Level(title.level.level), + ); + } buffer.append( buffer_msg_line_offset, "]", diff --git a/src/snippet.rs b/src/snippet.rs index f3c517bb..2d686a5e 100644 --- a/src/snippet.rs +++ b/src/snippet.rs @@ -13,7 +13,7 @@ pub(crate) const WARNING_TXT: &str = "warning"; /// Top-level user message #[derive(Clone, Debug)] pub struct Message<'a> { - pub(crate) id: Option<&'a str>, // for "correctness", could be sloppy and be on Title + pub(crate) id: Option>, // for "correctness", could be sloppy and be on Title pub(crate) groups: Vec>, } @@ -26,7 +26,17 @@ impl<'a> Message<'a> { /// /// pub fn id(mut self, id: &'a str) -> Self { - self.id = Some(id); + self.id.get_or_insert(Id::default()).id = Some(id); + self + } + + ///
+ /// + /// This is only relevant if the `id` present + /// + ///
+ pub fn id_url(mut self, url: &'a str) -> Self { + self.id.get_or_insert(Id::default()).url = Some(url); self } @@ -75,6 +85,12 @@ impl<'a> Message<'a> { } } +#[derive(Clone, Debug, Default)] +pub(crate) struct Id<'a> { + pub(crate) id: Option<&'a str>, + pub(crate) url: Option<&'a str>, +} + /// An [`Element`] container #[derive(Clone, Debug)] pub struct Group<'a> {