diff --git a/Cargo.lock b/Cargo.lock index ce5ea35821..1125f84052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7555,6 +7555,7 @@ dependencies = [ "gpui", "language", "lazy_static", + "linkify", "pulldown-cmark", "smallvec", "smol", diff --git a/Cargo.toml b/Cargo.toml index 77f91e3e0c..84e585d980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,6 +199,7 @@ indoc = "1" # We explicitly disable a http2 support in isahc. isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] } lazy_static = "1.4.0" +linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = "2.1.1" parking_lot = "0.11.1" diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index afbfe6a476..5c961e4535 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -371,9 +371,9 @@ impl ChatPanel { .px_1() .py_0p5() .mb_1() - .overflow_hidden() .child( div() + .overflow_hidden() .max_h_12() .child(reply_to_message_body.element(body_element_id, cx)), ), @@ -840,18 +840,21 @@ impl Render for ChatPanel { el.when_some(reply_message, |el, reply_message| { el.child( - div() + h_flex() .when(!self.is_scrolled_to_bottom, |el| { el.border_t_1().border_color(cx.theme().colors().border) }) - .flex() - .w_full() - .items_start() + .justify_between() .overflow_hidden() + .items_start() .py_1() .px_2() .bg(cx.theme().colors().background) - .child(self.render_replied_to_message(None, &reply_message, cx)) + .child( + div().flex_shrink().overflow_hidden().child( + self.render_replied_to_message(None, &reply_message, cx), + ), + ) .child( IconButton::new("close-reply-preview", IconName::Close) .shape(ui::IconButtonShape::Square) @@ -1094,6 +1097,107 @@ mod tests { ); } + #[gpui::test] + fn test_render_markdown_with_auto_detect_links() { + let language_registry = Arc::new(LanguageRegistry::test()); + let message = channel::ChannelMessage { + id: ChannelMessageId::Saved(0), + body: "Here is a link https://zed.dev to zeds website".to_string(), + timestamp: OffsetDateTime::now_utc(), + sender: Arc::new(client::User { + github_login: "fgh".into(), + avatar_uri: "avatar_fgh".into(), + id: 103, + }), + nonce: 5, + mentions: Vec::new(), + reply_to_message_id: None, + }; + + let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message); + + // Note that the "'" was replaced with ’ due to smart punctuation. + let (body, ranges) = + marked_text_ranges("Here is a link «https://zed.dev» to zeds website", false); + assert_eq!(message.text, body); + assert_eq!(1, ranges.len()); + assert_eq!( + message.highlights, + vec![( + ranges[0].clone(), + HighlightStyle { + underline: Some(gpui::UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + } + .into() + ),] + ); + } + + #[gpui::test] + fn test_render_markdown_with_auto_detect_links_and_additional_formatting() { + let language_registry = Arc::new(LanguageRegistry::test()); + let message = channel::ChannelMessage { + id: ChannelMessageId::Saved(0), + body: "**Here is a link https://zed.dev to zeds website**".to_string(), + timestamp: OffsetDateTime::now_utc(), + sender: Arc::new(client::User { + github_login: "fgh".into(), + avatar_uri: "avatar_fgh".into(), + id: 103, + }), + nonce: 5, + mentions: Vec::new(), + reply_to_message_id: None, + }; + + let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message); + + // Note that the "'" was replaced with ’ due to smart punctuation. + let (body, ranges) = marked_text_ranges( + "«Here is a link »«https://zed.dev»« to zeds website»", + false, + ); + assert_eq!(message.text, body); + assert_eq!(3, ranges.len()); + assert_eq!( + message.highlights, + vec![ + ( + ranges[0].clone(), + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + } + .into() + ), + ( + ranges[1].clone(), + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + underline: Some(gpui::UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + } + .into() + ), + ( + ranges[2].clone(), + HighlightStyle { + font_weight: Some(gpui::FontWeight::BOLD), + ..Default::default() + } + .into() + ), + ] + ); + } + #[test] fn test_format_locale() { let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index a838dd6572..2e2143567d 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -40,7 +40,7 @@ indoc = "1.0.4" itertools = "0.10" language.workspace = true lazy_static.workspace = true -linkify = "0.10.0" +linkify.workspace = true log.workspace = true lsp.workspace = true multi_buffer.workspace = true diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml index b9f765d123..6576b5ec4c 100644 --- a/crates/rich_text/Cargo.toml +++ b/crates/rich_text/Cargo.toml @@ -22,6 +22,7 @@ futures.workspace = true gpui.workspace = true language.workspace = true lazy_static.workspace = true +linkify.workspace = true pulldown-cmark.workspace = true smallvec.workspace = true smol.workspace = true diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 1063d89db3..4f61cd5b9a 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -175,19 +175,54 @@ pub fn render_markdown_mut( if italic_depth > 0 { style.font_style = Some(FontStyle::Italic); } - if let Some(link_url) = link_url.clone() { + let last_run_len = if let Some(link_url) = link_url.clone() { link_ranges.push(prev_len..text.len()); link_urls.push(link_url); style.underline = Some(UnderlineStyle { thickness: 1.0.into(), ..Default::default() }); - } + 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 != HighlightStyle::default() { + // If there is a style before we match a link, we have to add this to the highlighted ranges + if style != HighlightStyle::default() && last_link_len < link.start() { + highlights.push(( + last_link_len..link.start(), + Highlight::Highlight(style), + )); + } + + highlights.push(( + range, + Highlight::Highlight(HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..style + }), + )); + + last_link_len = end; + } + last_link_len + }; + + if style != HighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == prev_len + if last_range.end == last_run_len && last_style == &Highlight::Highlight(style) { last_range.end = text.len(); @@ -195,7 +230,8 @@ pub fn render_markdown_mut( } } if new_highlight { - highlights.push((prev_len..text.len(), Highlight::Highlight(style))); + highlights + .push((last_run_len..text.len(), Highlight::Highlight(style))); } } }