Cargo.lock 🔗
@@ -5680,6 +5680,7 @@ dependencies = [
"editor",
"gpui",
"language",
+ "linkify",
"pretty_assertions",
"pulldown-cmark",
"theme",
Bennet Bo Fenner created
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
crates/markdown_preview/src/markdown_parser.rs | 98 ++++++++++++++++++-
3 files changed, 91 insertions(+), 9 deletions(-)
@@ -5680,6 +5680,7 @@ dependencies = [
"editor",
"gpui",
"language",
+ "linkify",
"pretty_assertions",
"pulldown-cmark",
"theme",
@@ -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
@@ -188,6 +188,9 @@ impl<'a> MarkdownParser<'a> {
let mut regions: Vec<ParsedRegion> = vec![];
let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
+ let mut link_urls: Vec<String> = vec![];
+ let mut link_ranges: Vec<Range<usize>> = 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 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() {
+ 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 = "\