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>
This commit is contained in:
Bennet Bo Fenner 2024-02-20 05:49:47 +01:00 committed by GitHub
parent 1e44bac418
commit 3ef8a9910d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 155 additions and 12 deletions

1
Cargo.lock generated
View File

@ -7555,6 +7555,7 @@ dependencies = [
"gpui",
"language",
"lazy_static",
"linkify",
"pulldown-cmark",
"smallvec",
"smol",

View File

@ -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"

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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),
));
}
if style != HighlightStyle::default() {
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)));
}
}
}