Improve the chat panel in zed 2 (#3573)

Max Brunsfeld created

* render markdown formatting
* wrap the chat messages
* enable the status bar button

Change summary

crates/collab_ui2/src/chat_panel.rs                | 108 +++--
crates/collab_ui2/src/chat_panel/message_editor.rs | 188 +++++-----
crates/collab_ui2/src/collab_ui.rs                 |   2 
crates/rich_text2/src/rich_text.rs                 | 287 +++++++--------
4 files changed, 286 insertions(+), 299 deletions(-)

Detailed changes

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -8,8 +8,8 @@ use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
     actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
-    ClickEvent, Div, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model,
-    Render, SharedString, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    ClickEvent, Div, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent,
+    ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use language::LanguageRegistry;
 use menu::Confirm;
@@ -36,6 +36,15 @@ mod message_editor;
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<ChatPanel>(cx);
+        });
+    })
+    .detach();
+}
+
 pub struct ChatPanel {
     client: Arc<Client>,
     channel_store: Model<ChannelStore>,
@@ -259,12 +268,10 @@ impl ChatPanel {
                     .justify_between()
                     .z_index(1)
                     .bg(cx.theme().colors().background)
-                    .border()
-                    .border_color(gpui::red())
                     .child(Label::new(
                         self.active_chat
                             .as_ref()
-                            .and_then(|c| Some(c.0.read(cx).channel(cx)?.name.clone()))
+                            .and_then(|c| Some(format!("#{}", c.0.read(cx).channel(cx)?.name)))
                             .unwrap_or_default(),
                     ))
                     .child(
@@ -342,40 +349,50 @@ impl ChatPanel {
             None
         };
 
-        // todo!("render the text with markdown formatting")
-        if is_continuation {
-            h_stack()
-                .child(SharedString::from(text.text.clone()))
-                .child(render_remove(message_id_to_remove, cx))
-                .mb_1()
-                .into_any()
-        } else {
-            v_stack()
-                .child(
-                    h_stack()
-                        .children(
-                            message
-                                .sender
-                                .avatar
-                                .clone()
-                                .map(|avatar| Avatar::data(avatar)),
-                        )
-                        .child(Label::new(message.sender.github_login.clone()))
-                        .child(Label::new(format_timestamp(
-                            message.timestamp,
-                            now,
-                            self.local_timezone,
-                        )))
-                        .child(render_remove(message_id_to_remove, cx)),
-                )
-                .child(
-                    h_stack()
-                        .child(SharedString::from(text.text.clone()))
-                        .child(render_remove(None, cx)),
-                )
-                .mb_1()
-                .into_any()
+        let element_id: ElementId = match message.id {
+            ChannelMessageId::Saved(id) => ("saved-message", id).into(),
+            ChannelMessageId::Pending(id) => ("pending-message", id).into(),
+        };
+
+        let mut result = v_stack()
+            .w_full()
+            .id(element_id)
+            .relative()
+            .group("")
+            .mb_1();
+
+        if !is_continuation {
+            result = result.child(
+                h_stack()
+                    .children(
+                        message
+                            .sender
+                            .avatar
+                            .clone()
+                            .map(|avatar| Avatar::data(avatar)),
+                    )
+                    .child(Label::new(message.sender.github_login.clone()))
+                    .child(Label::new(format_timestamp(
+                        message.timestamp,
+                        now,
+                        self.local_timezone,
+                    ))),
+            );
         }
+
+        result
+            .child(text.element("body".into(), cx))
+            .child(
+                div()
+                    .invisible()
+                    .absolute()
+                    .top_1()
+                    .right_2()
+                    .w_8()
+                    .group_hover("", |this| this.visible())
+                    .child(render_remove(message_id_to_remove, cx)),
+            )
+            .into_any()
     }
 
     fn render_markdown_with_mentions(
@@ -629,7 +646,7 @@ mod tests {
     use super::*;
     use gpui::HighlightStyle;
     use pretty_assertions::assert_eq;
-    use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+    use rich_text::Highlight;
     use util::test::marked_text_ranges;
 
     #[gpui::test]
@@ -677,18 +694,5 @@ mod tests {
                 (ranges[3].clone(), Highlight::SelfMention)
             ]
         );
-        assert_eq!(
-            message.regions,
-            vec![
-                RenderedRegion {
-                    background_kind: Some(BackgroundKind::Mention),
-                    link_url: None
-                },
-                RenderedRegion {
-                    background_kind: Some(BackgroundKind::SelfMention),
-                    link_url: None
-                },
-            ]
-        );
     }
 }

crates/collab_ui2/src/chat_panel/message_editor.rs 🔗

@@ -203,98 +203,96 @@ impl Render for MessageEditor {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use client::{Client, User, UserStore};
-//     use gpui::{TestAppContext, WindowHandle};
-//     use language::{Language, LanguageConfig};
-//     use rpc::proto;
-//     use settings::SettingsStore;
-//     use util::{http::FakeHttpClient, test::marked_text_ranges};
-
-//     #[gpui::test]
-//     async fn test_message_editor(cx: &mut TestAppContext) {
-//         let editor = init_test(cx);
-//         let editor = editor.root(cx);
-
-//         editor.update(cx, |editor, cx| {
-//             editor.set_members(
-//                 vec![
-//                     ChannelMembership {
-//                         user: Arc::new(User {
-//                             github_login: "a-b".into(),
-//                             id: 101,
-//                             avatar: None,
-//                         }),
-//                         kind: proto::channel_member::Kind::Member,
-//                         role: proto::ChannelRole::Member,
-//                     },
-//                     ChannelMembership {
-//                         user: Arc::new(User {
-//                             github_login: "C_D".into(),
-//                             id: 102,
-//                             avatar: None,
-//                         }),
-//                         kind: proto::channel_member::Kind::Member,
-//                         role: proto::ChannelRole::Member,
-//                     },
-//                 ],
-//                 cx,
-//             );
-
-//             editor.editor.update(cx, |editor, cx| {
-//                 editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
-//             });
-//         });
-
-//         cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
-
-//         editor.update(cx, |editor, cx| {
-//             let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
-//             assert_eq!(
-//                 editor.take_message(cx),
-//                 MessageParams {
-//                     text,
-//                     mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-//                 }
-//             );
-//         });
-//     }
-
-//     fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
-//         cx.foreground().forbid_parking();
-
-//         cx.update(|cx| {
-//             let http = FakeHttpClient::with_404_response();
-//             let client = Client::new(http.clone(), cx);
-//             let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-//             cx.set_global(SettingsStore::test(cx));
-//             theme::init((), cx);
-//             language::init(cx);
-//             editor::init(cx);
-//             client::init(&client, cx);
-//             channel::init(&client, user_store, cx);
-//         });
-
-//         let language_registry = Arc::new(LanguageRegistry::test());
-//         language_registry.add(Arc::new(Language::new(
-//             LanguageConfig {
-//                 name: "Markdown".into(),
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_markdown::language()),
-//         )));
-
-//         let editor = cx.add_window(|cx| {
-//             MessageEditor::new(
-//                 language_registry,
-//                 ChannelStore::global(cx),
-//                 cx.add_view(|cx| Editor::auto_height(4, cx)),
-//                 cx,
-//             )
-//         });
-//         cx.foreground().run_until_parked();
-//         editor
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use client::{Client, User, UserStore};
+    use gpui::{Context as _, TestAppContext, VisualContext as _};
+    use language::{Language, LanguageConfig};
+    use rpc::proto;
+    use settings::SettingsStore;
+    use util::{http::FakeHttpClient, test::marked_text_ranges};
+
+    #[gpui::test]
+    async fn test_message_editor(cx: &mut TestAppContext) {
+        let language_registry = init_test(cx);
+
+        let (editor, cx) = cx.add_window_view(|cx| {
+            MessageEditor::new(
+                language_registry,
+                ChannelStore::global(cx),
+                cx.build_view(|cx| Editor::auto_height(4, cx)),
+                cx,
+            )
+        });
+        cx.executor().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            editor.set_members(
+                vec![
+                    ChannelMembership {
+                        user: Arc::new(User {
+                            github_login: "a-b".into(),
+                            id: 101,
+                            avatar: None,
+                        }),
+                        kind: proto::channel_member::Kind::Member,
+                        role: proto::ChannelRole::Member,
+                    },
+                    ChannelMembership {
+                        user: Arc::new(User {
+                            github_login: "C_D".into(),
+                            id: 102,
+                            avatar: None,
+                        }),
+                        kind: proto::channel_member::Kind::Member,
+                        role: proto::ChannelRole::Member,
+                    },
+                ],
+                cx,
+            );
+
+            editor.editor.update(cx, |editor, cx| {
+                editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
+            });
+        });
+
+        cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+
+        editor.update(cx, |editor, cx| {
+            let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
+            assert_eq!(
+                editor.take_message(cx),
+                MessageParams {
+                    text,
+                    mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+                }
+            );
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
+        cx.update(|cx| {
+            let http = FakeHttpClient::with_404_response();
+            let client = Client::new(http.clone(), cx);
+            let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
+            let settings = SettingsStore::test(cx);
+            cx.set_global(settings);
+            theme::init(theme::LoadThemes::JustBase, cx);
+            language::init(cx);
+            editor::init(cx);
+            client::init(&client, cx);
+            channel::init(&client, user_store, cx);
+        });
+
+        let language_registry = Arc::new(LanguageRegistry::test());
+        language_registry.add(Arc::new(Language::new(
+            LanguageConfig {
+                name: "Markdown".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_markdown::language()),
+        )));
+        language_registry
+    }
+}

crates/collab_ui2/src/collab_ui.rs 🔗

@@ -35,7 +35,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     collab_titlebar_item::init(cx);
     collab_panel::init(cx);
     channel_view::init(cx);
-    // chat_panel::init(cx);
+    chat_panel::init(cx);
     notifications::init(&app_state, cx);
 
     // cx.add_global_action(toggle_screen_sharing);

crates/rich_text2/src/rich_text.rs 🔗

@@ -1,13 +1,16 @@
-use std::{ops::Range, sync::Arc};
-
-use anyhow::bail;
 use futures::FutureExt;
-use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle, WindowContext};
+use gpui::{
+    AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement,
+    SharedString, StyledText, UnderlineStyle, WindowContext,
+};
 use language::{HighlightId, Language, LanguageRegistry};
+use std::{ops::Range, sync::Arc};
+use theme::ActiveTheme;
 use util::RangeExt;
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum Highlight {
+    Code,
     Id(HighlightId),
     Highlight(HighlightStyle),
     Mention,
@@ -28,24 +31,10 @@ impl From<HighlightId> for Highlight {
 
 #[derive(Debug, Clone)]
 pub struct RichText {
-    pub text: String,
+    pub text: SharedString,
     pub highlights: Vec<(Range<usize>, Highlight)>,
-    pub region_ranges: Vec<Range<usize>>,
-    pub regions: Vec<RenderedRegion>,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum BackgroundKind {
-    Code,
-    /// A mention background for non-self user.
-    Mention,
-    SelfMention,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RenderedRegion {
-    pub background_kind: Option<BackgroundKind>,
-    pub link_url: Option<String>,
+    pub link_ranges: Vec<Range<usize>>,
+    pub link_urls: Arc<[String]>,
 }
 
 /// Allows one to specify extra links to the rendered markdown, which can be used
@@ -56,89 +45,71 @@ pub struct Mention {
 }
 
 impl RichText {
-    pub fn element(&self, _cx: &mut WindowContext) -> AnyElement {
-        todo!();
+    pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
+        let theme = cx.theme();
+        let code_background = theme.colors().surface_background;
 
-        // let mut region_id = 0;
-        // let view_id = cx.view_id();
-
-        // let regions = self.regions.clone();
-
-        // enum Markdown {}
-        // Text::new(self.text.clone(), style.text.clone())
-        //     .with_highlights(
-        //         self.highlights
-        //             .iter()
-        //             .filter_map(|(range, highlight)| {
-        //                 let style = match highlight {
-        //                     Highlight::Id(id) => id.style(&syntax)?,
-        //                     Highlight::Highlight(style) => style.clone(),
-        //                     Highlight::Mention => style.mention_highlight,
-        //                     Highlight::SelfMention => style.self_mention_highlight,
-        //                 };
-        //                 Some((range.clone(), style))
-        //             })
-        //             .collect::<Vec<_>>(),
-        //     )
-        //     .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| {
-        //         region_id += 1;
-        //         let region = regions[ix].clone();
-        //         if let Some(url) = region.link_url {
-        //             cx.scene().push_cursor_region(CursorRegion {
-        //                 bounds,
-        //                 style: CursorStyle::PointingHand,
-        //             });
-        //             cx.scene().push_mouse_region(
-        //                 MouseRegion::new::<Markdown>(view_id, region_id, bounds)
-        //                     .on_click::<V, _>(MouseButton::Left, move |_, _, cx| {
-        //                         cx.platform().open_url(&url)
-        //                     }),
-        //             );
-        //         }
-        //         if let Some(region_kind) = &region.background_kind {
-        //             let background = match region_kind {
-        //                 BackgroundKind::Code => style.code_background,
-        //                 BackgroundKind::Mention => style.mention_background,
-        //                 BackgroundKind::SelfMention => style.self_mention_background,
-        //             };
-        //             if background.is_some() {
-        //                 cx.scene().push_quad(gpui::Quad {
-        //                     bounds,
-        //                     background,
-        //                     border: Default::default(),
-        //                     corner_radii: (2.0).into(),
-        //                 });
-        //             }
-        //         }
-        //     })
-        //     .with_soft_wrap(true)
-        //     .into_any()
+        InteractiveText::new(
+            id,
+            StyledText::new(self.text.clone()).with_highlights(
+                &cx.text_style(),
+                self.highlights.iter().map(|(range, highlight)| {
+                    (
+                        range.clone(),
+                        match highlight {
+                            Highlight::Code => HighlightStyle {
+                                background_color: Some(code_background),
+                                ..Default::default()
+                            },
+                            Highlight::Id(id) => HighlightStyle {
+                                background_color: Some(code_background),
+                                ..id.style(&theme.syntax()).unwrap_or_default()
+                            },
+                            Highlight::Highlight(highlight) => *highlight,
+                            Highlight::Mention => HighlightStyle {
+                                font_weight: Some(FontWeight::BOLD),
+                                ..Default::default()
+                            },
+                            Highlight::SelfMention => HighlightStyle {
+                                font_weight: Some(FontWeight::BOLD),
+                                ..Default::default()
+                            },
+                        },
+                    )
+                }),
+            ),
+        )
+        .on_click(self.link_ranges.clone(), {
+            let link_urls = self.link_urls.clone();
+            move |ix, cx| cx.open_url(&link_urls[ix])
+        })
+        .into_any_element()
     }
 
-    pub fn add_mention(
-        &mut self,
-        range: Range<usize>,
-        is_current_user: bool,
-        mention_style: HighlightStyle,
-    ) -> anyhow::Result<()> {
-        if range.end > self.text.len() {
-            bail!(
-                "Mention in range {range:?} is outside of bounds for a message of length {}",
-                self.text.len()
-            );
-        }
+    // pub fn add_mention(
+    //     &mut self,
+    //     range: Range<usize>,
+    //     is_current_user: bool,
+    //     mention_style: HighlightStyle,
+    // ) -> anyhow::Result<()> {
+    //     if range.end > self.text.len() {
+    //         bail!(
+    //             "Mention in range {range:?} is outside of bounds for a message of length {}",
+    //             self.text.len()
+    //         );
+    //     }
 
-        if is_current_user {
-            self.region_ranges.push(range.clone());
-            self.regions.push(RenderedRegion {
-                background_kind: Some(BackgroundKind::Mention),
-                link_url: None,
-            });
-        }
-        self.highlights
-            .push((range, Highlight::Highlight(mention_style)));
-        Ok(())
-    }
+    //     if is_current_user {
+    //         self.region_ranges.push(range.clone());
+    //         self.regions.push(RenderedRegion {
+    //             background_kind: Some(BackgroundKind::Mention),
+    //             link_url: None,
+    //         });
+    //     }
+    //     self.highlights
+    //         .push((range, Highlight::Highlight(mention_style)));
+    //     Ok(())
+    // }
 }
 
 pub fn render_markdown_mut(
@@ -146,7 +117,10 @@ pub fn render_markdown_mut(
     mut mentions: &[Mention],
     language_registry: &Arc<LanguageRegistry>,
     language: Option<&Arc<Language>>,
-    data: &mut RichText,
+    text: &mut String,
+    highlights: &mut Vec<(Range<usize>, Highlight)>,
+    link_ranges: &mut Vec<Range<usize>>,
+    link_urls: &mut Vec<String>,
 ) {
     use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 
@@ -158,18 +132,18 @@ pub fn render_markdown_mut(
 
     let options = Options::all();
     for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() {
-        let prev_len = data.text.len();
+        let prev_len = text.len();
         match event {
             Event::Text(t) => {
                 if let Some(language) = &current_language {
-                    render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
+                    render_code(text, highlights, t.as_ref(), language);
                 } else {
                     if let Some(mention) = mentions.first() {
                         if source_range.contains_inclusive(&mention.range) {
                             mentions = &mentions[1..];
                             let range = (prev_len + mention.range.start - source_range.start)
                                 ..(prev_len + mention.range.end - source_range.start);
-                            data.highlights.push((
+                            highlights.push((
                                 range.clone(),
                                 if mention.is_self_mention {
                                     Highlight::SelfMention
@@ -177,19 +151,10 @@ pub fn render_markdown_mut(
                                     Highlight::Mention
                                 },
                             ));
-                            data.region_ranges.push(range);
-                            data.regions.push(RenderedRegion {
-                                background_kind: Some(if mention.is_self_mention {
-                                    BackgroundKind::SelfMention
-                                } else {
-                                    BackgroundKind::Mention
-                                }),
-                                link_url: None,
-                            });
                         }
                     }
 
-                    data.text.push_str(t.as_ref());
+                    text.push_str(t.as_ref());
                     let mut style = HighlightStyle::default();
                     if bold_depth > 0 {
                         style.font_weight = Some(FontWeight::BOLD);
@@ -198,11 +163,8 @@ pub fn render_markdown_mut(
                         style.font_style = Some(FontStyle::Italic);
                     }
                     if let Some(link_url) = link_url.clone() {
-                        data.region_ranges.push(prev_len..data.text.len());
-                        data.regions.push(RenderedRegion {
-                            link_url: Some(link_url),
-                            background_kind: None,
-                        });
+                        link_ranges.push(prev_len..text.len());
+                        link_urls.push(link_url);
                         style.underline = Some(UnderlineStyle {
                             thickness: 1.0.into(),
                             ..Default::default()
@@ -211,27 +173,25 @@ pub fn render_markdown_mut(
 
                     if style != HighlightStyle::default() {
                         let mut new_highlight = true;
-                        if let Some((last_range, last_style)) = data.highlights.last_mut() {
+                        if let Some((last_range, last_style)) = highlights.last_mut() {
                             if last_range.end == prev_len
                                 && last_style == &Highlight::Highlight(style)
                             {
-                                last_range.end = data.text.len();
+                                last_range.end = text.len();
                                 new_highlight = false;
                             }
                         }
                         if new_highlight {
-                            data.highlights
-                                .push((prev_len..data.text.len(), Highlight::Highlight(style)));
+                            highlights.push((prev_len..text.len(), Highlight::Highlight(style)));
                         }
                     }
                 }
             }
             Event::Code(t) => {
-                data.text.push_str(t.as_ref());
-                data.region_ranges.push(prev_len..data.text.len());
+                text.push_str(t.as_ref());
                 if link_url.is_some() {
-                    data.highlights.push((
-                        prev_len..data.text.len(),
+                    highlights.push((
+                        prev_len..text.len(),
                         Highlight::Highlight(HighlightStyle {
                             underline: Some(UnderlineStyle {
                                 thickness: 1.0.into(),
@@ -241,19 +201,19 @@ pub fn render_markdown_mut(
                         }),
                     ));
                 }
-                data.regions.push(RenderedRegion {
-                    background_kind: Some(BackgroundKind::Code),
-                    link_url: link_url.clone(),
-                });
+                if let Some(link_url) = link_url.clone() {
+                    link_ranges.push(prev_len..text.len());
+                    link_urls.push(link_url);
+                }
             }
             Event::Start(tag) => match tag {
-                Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack),
+                Tag::Paragraph => new_paragraph(text, &mut list_stack),
                 Tag::Heading(_, _, _) => {
-                    new_paragraph(&mut data.text, &mut list_stack);
+                    new_paragraph(text, &mut list_stack);
                     bold_depth += 1;
                 }
                 Tag::CodeBlock(kind) => {
-                    new_paragraph(&mut data.text, &mut list_stack);
+                    new_paragraph(text, &mut list_stack);
                     current_language = if let CodeBlockKind::Fenced(language) = kind {
                         language_registry
                             .language_for_name(language.as_ref())
@@ -273,18 +233,18 @@ pub fn render_markdown_mut(
                     let len = list_stack.len();
                     if let Some((list_number, has_content)) = list_stack.last_mut() {
                         *has_content = false;
-                        if !data.text.is_empty() && !data.text.ends_with('\n') {
-                            data.text.push('\n');
+                        if !text.is_empty() && !text.ends_with('\n') {
+                            text.push('\n');
                         }
                         for _ in 0..len - 1 {
-                            data.text.push_str("  ");
+                            text.push_str("  ");
                         }
                         if let Some(number) = list_number {
-                            data.text.push_str(&format!("{}. ", number));
+                            text.push_str(&format!("{}. ", number));
                             *number += 1;
                             *has_content = false;
                         } else {
-                            data.text.push_str("- ");
+                            text.push_str("- ");
                         }
                     }
                 }
@@ -299,8 +259,8 @@ pub fn render_markdown_mut(
                 Tag::List(_) => drop(list_stack.pop()),
                 _ => {}
             },
-            Event::HardBreak => data.text.push('\n'),
-            Event::SoftBreak => data.text.push(' '),
+            Event::HardBreak => text.push('\n'),
+            Event::SoftBreak => text.push(' '),
             _ => {}
         }
     }
@@ -312,18 +272,35 @@ pub fn render_markdown(
     language_registry: &Arc<LanguageRegistry>,
     language: Option<&Arc<Language>>,
 ) -> RichText {
-    let mut data = RichText {
-        text: Default::default(),
-        highlights: Default::default(),
-        region_ranges: Default::default(),
-        regions: Default::default(),
-    };
-
-    render_markdown_mut(&block, mentions, language_registry, language, &mut data);
+    // let mut data = RichText {
+    //     text: Default::default(),
+    //     highlights: Default::default(),
+    //     region_ranges: Default::default(),
+    //     regions: Default::default(),
+    // };
 
-    data.text = data.text.trim().to_string();
+    let mut text = String::new();
+    let mut highlights = Vec::new();
+    let mut link_ranges = Vec::new();
+    let mut link_urls = Vec::new();
+    render_markdown_mut(
+        &block,
+        mentions,
+        language_registry,
+        language,
+        &mut text,
+        &mut highlights,
+        &mut link_ranges,
+        &mut link_urls,
+    );
+    text.truncate(text.trim_end().len());
 
-    data
+    RichText {
+        text: SharedString::from(text),
+        link_urls: link_urls.into(),
+        link_ranges,
+        highlights,
+    }
 }
 
 pub fn render_code(
@@ -334,11 +311,19 @@ pub fn render_code(
 ) {
     let prev_len = text.len();
     text.push_str(content);
+    let mut offset = 0;
     for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
+        if range.start > offset {
+            highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
+        }
         highlights.push((
             prev_len + range.start..prev_len + range.end,
             Highlight::Id(highlight_id),
         ));
+        offset = range.end;
+    }
+    if offset < content.len() {
+        highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
     }
 }