Render chat messages in `ChatPanel`

Antonio Scandurra created

Change summary

server/src/rpc.rs     |   2 
zed/src/channel.rs    | 113 ++++++++++++++++++++++++++++++++++----------
zed/src/chat_panel.rs |  66 +++++++++++++++++++++++--
zed/src/workspace.rs  |  10 +++
4 files changed, 155 insertions(+), 36 deletions(-)

Detailed changes

server/src/rpc.rs 🔗

@@ -1520,7 +1520,7 @@ mod tests {
         fn channel_messages(channel: &Channel) -> Vec<(u64, String)> {
             channel
                 .messages()
-                .iter()
+                .cursor::<(), ()>()
                 .map(|m| (m.sender_id, m.body.clone()))
                 .collect()
         }

zed/src/channel.rs 🔗

@@ -3,10 +3,13 @@ use crate::{
     util::log_async_errors,
 };
 use anyhow::{anyhow, Context, Result};
-use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle};
+use gpui::{
+    sum_tree::{self, Bias, SumTree},
+    Entity, ModelContext, ModelHandle, MutableAppContext, WeakModelHandle,
+};
 use std::{
-    cmp::Ordering,
-    collections::{hash_map, BTreeSet, HashMap},
+    collections::{hash_map, HashMap},
+    ops::Range,
     sync::Arc,
 };
 use zrpc::{
@@ -28,13 +31,14 @@ pub struct ChannelDetails {
 
 pub struct Channel {
     details: ChannelDetails,
-    messages: BTreeSet<ChannelMessage>,
+    messages: SumTree<ChannelMessage>,
     pending_messages: Vec<PendingChannelMessage>,
     next_local_message_id: u64,
     rpc: Arc<Client>,
     _subscription: rpc::Subscription,
 }
 
+#[derive(Clone, Debug)]
 pub struct ChannelMessage {
     pub id: u64,
     pub sender_id: u64,
@@ -46,10 +50,26 @@ pub struct PendingChannelMessage {
     local_id: u64,
 }
 
-pub enum Event {}
+#[derive(Clone, Debug, Default)]
+pub struct ChannelMessageSummary {
+    max_id: u64,
+    count: Count,
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+struct Count(usize);
+
+pub enum ChannelListEvent {}
+
+pub enum ChannelEvent {
+    Message {
+        old_range: Range<usize>,
+        message: ChannelMessage,
+    },
+}
 
 impl Entity for ChannelList {
-    type Event = Event;
+    type Event = ChannelListEvent;
 }
 
 impl ChannelList {
@@ -107,7 +127,7 @@ impl ChannelList {
 }
 
 impl Entity for Channel {
-    type Event = ();
+    type Event = ChannelEvent;
 
     fn release(&mut self, cx: &mut MutableAppContext) {
         let rpc = self.rpc.clone();
@@ -132,7 +152,10 @@ impl Channel {
             cx.spawn(|channel, mut cx| async move {
                 match rpc.request(proto::JoinChannel { channel_id }).await {
                     Ok(response) => channel.update(&mut cx, |channel, cx| {
-                        channel.messages = response.messages.into_iter().map(Into::into).collect();
+                        channel.messages = SumTree::new();
+                        channel
+                            .messages
+                            .extend(response.messages.into_iter().map(Into::into), &());
                         cx.notify();
                     }),
                     Err(error) => log::error!("error joining channel: {}", error),
@@ -171,12 +194,14 @@ impl Channel {
                         .binary_search_by_key(&local_id, |msg| msg.local_id)
                     {
                         let body = this.pending_messages.remove(i).body;
-                        this.messages.insert(ChannelMessage {
-                            id: response.message_id,
-                            sender_id: current_user_id,
-                            body,
-                        });
-                        cx.notify();
+                        this.insert_message(
+                            ChannelMessage {
+                                id: response.message_id,
+                                sender_id: current_user_id,
+                                body,
+                            },
+                            cx,
+                        );
                     }
                 });
                 Ok(())
@@ -187,7 +212,7 @@ impl Channel {
         Ok(())
     }
 
-    pub fn messages(&self) -> &BTreeSet<ChannelMessage> {
+    pub fn messages(&self) -> &SumTree<ChannelMessage> {
         &self.messages
     }
 
@@ -209,10 +234,31 @@ impl Channel {
             .payload
             .message
             .ok_or_else(|| anyhow!("empty message"))?;
-        self.messages.insert(message.into());
-        cx.notify();
+        self.insert_message(message.into(), cx);
         Ok(())
     }
+
+    fn insert_message(&mut self, message: ChannelMessage, cx: &mut ModelContext<Self>) {
+        let mut old_cursor = self.messages.cursor::<u64, Count>();
+        let mut new_messages = old_cursor.slice(&message.id, Bias::Left, &());
+        let start_ix = old_cursor.sum_start().0;
+        let mut end_ix = start_ix;
+        if old_cursor.item().map_or(false, |m| m.id == message.id) {
+            old_cursor.next(&());
+            end_ix += 1;
+        }
+
+        new_messages.push(message.clone(), &());
+        new_messages.push_tree(old_cursor.suffix(&()), &());
+        drop(old_cursor);
+        self.messages = new_messages;
+
+        cx.emit(ChannelEvent::Message {
+            old_range: start_ix..end_ix,
+            message,
+        });
+        cx.notify();
+    }
 }
 
 impl From<proto::Channel> for ChannelDetails {
@@ -234,22 +280,35 @@ impl From<proto::ChannelMessage> for ChannelMessage {
     }
 }
 
-impl PartialOrd for ChannelMessage {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
+impl sum_tree::Item for ChannelMessage {
+    type Summary = ChannelMessageSummary;
+
+    fn summary(&self) -> Self::Summary {
+        ChannelMessageSummary {
+            max_id: self.id,
+            count: Count(1),
+        }
     }
 }
 
-impl Ord for ChannelMessage {
-    fn cmp(&self, other: &Self) -> Ordering {
-        self.id.cmp(&other.id)
+impl sum_tree::Summary for ChannelMessageSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.max_id = summary.max_id;
+        self.count.0 += summary.count.0;
     }
 }
 
-impl PartialEq for ChannelMessage {
-    fn eq(&self, other: &Self) -> bool {
-        self.id == other.id
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
+    fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+        debug_assert!(summary.max_id > *self);
+        *self = summary.max_id;
     }
 }
 
-impl Eq for ChannelMessage {}
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
+    fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+        self.0 += summary.count.0;
+    }
+}

zed/src/chat_panel.rs 🔗

@@ -1,20 +1,30 @@
-use super::channel::{Channel, ChannelList};
+use crate::{
+    channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
+    Settings,
+};
 use gpui::{elements::*, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext};
+use postage::watch;
 
 pub struct ChatPanel {
     channel_list: ModelHandle<ChannelList>,
     active_channel: Option<(ModelHandle<Channel>, Subscription)>,
     messages: ListState,
+    settings: watch::Receiver<Settings>,
 }
 
 pub enum Event {}
 
 impl ChatPanel {
-    pub fn new(channel_list: ModelHandle<ChannelList>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        channel_list: ModelHandle<ChannelList>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let mut this = Self {
             channel_list,
             messages: ListState::new(Vec::new()),
             active_channel: None,
+            settings,
         };
 
         this.assign_active_channel(cx);
@@ -39,7 +49,15 @@ impl ChatPanel {
         });
         if let Some(channel) = channel {
             if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
-                let subscription = cx.observe(&channel, Self::channel_did_change);
+                let subscription = cx.subscribe(&channel, Self::channel_did_change);
+                self.messages = ListState::new(
+                    channel
+                        .read(cx)
+                        .messages()
+                        .cursor::<(), ()>()
+                        .map(|m| self.render_message(m))
+                        .collect(),
+                );
                 self.active_channel = Some((channel, subscription));
             }
         } else {
@@ -47,9 +65,42 @@ impl ChatPanel {
         }
     }
 
-    fn channel_did_change(&mut self, _: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
+    fn channel_did_change(
+        &mut self,
+        _: ModelHandle<Channel>,
+        event: &ChannelEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            ChannelEvent::Message { old_range, message } => {
+                self.messages
+                    .splice(old_range.clone(), Some(self.render_message(message)));
+            }
+        }
         cx.notify();
     }
+
+    fn render_active_channel_messages(&self) -> ElementBox {
+        Expanded::new(0.8, List::new(self.messages.clone()).boxed()).boxed()
+    }
+
+    fn render_message(&self, message: &ChannelMessage) -> ElementBox {
+        let settings = self.settings.borrow();
+        Flex::column()
+            .with_child(
+                Label::new(
+                    message.body.clone(),
+                    settings.ui_font_family,
+                    settings.ui_font_size,
+                )
+                .boxed(),
+            )
+            .boxed()
+    }
+
+    fn render_input_box(&self) -> ElementBox {
+        Empty::new().boxed()
+    }
 }
 
 impl Entity for ChatPanel {
@@ -61,7 +112,10 @@ impl View for ChatPanel {
         "ChatPanel"
     }
 
-    fn render(&self, _: &RenderContext<Self>) -> gpui::ElementBox {
-        List::new(self.messages.clone()).boxed()
+    fn render(&self, _: &RenderContext<Self>) -> ElementBox {
+        Flex::column()
+            .with_child(self.render_active_channel_messages())
+            .with_child(self.render_input_box())
+            .boxed()
     }
 }

zed/src/workspace.rs 🔗

@@ -374,8 +374,14 @@ impl Workspace {
         let mut right_sidebar = Sidebar::new(Side::Right);
         right_sidebar.add_item(
             "icons/comment-16.svg",
-            cx.add_view(|cx| ChatPanel::new(app_state.channel_list.clone(), cx))
-                .into(),
+            cx.add_view(|cx| {
+                ChatPanel::new(
+                    app_state.channel_list.clone(),
+                    app_state.settings.clone(),
+                    cx,
+                )
+            })
+            .into(),
         );
         right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());