From 3ef8a9910d0da47f53be239bd1e5473a2f6410ce Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Date: Tue, 20 Feb 2024 05:49:47 +0100 Subject: [PATCH] chat: auto detect links (#8028) @ConradIrwin here's our current implementation for auto detecting links in the chat. We also fixed an edge case where the close reply to preview button was cut off (rendered off screen). Release Notes: - Added auto detection for links in the chat panel. --------- Co-authored-by: Remco Smits <62463826+RemcoSmitsDev@users.noreply.github.com> --- Cargo.lock | 1 + Cargo.toml | 1 + crates/collab_ui/src/chat_panel.rs | 116 +++++++++++++++++++++++++++-- crates/editor/Cargo.toml | 2 +- crates/rich_text/Cargo.toml | 1 + crates/rich_text/src/rich_text.rs | 46 ++++++++++-- 6 files changed, 155 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce5ea35821a5a49da275e18bdb0a1f0200f9b3a7..1125f84052f5c2174843f2b239136bb075522c08 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 77f91e3e0c59e4ea58bd9c292f3e455ab8251286..84e585d980ef98110bf139e47ca2c1f67d90777d 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 afbfe6a476d345d489e297da0c3168e1a37a933f..5c961e45350c6b1bf46ae11de37d0b05c4c9f53e 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 a838dd65722a1d70284eda87b6f4a02afe8e05a4..2e2143567d98cde685e5d53824140062adea9e2a 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 b9f765d123f54a071475112bd08fffd06b9f0de8..6576b5ec4c6d1ba5fd4e1f2213a27648c02c96a3 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 1063d89db3d65627a22aefbca8f5eb21cde41896..4f61cd5b9a62e64dfd12d8d07bad2c28a663e5aa 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 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() { + 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))); } } }