From dde87f64688af3b57edb40c97624f638c471982c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:05:35 +0200 Subject: [PATCH] markdown preview: Auto detect raw links (#10162) Similar to the work done in `rich_text`, raw links now get picked up in the markdown preview. https://github.com/zed-industries/zed/assets/53836821/3c5173fd-cf8b-4819-ad7f-3127c158acaa Release Notes: - Added support for detecting and highlighting links in markdown preview --- Cargo.lock | 1 + crates/markdown_preview/Cargo.toml | 1 + .../markdown_preview/src/markdown_parser.rs | 98 +++++++++++++++++-- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c88281dfbe..a5a12fa45b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5680,6 +5680,7 @@ dependencies = [ "editor", "gpui", "language", + "linkify", "pretty_assertions", "pulldown-cmark", "theme", diff --git a/crates/markdown_preview/Cargo.toml b/crates/markdown_preview/Cargo.toml index 91bcfe0ba9..82af756e13 100644 --- a/crates/markdown_preview/Cargo.toml +++ b/crates/markdown_preview/Cargo.toml @@ -19,6 +19,7 @@ async-recursion.workspace = true editor.workspace = true gpui.workspace = true language.workspace = true +linkify.workspace = true pretty_assertions.workspace = true pulldown-cmark.workspace = true theme.workspace = true diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 5e78600042..47db011cef 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -188,6 +188,9 @@ impl<'a> MarkdownParser<'a> { let mut regions: Vec = vec![]; let mut highlights: Vec<(Range, MarkdownHighlight)> = vec![]; + let mut link_urls: Vec = vec![]; + let mut link_ranges: Vec> = vec![]; + loop { if self.eof() { break; @@ -226,28 +229,69 @@ impl<'a> MarkdownParser<'a> { style.strikethrough = true; } - if let Some(link) = link.clone() { + let last_run_len = if let Some(link) = link.clone() { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { code: false, link: Some(link), }); style.underline = true; - } + prev_len + } else { + // Manually scan for links + let mut finder = linkify::LinkFinder::new(); + finder.kinds(&[linkify::LinkKind::Url]); + let mut last_link_len = prev_len; + for link in finder.links(&t) { + let start = link.start(); + let end = link.end(); + let range = (prev_len + start)..(prev_len + end); + link_ranges.push(range.clone()); + link_urls.push(link.as_str().to_string()); - if style != MarkdownHighlightStyle::default() { + // If there is a style before we match a link, we have to add this to the highlighted ranges + if style != MarkdownHighlightStyle::default() + && last_link_len < link.start() + { + highlights.push(( + last_link_len..link.start(), + MarkdownHighlight::Style(style.clone()), + )); + } + + highlights.push(( + range.clone(), + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, + ..style + }), + )); + region_ranges.push(range.clone()); + regions.push(ParsedRegion { + code: false, + link: Some(Link::Web { + url: t[range].to_string(), + }), + }); + + last_link_len = end; + } + last_link_len + }; + + if style != MarkdownHighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; - if let Some((last_range, MarkdownHighlight::Style(last_style))) = - highlights.last_mut() - { - if last_range.end == prev_len && last_style == &style { + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == last_run_len + && last_style == &MarkdownHighlight::Style(style.clone()) + { last_range.end = text.len(); new_highlight = false; } } if new_highlight { - let range = prev_len..text.len(); - highlights.push((range, MarkdownHighlight::Style(style))); + highlights + .push((last_run_len..text.len(), MarkdownHighlight::Style(style))); } } } @@ -744,6 +788,42 @@ mod tests { ); } + #[gpui::test] + async fn test_raw_links_detection() { + let parsed = parse("Checkout this https://zed.dev link").await; + + assert_eq!( + parsed.children, + vec![p("Checkout this https://zed.dev link", 0..34)] + ); + + let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { + text + } else { + panic!("Expected a paragraph"); + }; + assert_eq!( + paragraph.highlights, + vec![( + 14..29, + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, + ..Default::default() + }), + )] + ); + assert_eq!( + paragraph.regions, + vec![ParsedRegion { + code: false, + link: Some(Link::Web { + url: "https://zed.dev".to_string() + }), + }] + ); + assert_eq!(paragraph.region_ranges, vec![14..29]); + } + #[gpui::test] async fn test_header_only_table() { let markdown = "\