Highlight mentions in the Saved chat messages

Piotr Osiewicz created

Change summary

crates/collab_ui/src/chat_panel.rs  | 25 +++++++++++--
crates/rich_text/src/rich_text.rs   | 53 ++++++++++++++++++++++++++++--
crates/theme/src/theme.rs           |  1 
styles/src/style_tree/chat_panel.ts | 30 +++++++---------
4 files changed, 82 insertions(+), 27 deletions(-)

Detailed changes

crates/collab_ui/src/chat_panel.rs 🔗

@@ -366,13 +366,26 @@ impl ChatPanel {
         };
 
         let is_pending = message.is_pending();
-        let text = self
-            .markdown_data
-            .entry(message.id)
-            .or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
+        let theme = theme::current(cx);
+        let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+            let mut markdown =
+                rich_text::render_markdown(message.body.clone(), &self.languages, None);
+            let self_client_id = self.client.id();
+            for (mention_range, user_id) in message.mentions {
+                let is_current_user = self_client_id == user_id;
+                markdown
+                    .add_mention(
+                        mention_range,
+                        is_current_user,
+                        theme.chat_panel.mention_highlight.clone(),
+                    )
+                    .log_err();
+            }
+            markdown
+        });
 
         let now = OffsetDateTime::now_utc();
-        let theme = theme::current(cx);
+
         let style = if is_pending {
             &theme.chat_panel.pending_message
         } else if is_continuation {
@@ -400,6 +413,7 @@ impl ChatPanel {
                             theme.editor.syntax.clone(),
                             style.body.clone(),
                             theme.editor.document_highlight_read_background,
+                            theme.chat_panel.self_mention_background,
                             cx,
                         )
                         .flex(1., true),
@@ -456,6 +470,7 @@ impl ChatPanel {
                                     theme.editor.syntax.clone(),
                                     style.body.clone(),
                                     theme.editor.document_highlight_read_background,
+                                    theme.chat_panel.self_mention_background,
                                     cx,
                                 )
                                 .flex(1., true),

crates/rich_text/src/rich_text.rs 🔗

@@ -1,5 +1,6 @@
 use std::{ops::Range, sync::Arc};
 
+use anyhow::bail;
 use futures::FutureExt;
 use gpui::{
     color::Color,
@@ -25,9 +26,15 @@ pub struct RichText {
     pub regions: Vec<RenderedRegion>,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum BackgroundKind {
+    Code,
+    Mention,
+}
+
 #[derive(Debug, Clone)]
 pub struct RenderedRegion {
-    code: bool,
+    background_kind: Option<BackgroundKind>,
     link_url: Option<String>,
 }
 
@@ -37,6 +44,7 @@ impl RichText {
         syntax: Arc<SyntaxTheme>,
         style: TextStyle,
         code_span_background_color: Color,
+        self_mention_span_background_color: Color,
         cx: &mut ViewContext<V>,
     ) -> AnyElement<V> {
         let mut region_id = 0;
@@ -73,18 +81,49 @@ impl RichText {
                             }),
                     );
                 }
-                if region.code {
+                if region.background_kind == Some(BackgroundKind::Code) {
                     cx.scene().push_quad(gpui::Quad {
                         bounds,
                         background: Some(code_span_background_color),
                         border: Default::default(),
                         corner_radii: (2.0).into(),
                     });
+                } else if region.background_kind == Some(BackgroundKind::Mention) {
+                    cx.scene().push_quad(gpui::Quad {
+                        bounds,
+                        background: Some(self_mention_span_background_color),
+                        border: Default::default(),
+                        corner_radii: (2.0).into(),
+                    });
                 }
             })
             .with_soft_wrap(true)
             .into_any()
     }
+    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(())
+    }
 }
 
 pub fn render_markdown_mut(
@@ -101,7 +140,11 @@ pub fn render_markdown_mut(
     let mut current_language = None;
     let mut list_stack = Vec::new();
 
-    for event in Parser::new_ext(&block, Options::all()) {
+    // Smart Punctuation is disabled as that messes with offsets within the message.
+    let mut options = Options::all();
+    options.remove(Options::ENABLE_SMART_PUNCTUATION);
+
+    for event in Parser::new_ext(&block, options) {
         let prev_len = data.text.len();
         match event {
             Event::Text(t) => {
@@ -121,7 +164,7 @@ pub fn render_markdown_mut(
                         data.region_ranges.push(prev_len..data.text.len());
                         data.regions.push(RenderedRegion {
                             link_url: Some(link_url),
-                            code: false,
+                            background_kind: None,
                         });
                         style.underline = Some(Underline {
                             thickness: 1.0.into(),
@@ -162,7 +205,7 @@ pub fn render_markdown_mut(
                     ));
                 }
                 data.regions.push(RenderedRegion {
-                    code: true,
+                    background_kind: Some(BackgroundKind::Code),
                     link_url: link_url.clone(),
                 });
             }

crates/theme/src/theme.rs 🔗

@@ -641,6 +641,7 @@ pub struct ChatPanel {
     pub avatar_container: ContainerStyle,
     pub message: ChatMessage,
     pub mention_highlight: HighlightStyle,
+    pub self_mention_background: Color,
     pub continuation_message: ChatMessage,
     pub last_message_bottom_spacing: f32,
     pub pending_message: ChatMessage,

styles/src/style_tree/chat_panel.ts 🔗

@@ -1,11 +1,8 @@
-import {
-    background,
-    border,
-    text,
-} from "./components"
+import { background, border, text } from "./components"
 import { icon_button } from "../component/icon_button"
 import { useTheme } from "../theme"
 import { interactive } from "../element"
+import { Color } from "ayu/dist/color"
 
 export default function chat_panel(): any {
     const theme = useTheme()
@@ -41,15 +38,13 @@ export default function chat_panel(): any {
                 left: 2,
                 top: 2,
                 bottom: 2,
-            }
-        },
-        list: {
-
+            },
         },
+        list: {},
         channel_select: {
             header: {
                 ...channel_name,
-                border: border(layer, { bottom: true })
+                border: border(layer, { bottom: true }),
             },
             item: channel_name,
             active_item: {
@@ -62,8 +57,8 @@ export default function chat_panel(): any {
             },
             menu: {
                 background: background(layer, "on"),
-                border: border(layer, { bottom: true })
-            }
+                border: border(layer, { bottom: true }),
+            },
         },
         icon_button: icon_button({
             variant: "ghost",
@@ -91,7 +86,8 @@ export default function chat_panel(): any {
                 top: 4,
             },
         },
-        mention_highlight: { weight: 'bold' },
+        mention_highlight: { weight: "bold" },
+        self_mention_background: background(layer, "active"),
         message: {
             ...interactive({
                 base: {
@@ -101,7 +97,7 @@ export default function chat_panel(): any {
                         bottom: 4,
                         left: SPACING / 2,
                         right: SPACING / 3,
-                    }
+                    },
                 },
                 state: {
                     hovered: {
@@ -135,7 +131,7 @@ export default function chat_panel(): any {
                         bottom: 4,
                         left: SPACING / 2,
                         right: SPACING / 3,
-                    }
+                    },
                 },
                 state: {
                     hovered: {
@@ -160,7 +156,7 @@ export default function chat_panel(): any {
                         bottom: 4,
                         left: SPACING / 2,
                         right: SPACING / 3,
-                    }
+                    },
                 },
                 state: {
                     hovered: {
@@ -171,6 +167,6 @@ export default function chat_panel(): any {
         },
         sign_in_prompt: {
             default: text(layer, "sans", "base"),
-        }
+        },
     }
 }