Enable chat panel in zed 2 (#3564)

Max Brunsfeld created

Change summary

crates/assistant2/src/assistant.rs                      |    4 
crates/assistant2/src/assistant_panel.rs                |    7 
crates/channel2/src/channel_store.rs                    |   11 
crates/channel2/src/channel_store/channel_index.rs      |    8 
crates/collab2/src/tests/channel_tests.rs               |   64 
crates/collab2/src/tests/random_channel_buffer_tests.rs |    8 
crates/collab_ui2/src/chat_panel.rs                     | 1677 ++++------
crates/collab_ui2/src/chat_panel/message_editor.rs      |  255 
crates/collab_ui2/src/collab_panel.rs                   |   29 
crates/collab_ui2/src/collab_ui.rs                      |    7 
crates/gpui2/src/app.rs                                 |    5 
crates/gpui2/src/elements/list.rs                       |  496 +++
crates/gpui2/src/elements/mod.rs                        |    2 
crates/gpui2/src/elements/uniform_list.rs               |    2 
crates/gpui2/src/gpui2.rs                               |   45 
crates/gpui2/src/key_dispatch.rs                        |   14 
crates/gpui2/src/shared_string.rs                       |  101 
crates/rich_text2/src/rich_text.rs                      |    9 
crates/workspace2/src/status_bar.rs                     |   10 
crates/zed2/src/zed2.rs                                 |   10 
20 files changed, 1,522 insertions(+), 1,242 deletions(-)

Detailed changes

crates/assistant2/src/assistant.rs ๐Ÿ”—

@@ -12,7 +12,7 @@ use chrono::{DateTime, Local};
 use collections::HashMap;
 use fs::Fs;
 use futures::StreamExt;
-use gpui::{actions, AppContext};
+use gpui::{actions, AppContext, SharedString};
 use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
@@ -47,7 +47,7 @@ struct MessageMetadata {
 enum MessageStatus {
     Pending,
     Done,
-    Error(Arc<str>),
+    Error(SharedString),
 }
 
 #[derive(Serialize, Deserialize)]

crates/assistant2/src/assistant_panel.rs ๐Ÿ”—

@@ -1628,8 +1628,9 @@ impl Conversation {
                                     metadata.status = MessageStatus::Done;
                                 }
                                 Err(error) => {
-                                    metadata.status =
-                                        MessageStatus::Error(error.to_string().trim().into());
+                                    metadata.status = MessageStatus::Error(SharedString::from(
+                                        error.to_string().trim().to_string(),
+                                    ));
                                 }
                             }
                             cx.notify();
@@ -2273,7 +2274,7 @@ impl ConversationEditor {
                                         Some(
                                             div()
                                                 .id("error")
-                                                .tooltip(move |cx| Tooltip::text(&error, cx))
+                                                .tooltip(move |cx| Tooltip::text(error.clone(), cx))
                                                 .child(IconElement::new(Icon::XCircle)),
                                         )
                                     } else {

crates/channel2/src/channel_store.rs ๐Ÿ”—

@@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
 use db::RELEASE_CHANNEL;
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
+    WeakModel,
 };
 use rpc::{
     proto::{self, ChannelVisibility},
@@ -46,7 +47,7 @@ pub struct ChannelStore {
 #[derive(Clone, Debug, PartialEq)]
 pub struct Channel {
     pub id: ChannelId,
-    pub name: String,
+    pub name: SharedString,
     pub visibility: proto::ChannelVisibility,
     pub role: proto::ChannelRole,
     pub unseen_note_version: Option<(u64, clock::Global)>,
@@ -895,14 +896,16 @@ impl ChannelStore {
                 .channel_invitations
                 .binary_search_by_key(&channel.id, |c| c.id)
             {
-                Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
+                Ok(ix) => {
+                    Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
+                }
                 Err(ix) => self.channel_invitations.insert(
                     ix,
                     Arc::new(Channel {
                         id: channel.id,
                         visibility: channel.visibility(),
                         role: channel.role(),
-                        name: channel.name,
+                        name: channel.name.into(),
                         unseen_note_version: None,
                         unseen_message_id: None,
                         parent_path: channel.parent_path,

crates/channel2/src/channel_store/channel_index.rs ๐Ÿ”—

@@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
 
             existing_channel.visibility = channel_proto.visibility();
             existing_channel.role = channel_proto.role();
-            existing_channel.name = channel_proto.name;
+            existing_channel.name = channel_proto.name.into();
         } else {
             self.channels_by_id.insert(
                 channel_proto.id,
@@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
                     id: channel_proto.id,
                     visibility: channel_proto.visibility(),
                     role: channel_proto.role(),
-                    name: channel_proto.name,
+                    name: channel_proto.name.into(),
                     unseen_note_version: None,
                     unseen_message_id: None,
                     parent_path: channel_proto.parent_path,
@@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
     let (parent_path, name) = channels_by_id
         .get(&id)
         .map_or((&[] as &[_], None), |channel| {
-            (channel.parent_path.as_slice(), Some(channel.name.as_str()))
+            (channel.parent_path.as_slice(), Some(channel.name.as_ref()))
         });
     parent_path
         .iter()
-        .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+        .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
         .chain(name)
 }
 

crates/collab2/src/tests/channel_tests.rs ๐Ÿ”—

@@ -7,7 +7,7 @@ use call::ActiveCall;
 use channel::{ChannelId, ChannelMembership, ChannelStore};
 use client::User;
 use futures::future::try_join_all;
-use gpui::{BackgroundExecutor, Model, TestAppContext};
+use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
 use rpc::{
     proto::{self, ChannelRole},
     RECEIVE_TIMEOUT,
@@ -46,13 +46,13 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 depth: 0,
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 depth: 1,
                 role: ChannelRole::Admin,
             },
@@ -92,7 +92,7 @@ async fn test_core_channels(
         cx_b,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Member,
         }],
@@ -140,13 +140,13 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 role: ChannelRole::Member,
                 depth: 0,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 role: ChannelRole::Member,
                 depth: 1,
             },
@@ -168,19 +168,19 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 role: ChannelRole::Member,
                 depth: 0,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 role: ChannelRole::Member,
                 depth: 1,
             },
             ExpectedChannel {
                 id: channel_c_id,
-                name: "channel-c".to_string(),
+                name: "channel-c".into(),
                 role: ChannelRole::Member,
                 depth: 2,
             },
@@ -211,19 +211,19 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 depth: 0,
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 depth: 1,
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_c_id,
-                name: "channel-c".to_string(),
+                name: "channel-c".into(),
                 depth: 2,
                 role: ChannelRole::Admin,
             },
@@ -245,7 +245,7 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -255,7 +255,7 @@ async fn test_core_channels(
         cx_b,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -278,7 +278,7 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -309,7 +309,7 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a-renamed".to_string(),
+            name: "channel-a-renamed".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -418,7 +418,7 @@ async fn test_channel_room(
         cx_b,
         &[ExpectedChannel {
             id: zed_id,
-            name: "zed".to_string(),
+            name: "zed".into(),
             depth: 0,
             role: ChannelRole::Member,
         }],
@@ -680,7 +680,7 @@ async fn test_permissions_update_while_invited(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust".to_string(),
+            name: "rust".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -708,7 +708,7 @@ async fn test_permissions_update_while_invited(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust".to_string(),
+            name: "rust".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -747,7 +747,7 @@ async fn test_channel_rename(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust-archive".to_string(),
+            name: "rust-archive".into(),
             role: ChannelRole::Admin,
         }],
     );
@@ -759,7 +759,7 @@ async fn test_channel_rename(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust-archive".to_string(),
+            name: "rust-archive".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -888,7 +888,7 @@ async fn test_lost_channel_creation(
         &[ExpectedChannel {
             depth: 0,
             id: channel_id,
-            name: "x".to_string(),
+            name: "x".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -912,13 +912,13 @@ async fn test_lost_channel_creation(
             ExpectedChannel {
                 depth: 0,
                 id: channel_id,
-                name: "x".to_string(),
+                name: "x".into(),
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 depth: 1,
                 id: subchannel_id,
-                name: "subchannel".to_string(),
+                name: "subchannel".into(),
                 role: ChannelRole::Admin,
             },
         ],
@@ -943,13 +943,13 @@ async fn test_lost_channel_creation(
             ExpectedChannel {
                 depth: 0,
                 id: channel_id,
-                name: "x".to_string(),
+                name: "x".into(),
                 role: ChannelRole::Member,
             },
             ExpectedChannel {
                 depth: 1,
                 id: subchannel_id,
-                name: "subchannel".to_string(),
+                name: "subchannel".into(),
                 role: ChannelRole::Member,
             },
         ],
@@ -1221,13 +1221,13 @@ async fn test_channel_membership_notifications(
             ExpectedChannel {
                 depth: 0,
                 id: zed_channel,
-                name: "zed".to_string(),
+                name: "zed".into(),
                 role: ChannelRole::Guest,
             },
             ExpectedChannel {
                 depth: 1,
                 id: vim_channel,
-                name: "vim".to_string(),
+                name: "vim".into(),
                 role: ChannelRole::Member,
             },
         ],
@@ -1250,13 +1250,13 @@ async fn test_channel_membership_notifications(
             ExpectedChannel {
                 depth: 0,
                 id: zed_channel,
-                name: "zed".to_string(),
+                name: "zed".into(),
                 role: ChannelRole::Guest,
             },
             ExpectedChannel {
                 depth: 1,
                 id: vim_channel,
-                name: "vim".to_string(),
+                name: "vim".into(),
                 role: ChannelRole::Guest,
             },
         ],
@@ -1476,7 +1476,7 @@ async fn test_channel_moving(
 struct ExpectedChannel {
     depth: usize,
     id: ChannelId,
-    name: String,
+    name: SharedString,
     role: ChannelRole,
 }
 
@@ -1515,7 +1515,7 @@ fn assert_channels(
                 .ordered_channels()
                 .map(|(depth, channel)| ExpectedChannel {
                     depth,
-                    name: channel.name.clone(),
+                    name: channel.name.clone().into(),
                     id: channel.id,
                     role: channel.role,
                 })

crates/collab2/src/tests/random_channel_buffer_tests.rs ๐Ÿ”—

@@ -3,7 +3,7 @@ use crate::db::ChannelRole;
 use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
 use anyhow::Result;
 use async_trait::async_trait;
-use gpui::{BackgroundExecutor, TestAppContext};
+use gpui::{BackgroundExecutor, SharedString, TestAppContext};
 use rand::prelude::*;
 use serde_derive::{Deserialize, Serialize};
 use std::{
@@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
 #[derive(Clone, Serialize, Deserialize)]
 enum ChannelBufferOperation {
     JoinChannelNotes {
-        channel_name: String,
+        channel_name: SharedString,
     },
     LeaveChannelNotes {
-        channel_name: String,
+        channel_name: SharedString,
     },
     EditChannelNotes {
-        channel_name: String,
+        channel_name: SharedString,
         edits: Vec<(Range<usize>, Arc<str>)>,
     },
     Noop,

crates/collab_ui2/src/chat_panel.rs ๐Ÿ”—

@@ -1,983 +1,694 @@
-// use crate::{
-//     channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
-// };
-// use anyhow::Result;
-// use call::ActiveCall;
-// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
-// use client::Client;
-// use collections::HashMap;
-// use db::kvp::KEY_VALUE_STORE;
-// use editor::Editor;
-// use gpui::{
-//     actions,
-//     elements::*,
-//     platform::{CursorStyle, MouseButton},
-//     serde_json,
-//     views::{ItemType, Select, SelectStyle},
-//     AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
-//     ViewContext, ViewHandle, WeakViewHandle,
-// };
-// use language::LanguageRegistry;
-// use menu::Confirm;
-// use message_editor::MessageEditor;
-// use project::Fs;
-// use rich_text::RichText;
-// use serde::{Deserialize, Serialize};
-// use settings::SettingsStore;
-// use std::sync::Arc;
-// use theme::{IconButton, Theme};
-// use time::{OffsetDateTime, UtcOffset};
-// use util::{ResultExt, TryFutureExt};
-// use workspace::{
-//     dock::{DockPosition, Panel},
-//     Workspace,
-// };
-
-// mod message_editor;
-
-// const MESSAGE_LOADING_THRESHOLD: usize = 50;
-// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
-
-// pub struct ChatPanel {
-//     client: Arc<Client>,
-//     channel_store: ModelHandle<ChannelStore>,
-//     languages: Arc<LanguageRegistry>,
-//     active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
-//     message_list: ListState<ChatPanel>,
-//     input_editor: ViewHandle<MessageEditor>,
-//     channel_select: ViewHandle<Select>,
-//     local_timezone: UtcOffset,
-//     fs: Arc<dyn Fs>,
-//     width: Option<f32>,
-//     active: bool,
-//     pending_serialization: Task<Option<()>>,
-//     subscriptions: Vec<gpui::Subscription>,
-//     workspace: WeakViewHandle<Workspace>,
-//     is_scrolled_to_bottom: bool,
-//     has_focus: bool,
-//     markdown_data: HashMap<ChannelMessageId, RichText>,
-// }
-
-// #[derive(Serialize, Deserialize)]
-// struct SerializedChatPanel {
-//     width: Option<f32>,
-// }
-
-// #[derive(Debug)]
-// pub enum Event {
-//     DockPositionChanged,
-//     Focus,
-//     Dismissed,
-// }
-
-// actions!(
-//     chat_panel,
-//     [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
-// );
-
-// pub fn init(cx: &mut AppContext) {
-//     cx.add_action(ChatPanel::send);
-//     cx.add_action(ChatPanel::load_more_messages);
-//     cx.add_action(ChatPanel::open_notes);
-//     cx.add_action(ChatPanel::join_call);
-// }
-
-// impl ChatPanel {
-//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
-//         let fs = workspace.app_state().fs.clone();
-//         let client = workspace.app_state().client.clone();
-//         let channel_store = ChannelStore::global(cx);
-//         let languages = workspace.app_state().languages.clone();
-
-//         let input_editor = cx.add_view(|cx| {
-//             MessageEditor::new(
-//                 languages.clone(),
-//                 channel_store.clone(),
-//                 cx.add_view(|cx| {
-//                     Editor::auto_height(
-//                         4,
-//                         Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
-//                         cx,
-//                     )
-//                 }),
-//                 cx,
-//             )
-//         });
-
-//         let workspace_handle = workspace.weak_handle();
-
-//         let channel_select = cx.add_view(|cx| {
-//             let channel_store = channel_store.clone();
-//             let workspace = workspace_handle.clone();
-//             Select::new(0, cx, {
-//                 move |ix, item_type, is_hovered, cx| {
-//                     Self::render_channel_name(
-//                         &channel_store,
-//                         ix,
-//                         item_type,
-//                         is_hovered,
-//                         workspace,
-//                         cx,
-//                     )
-//                 }
-//             })
-//             .with_style(move |cx| {
-//                 let style = &theme::current(cx).chat_panel.channel_select;
-//                 SelectStyle {
-//                     header: Default::default(),
-//                     menu: style.menu,
-//                 }
-//             })
-//         });
-
-//         let mut message_list =
-//             ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
-//                 this.render_message(ix, cx)
-//             });
-//         message_list.set_scroll_handler(|visible_range, count, this, cx| {
-//             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
-//                 this.load_more_messages(&LoadMoreMessages, cx);
-//             }
-//             this.is_scrolled_to_bottom = visible_range.end == count;
-//         });
-
-//         cx.add_view(|cx| {
-//             let mut this = Self {
-//                 fs,
-//                 client,
-//                 channel_store,
-//                 languages,
-//                 active_chat: Default::default(),
-//                 pending_serialization: Task::ready(None),
-//                 message_list,
-//                 input_editor,
-//                 channel_select,
-//                 local_timezone: cx.platform().local_timezone(),
-//                 has_focus: false,
-//                 subscriptions: Vec::new(),
-//                 workspace: workspace_handle,
-//                 is_scrolled_to_bottom: true,
-//                 active: false,
-//                 width: None,
-//                 markdown_data: Default::default(),
-//             };
-
-//             let mut old_dock_position = this.position(cx);
-//             this.subscriptions
-//                 .push(
-//                     cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-//                         let new_dock_position = this.position(cx);
-//                         if new_dock_position != old_dock_position {
-//                             old_dock_position = new_dock_position;
-//                             cx.emit(Event::DockPositionChanged);
-//                         }
-//                         cx.notify();
-//                     }),
-//                 );
-
-//             this.update_channel_count(cx);
-//             cx.observe(&this.channel_store, |this, _, cx| {
-//                 this.update_channel_count(cx)
-//             })
-//             .detach();
-
-//             cx.observe(&this.channel_select, |this, channel_select, cx| {
-//                 let selected_ix = channel_select.read(cx).selected_index();
-
-//                 let selected_channel_id = this
-//                     .channel_store
-//                     .read(cx)
-//                     .channel_at(selected_ix)
-//                     .map(|e| e.id);
-//                 if let Some(selected_channel_id) = selected_channel_id {
-//                     this.select_channel(selected_channel_id, None, cx)
-//                         .detach_and_log_err(cx);
-//                 }
-//             })
-//             .detach();
-
-//             this
-//         })
-//     }
-
-//     pub fn is_scrolled_to_bottom(&self) -> bool {
-//         self.is_scrolled_to_bottom
-//     }
-
-//     pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
-//         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
-//     }
-
-//     pub fn load(
-//         workspace: WeakViewHandle<Workspace>,
-//         cx: AsyncAppContext,
-//     ) -> Task<Result<ViewHandle<Self>>> {
-//         cx.spawn(|mut cx| async move {
-//             let serialized_panel = if let Some(panel) = cx
-//                 .background()
-//                 .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
-//                 .await
-//                 .log_err()
-//                 .flatten()
-//             {
-//                 Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
-//             } else {
-//                 None
-//             };
-
-//             workspace.update(&mut cx, |workspace, cx| {
-//                 let panel = Self::new(workspace, cx);
-//                 if let Some(serialized_panel) = serialized_panel {
-//                     panel.update(cx, |panel, cx| {
-//                         panel.width = serialized_panel.width;
-//                         cx.notify();
-//                     });
-//                 }
-//                 panel
-//             })
-//         })
-//     }
-
-//     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-//         let width = self.width;
-//         self.pending_serialization = cx.background().spawn(
-//             async move {
-//                 KEY_VALUE_STORE
-//                     .write_kvp(
-//                         CHAT_PANEL_KEY.into(),
-//                         serde_json::to_string(&SerializedChatPanel { width })?,
-//                     )
-//                     .await?;
-//                 anyhow::Ok(())
-//             }
-//             .log_err(),
-//         );
-//     }
-
-//     fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
-//         let channel_count = self.channel_store.read(cx).channel_count();
-//         self.channel_select.update(cx, |select, cx| {
-//             select.set_item_count(channel_count, cx);
-//         });
-//     }
-
-//     fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
-//         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
-//             let channel_id = chat.read(cx).channel_id;
-//             {
-//                 self.markdown_data.clear();
-//                 let chat = chat.read(cx);
-//                 self.message_list.reset(chat.message_count());
-
-//                 let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
-//                 self.input_editor.update(cx, |editor, cx| {
-//                     editor.set_channel(channel_id, channel_name, cx);
-//                 });
-//             };
-//             let subscription = cx.subscribe(&chat, Self::channel_did_change);
-//             self.active_chat = Some((chat, subscription));
-//             self.acknowledge_last_message(cx);
-//             self.channel_select.update(cx, |select, cx| {
-//                 if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
-//                     select.set_selected_index(ix, cx);
-//                 }
-//             });
-//             cx.notify();
-//         }
-//     }
-
-//     fn channel_did_change(
-//         &mut self,
-//         _: ModelHandle<ChannelChat>,
-//         event: &ChannelChatEvent,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         match event {
-//             ChannelChatEvent::MessagesUpdated {
-//                 old_range,
-//                 new_count,
-//             } => {
-//                 self.message_list.splice(old_range.clone(), *new_count);
-//                 if self.active {
-//                     self.acknowledge_last_message(cx);
-//                 }
-//             }
-//             ChannelChatEvent::NewMessage {
-//                 channel_id,
-//                 message_id,
-//             } => {
-//                 if !self.active {
-//                     self.channel_store.update(cx, |store, cx| {
-//                         store.new_message(*channel_id, *message_id, cx)
-//                     })
-//                 }
-//             }
-//         }
-//         cx.notify();
-//     }
-
-//     fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
-//         if self.active && self.is_scrolled_to_bottom {
-//             if let Some((chat, _)) = &self.active_chat {
-//                 chat.update(cx, |chat, cx| {
-//                     chat.acknowledge_last_message(cx);
-//                 });
-//             }
-//         }
-//     }
-
-//     fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = theme::current(cx);
-//         Flex::column()
-//             .with_child(
-//                 ChildView::new(&self.channel_select, cx)
-//                     .contained()
-//                     .with_style(theme.chat_panel.channel_select.container),
-//             )
-//             .with_child(self.render_active_channel_messages(&theme))
-//             .with_child(self.render_input_box(&theme, cx))
-//             .into_any()
-//     }
-
-//     fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
-//         let messages = if self.active_chat.is_some() {
-//             List::new(self.message_list.clone())
-//                 .contained()
-//                 .with_style(theme.chat_panel.list)
-//                 .into_any()
-//         } else {
-//             Empty::new().into_any()
-//         };
-
-//         messages.flex(1., true).into_any()
-//     }
-
-//     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let (message, is_continuation, is_last, is_admin) = self
-//             .active_chat
-//             .as_ref()
-//             .unwrap()
-//             .0
-//             .update(cx, |active_chat, cx| {
-//                 let is_admin = self
-//                     .channel_store
-//                     .read(cx)
-//                     .is_channel_admin(active_chat.channel_id);
-
-//                 let last_message = active_chat.message(ix.saturating_sub(1));
-//                 let this_message = active_chat.message(ix).clone();
-//                 let is_continuation = last_message.id != this_message.id
-//                     && this_message.sender.id == last_message.sender.id;
-
-//                 if let ChannelMessageId::Saved(id) = this_message.id {
-//                     if this_message
-//                         .mentions
-//                         .iter()
-//                         .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
-//                     {
-//                         active_chat.acknowledge_message(id);
-//                     }
-//                 }
-
-//                 (
-//                     this_message,
-//                     is_continuation,
-//                     active_chat.message_count() == ix + 1,
-//                     is_admin,
-//                 )
-//             });
-
-//         let is_pending = message.is_pending();
-//         let theme = theme::current(cx);
-//         let text = self.markdown_data.entry(message.id).or_insert_with(|| {
-//             Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
-//         });
-
-//         let now = OffsetDateTime::now_utc();
-
-//         let style = if is_pending {
-//             &theme.chat_panel.pending_message
-//         } else if is_continuation {
-//             &theme.chat_panel.continuation_message
-//         } else {
-//             &theme.chat_panel.message
-//         };
-
-//         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-//         let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
-//             (message.id, belongs_to_user || is_admin)
-//         {
-//             Some(id)
-//         } else {
-//             None
-//         };
-
-//         enum MessageBackgroundHighlight {}
-//         MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
-//             let container = style.style_for(state);
-//             if is_continuation {
-//                 Flex::row()
-//                     .with_child(
-//                         text.element(
-//                             theme.editor.syntax.clone(),
-//                             theme.chat_panel.rich_text.clone(),
-//                             cx,
-//                         )
-//                         .flex(1., true),
-//                     )
-//                     .with_child(render_remove(message_id_to_remove, cx, &theme))
-//                     .contained()
-//                     .with_style(*container)
-//                     .with_margin_bottom(if is_last {
-//                         theme.chat_panel.last_message_bottom_spacing
-//                     } else {
-//                         0.
-//                     })
-//                     .into_any()
-//             } else {
-//                 Flex::column()
-//                     .with_child(
-//                         Flex::row()
-//                             .with_child(
-//                                 Flex::row()
-//                                     .with_child(render_avatar(
-//                                         message.sender.avatar.clone(),
-//                                         &theme.chat_panel.avatar,
-//                                         theme.chat_panel.avatar_container,
-//                                     ))
-//                                     .with_child(
-//                                         Label::new(
-//                                             message.sender.github_login.clone(),
-//                                             theme.chat_panel.message_sender.text.clone(),
-//                                         )
-//                                         .contained()
-//                                         .with_style(theme.chat_panel.message_sender.container),
-//                                     )
-//                                     .with_child(
-//                                         Label::new(
-//                                             format_timestamp(
-//                                                 message.timestamp,
-//                                                 now,
-//                                                 self.local_timezone,
-//                                             ),
-//                                             theme.chat_panel.message_timestamp.text.clone(),
-//                                         )
-//                                         .contained()
-//                                         .with_style(theme.chat_panel.message_timestamp.container),
-//                                     )
-//                                     .align_children_center()
-//                                     .flex(1., true),
-//                             )
-//                             .with_child(render_remove(message_id_to_remove, cx, &theme))
-//                             .align_children_center(),
-//                     )
-//                     .with_child(
-//                         Flex::row()
-//                             .with_child(
-//                                 text.element(
-//                                     theme.editor.syntax.clone(),
-//                                     theme.chat_panel.rich_text.clone(),
-//                                     cx,
-//                                 )
-//                                 .flex(1., true),
-//                             )
-//                             // Add a spacer to make everything line up
-//                             .with_child(render_remove(None, cx, &theme)),
-//                     )
-//                     .contained()
-//                     .with_style(*container)
-//                     .with_margin_bottom(if is_last {
-//                         theme.chat_panel.last_message_bottom_spacing
-//                     } else {
-//                         0.
-//                     })
-//                     .into_any()
-//             }
-//         })
-//         .into_any()
-//     }
-
-//     fn render_markdown_with_mentions(
-//         language_registry: &Arc<LanguageRegistry>,
-//         current_user_id: u64,
-//         message: &channel::ChannelMessage,
-//     ) -> RichText {
-//         let mentions = message
-//             .mentions
-//             .iter()
-//             .map(|(range, user_id)| rich_text::Mention {
-//                 range: range.clone(),
-//                 is_self_mention: *user_id == current_user_id,
-//             })
-//             .collect::<Vec<_>>();
-
-//         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
-//     }
-
-//     fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
-//         ChildView::new(&self.input_editor, cx)
-//             .contained()
-//             .with_style(theme.chat_panel.input_editor.container)
-//             .into_any()
-//     }
-
-//     fn render_channel_name(
-//         channel_store: &ModelHandle<ChannelStore>,
-//         ix: usize,
-//         item_type: ItemType,
-//         is_hovered: bool,
-//         workspace: WeakViewHandle<Workspace>,
-//         cx: &mut ViewContext<Select>,
-//     ) -> AnyElement<Select> {
-//         let theme = theme::current(cx);
-//         let tooltip_style = &theme.tooltip;
-//         let theme = &theme.chat_panel;
-//         let style = match (&item_type, is_hovered) {
-//             (ItemType::Header, _) => &theme.channel_select.header,
-//             (ItemType::Selected, _) => &theme.channel_select.active_item,
-//             (ItemType::Unselected, false) => &theme.channel_select.item,
-//             (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
-//         };
-
-//         let channel = &channel_store.read(cx).channel_at(ix).unwrap();
-//         let channel_id = channel.id;
-
-//         let mut row = Flex::row()
-//             .with_child(
-//                 Label::new("#".to_string(), style.hash.text.clone())
-//                     .contained()
-//                     .with_style(style.hash.container),
-//             )
-//             .with_child(Label::new(channel.name.clone(), style.name.clone()));
-
-//         if matches!(item_type, ItemType::Header) {
-//             row.add_children([
-//                 MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
-//                     render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
-//                 })
-//                 .on_click(MouseButton::Left, move |_, _, cx| {
-//                     if let Some(workspace) = workspace.upgrade(cx) {
-//                         ChannelView::open(channel_id, workspace, cx).detach();
-//                     }
-//                 })
-//                 .with_tooltip::<OpenChannelNotes>(
-//                     channel_id as usize,
-//                     "Open Notes",
-//                     Some(Box::new(OpenChannelNotes)),
-//                     tooltip_style.clone(),
-//                     cx,
-//                 )
-//                 .flex_float(),
-//                 MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
-//                     render_icon_button(
-//                         theme.icon_button.style_for(mouse_state),
-//                         "icons/speaker-loud.svg",
-//                     )
-//                 })
-//                 .on_click(MouseButton::Left, move |_, _, cx| {
-//                     ActiveCall::global(cx)
-//                         .update(cx, |call, cx| call.join_channel(channel_id, cx))
-//                         .detach_and_log_err(cx);
-//                 })
-//                 .with_tooltip::<ActiveCall>(
-//                     channel_id as usize,
-//                     "Join Call",
-//                     Some(Box::new(JoinCall)),
-//                     tooltip_style.clone(),
-//                     cx,
-//                 )
-//                 .flex_float(),
-//             ]);
-//         }
-
-//         row.align_children_center()
-//             .contained()
-//             .with_style(style.container)
-//             .into_any()
-//     }
-
-//     fn render_sign_in_prompt(
-//         &self,
-//         theme: &Arc<Theme>,
-//         cx: &mut ViewContext<Self>,
-//     ) -> AnyElement<Self> {
-//         enum SignInPromptLabel {}
-
-//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-//             Label::new(
-//                 "Sign in to use chat".to_string(),
-//                 theme
-//                     .chat_panel
-//                     .sign_in_prompt
-//                     .style_for(mouse_state)
-//                     .clone(),
-//             )
-//         })
-//         .with_cursor_style(CursorStyle::PointingHand)
-//         .on_click(MouseButton::Left, move |_, this, cx| {
-//             let client = this.client.clone();
-//             cx.spawn(|this, mut cx| async move {
-//                 if client
-//                     .authenticate_and_connect(true, &cx)
-//                     .log_err()
-//                     .await
-//                     .is_some()
-//                 {
-//                     this.update(&mut cx, |this, cx| {
-//                         if cx.handle().is_focused(cx) {
-//                             cx.focus(&this.input_editor);
-//                         }
-//                     })
-//                     .ok();
-//                 }
-//             })
-//             .detach();
-//         })
-//         .aligned()
-//         .into_any()
-//     }
-
-//     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = self.active_chat.as_ref() {
-//             let message = self
-//                 .input_editor
-//                 .update(cx, |editor, cx| editor.take_message(cx));
-
-//             if let Some(task) = chat
-//                 .update(cx, |chat, cx| chat.send_message(message, cx))
-//                 .log_err()
-//             {
-//                 task.detach();
-//             }
-//         }
-//     }
-
-//     fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = self.active_chat.as_ref() {
-//             chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
-//         }
-//     }
-
-//     fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = self.active_chat.as_ref() {
-//             chat.update(cx, |channel, cx| {
-//                 if let Some(task) = channel.load_more_messages(cx) {
-//                     task.detach();
-//                 }
-//             })
-//         }
-//     }
-
-//     pub fn select_channel(
-//         &mut self,
-//         selected_channel_id: u64,
-//         scroll_to_message_id: Option<u64>,
-//         cx: &mut ViewContext<ChatPanel>,
-//     ) -> Task<Result<()>> {
-//         let open_chat = self
-//             .active_chat
-//             .as_ref()
-//             .and_then(|(chat, _)| {
-//                 (chat.read(cx).channel_id == selected_channel_id)
-//                     .then(|| Task::ready(anyhow::Ok(chat.clone())))
-//             })
-//             .unwrap_or_else(|| {
-//                 self.channel_store.update(cx, |store, cx| {
-//                     store.open_channel_chat(selected_channel_id, cx)
-//                 })
-//             });
-
-//         cx.spawn(|this, mut cx| async move {
-//             let chat = open_chat.await?;
-//             this.update(&mut cx, |this, cx| {
-//                 this.set_active_chat(chat.clone(), cx);
-//             })?;
-
-//             if let Some(message_id) = scroll_to_message_id {
-//                 if let Some(item_ix) =
-//                     ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
-//                         .await
-//                 {
-//                     this.update(&mut cx, |this, cx| {
-//                         if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-//                             this.message_list.scroll_to(ListOffset {
-//                                 item_ix,
-//                                 offset_in_item: 0.,
-//                             });
-//                             cx.notify();
-//                         }
-//                     })?;
-//                 }
-//             }
-
-//             Ok(())
-//         })
-//     }
-
-//     fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = &self.active_chat {
-//             let channel_id = chat.read(cx).channel_id;
-//             if let Some(workspace) = self.workspace.upgrade(cx) {
-//                 ChannelView::open(channel_id, workspace, cx).detach();
-//             }
-//         }
-//     }
-
-//     fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = &self.active_chat {
-//             let channel_id = chat.read(cx).channel_id;
-//             ActiveCall::global(cx)
-//                 .update(cx, |call, cx| call.join_channel(channel_id, cx))
-//                 .detach_and_log_err(cx);
-//         }
-//     }
-// }
-
-// fn render_remove(
-//     message_id_to_remove: Option<u64>,
-//     cx: &mut ViewContext<'_, '_, ChatPanel>,
-//     theme: &Arc<Theme>,
-// ) -> AnyElement<ChatPanel> {
-//     enum DeleteMessage {}
-
-//     message_id_to_remove
-//         .map(|id| {
-//             MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
-//                 let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
-//                 render_icon_button(button_style, "icons/x.svg")
-//                     .aligned()
-//                     .into_any()
-//             })
-//             .with_padding(Padding::uniform(2.))
-//             .with_cursor_style(CursorStyle::PointingHand)
-//             .on_click(MouseButton::Left, move |_, this, cx| {
-//                 this.remove_message(id, cx);
-//             })
-//             .flex_float()
-//             .into_any()
-//         })
-//         .unwrap_or_else(|| {
-//             let style = theme.chat_panel.icon_button.default;
-
-//             Empty::new()
-//                 .constrained()
-//                 .with_width(style.icon_width)
-//                 .aligned()
-//                 .constrained()
-//                 .with_width(style.button_width)
-//                 .with_height(style.button_width)
-//                 .contained()
-//                 .with_uniform_padding(2.)
-//                 .flex_float()
-//                 .into_any()
-//         })
-// }
-
-// impl Entity for ChatPanel {
-//     type Event = Event;
-// }
-
-// impl View for ChatPanel {
-//     fn ui_name() -> &'static str {
-//         "ChatPanel"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = theme::current(cx);
-//         let element = if self.client.user_id().is_some() {
-//             self.render_channel(cx)
-//         } else {
-//             self.render_sign_in_prompt(&theme, cx)
-//         };
-//         element
-//             .contained()
-//             .with_style(theme.chat_panel.container)
-//             .constrained()
-//             .with_min_width(150.)
-//             .into_any()
-//     }
-
-//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-//         self.has_focus = true;
-//         if matches!(
-//             *self.client.status().borrow(),
-//             client::Status::Connected { .. }
-//         ) {
-//             let editor = self.input_editor.read(cx).editor.clone();
-//             cx.focus(&editor);
-//         }
-//     }
-
-//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-//         self.has_focus = false;
-//     }
-// }
-
-// impl Panel for ChatPanel {
-//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-//         settings::get::<ChatPanelSettings>(cx).dock
-//     }
-
-//     fn position_is_valid(&self, position: DockPosition) -> bool {
-//         matches!(position, DockPosition::Left | DockPosition::Right)
-//     }
-
-//     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-//         settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
-//             settings.dock = Some(position)
-//         });
-//     }
-
-//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
-//         self.width
-//             .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
-//     }
-
-//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-//         self.width = size;
-//         self.serialize(cx);
-//         cx.notify();
-//     }
-
-//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-//         self.active = active;
-//         if active {
-//             self.acknowledge_last_message(cx);
-//             if !is_channels_feature_enabled(cx) {
-//                 cx.emit(Event::Dismissed);
-//             }
-//         }
-//     }
-
-//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-//         (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
-//             .then(|| "icons/conversations.svg")
-//     }
-
-//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-//         ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
-//     }
-
-//     fn should_change_position_on_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::DockPositionChanged)
-//     }
-
-//     fn should_close_on_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::Dismissed)
-//     }
-
-//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-//         self.has_focus
-//     }
-
-//     fn is_focus_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::Focus)
-//     }
-// }
-
-// fn format_timestamp(
-//     mut timestamp: OffsetDateTime,
-//     mut now: OffsetDateTime,
-//     local_timezone: UtcOffset,
-// ) -> String {
-//     timestamp = timestamp.to_offset(local_timezone);
-//     now = now.to_offset(local_timezone);
-
-//     let today = now.date();
-//     let date = timestamp.date();
-//     let mut hour = timestamp.hour();
-//     let mut part = "am";
-//     if hour > 12 {
-//         hour -= 12;
-//         part = "pm";
-//     }
-//     if date == today {
-//         format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
-//     } else if date.next_day() == Some(today) {
-//         format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
-//     } else {
-//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-//     }
-// }
-
-// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
-//     Svg::new(svg_path)
-//         .with_color(style.color)
-//         .constrained()
-//         .with_width(style.icon_width)
-//         .aligned()
-//         .constrained()
-//         .with_width(style.button_width)
-//         .with_height(style.button_width)
-//         .contained()
-//         .with_style(style.container)
-// }
-
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use gpui::fonts::HighlightStyle;
-//     use pretty_assertions::assert_eq;
-//     use rich_text::{BackgroundKind, Highlight, RenderedRegion};
-//     use util::test::marked_text_ranges;
-
-//     #[gpui::test]
-//     fn test_render_markdown_with_mentions() {
-//         let language_registry = Arc::new(LanguageRegistry::test());
-//         let (body, ranges) = marked_text_ranges("*hi*, ยซ@abcยป, let's **call** ยซ@fghยป", false);
-//         let message = channel::ChannelMessage {
-//             id: ChannelMessageId::Saved(0),
-//             body,
-//             timestamp: OffsetDateTime::now_utc(),
-//             sender: Arc::new(client::User {
-//                 github_login: "fgh".into(),
-//                 avatar: None,
-//                 id: 103,
-//             }),
-//             nonce: 5,
-//             mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-//         };
-
-//         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("ยซhiยป, ยซ@abcยป, letโ€™s ยซcallยป ยซ@fghยป", false);
-//         assert_eq!(message.text, body);
-//         assert_eq!(
-//             message.highlights,
-//             vec![
-//                 (
-//                     ranges[0].clone(),
-//                     HighlightStyle {
-//                         italic: Some(true),
-//                         ..Default::default()
-//                     }
-//                     .into()
-//                 ),
-//                 (ranges[1].clone(), Highlight::Mention),
-//                 (
-//                     ranges[2].clone(),
-//                     HighlightStyle {
-//                         weight: Some(gpui::fonts::Weight::BOLD),
-//                         ..Default::default()
-//                     }
-//                     .into()
-//                 ),
-//                 (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
-//                 },
-//             ]
-//         );
-//     }
-// }
+use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
+use anyhow::Result;
+use call::ActiveCall;
+use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+use client::Client;
+use collections::HashMap;
+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,
+};
+use language::LanguageRegistry;
+use menu::Confirm;
+use message_editor::MessageEditor;
+use project::Fs;
+use rich_text::RichText;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use theme::ActiveTheme as _;
+use time::{OffsetDateTime, UtcOffset};
+use ui::{
+    h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
+    IconButton, Label, Tooltip,
+};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel, PanelEvent},
+    Workspace,
+};
+
+mod message_editor;
+
+const MESSAGE_LOADING_THRESHOLD: usize = 50;
+const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+
+pub struct ChatPanel {
+    client: Arc<Client>,
+    channel_store: Model<ChannelStore>,
+    languages: Arc<LanguageRegistry>,
+    message_list: ListState,
+    active_chat: Option<(Model<ChannelChat>, Subscription)>,
+    input_editor: View<MessageEditor>,
+    local_timezone: UtcOffset,
+    fs: Arc<dyn Fs>,
+    width: Option<f32>,
+    active: bool,
+    pending_serialization: Task<Option<()>>,
+    subscriptions: Vec<gpui::Subscription>,
+    workspace: WeakView<Workspace>,
+    is_scrolled_to_bottom: bool,
+    markdown_data: HashMap<ChannelMessageId, RichText>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedChatPanel {
+    width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
+
+actions!(ToggleFocus);
+
+impl ChatPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        let fs = workspace.app_state().fs.clone();
+        let client = workspace.app_state().client.clone();
+        let channel_store = ChannelStore::global(cx);
+        let languages = workspace.app_state().languages.clone();
+
+        let input_editor = cx.build_view(|cx| {
+            MessageEditor::new(
+                languages.clone(),
+                channel_store.clone(),
+                cx.build_view(|cx| Editor::auto_height(4, cx)),
+                cx,
+            )
+        });
+
+        let workspace_handle = workspace.weak_handle();
+
+        cx.build_view(|cx| {
+            let view: View<ChatPanel> = cx.view().clone();
+            let message_list =
+                ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
+                    view.update(cx, |view, cx| view.render_message(ix, cx))
+                });
+
+            message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
+                if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
+                    this.load_more_messages(cx);
+                }
+                this.is_scrolled_to_bottom = event.visible_range.end == event.count;
+            }));
+
+            let mut this = Self {
+                fs,
+                client,
+                channel_store,
+                languages,
+                message_list,
+                active_chat: Default::default(),
+                pending_serialization: Task::ready(None),
+                input_editor,
+                local_timezone: cx.local_timezone(),
+                subscriptions: Vec::new(),
+                workspace: workspace_handle,
+                is_scrolled_to_bottom: true,
+                active: false,
+                width: None,
+                markdown_data: Default::default(),
+            };
+
+            let mut old_dock_position = this.position(cx);
+            this.subscriptions.push(cx.observe_global::<SettingsStore>(
+                move |this: &mut Self, cx| {
+                    let new_dock_position = this.position(cx);
+                    if new_dock_position != old_dock_position {
+                        old_dock_position = new_dock_position;
+                        cx.emit(Event::DockPositionChanged);
+                    }
+                    cx.notify();
+                },
+            ));
+
+            this
+        })
+    }
+
+    pub fn is_scrolled_to_bottom(&self) -> bool {
+        self.is_scrolled_to_bottom
+    }
+
+    pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
+        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+    }
+
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background_executor()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = Self::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background_executor().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        CHAT_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedChatPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
+        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
+            let channel_id = chat.read(cx).channel_id;
+            {
+                self.markdown_data.clear();
+                let chat = chat.read(cx);
+                self.message_list.reset(chat.message_count());
+
+                let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
+                self.input_editor.update(cx, |editor, cx| {
+                    editor.set_channel(channel_id, channel_name, cx);
+                });
+            };
+            let subscription = cx.subscribe(&chat, Self::channel_did_change);
+            self.active_chat = Some((chat, subscription));
+            self.acknowledge_last_message(cx);
+            cx.notify();
+        }
+    }
+
+    fn channel_did_change(
+        &mut self,
+        _: Model<ChannelChat>,
+        event: &ChannelChatEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            ChannelChatEvent::MessagesUpdated {
+                old_range,
+                new_count,
+            } => {
+                self.message_list.splice(old_range.clone(), *new_count);
+                if self.active {
+                    self.acknowledge_last_message(cx);
+                }
+            }
+            ChannelChatEvent::NewMessage {
+                channel_id,
+                message_id,
+            } => {
+                if !self.active {
+                    self.channel_store.update(cx, |store, cx| {
+                        store.new_message(*channel_id, *message_id, cx)
+                    })
+                }
+            }
+        }
+        cx.notify();
+    }
+
+    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
+        if self.active && self.is_scrolled_to_bottom {
+            if let Some((chat, _)) = &self.active_chat {
+                chat.update(cx, |chat, cx| {
+                    chat.acknowledge_last_message(cx);
+                });
+            }
+        }
+    }
+
+    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        v_stack()
+            .full()
+            .on_action(cx.listener(Self::send))
+            .child(
+                h_stack()
+                    .w_full()
+                    .h_7()
+                    .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()))
+                            .unwrap_or_default(),
+                    ))
+                    .child(
+                        h_stack()
+                            .child(
+                                IconButton::new("notes", Icon::File)
+                                    .on_click(cx.listener(Self::open_notes))
+                                    .tooltip(|cx| Tooltip::text("Open notes", cx)),
+                            )
+                            .child(
+                                IconButton::new("call", Icon::AudioOn)
+                                    .on_click(cx.listener(Self::join_call))
+                                    .tooltip(|cx| Tooltip::text("Join call", cx)),
+                            ),
+                    ),
+            )
+            .child(div().grow().child(self.render_active_channel_messages(cx)))
+            .child(
+                div()
+                    .z_index(1)
+                    .p_2()
+                    .bg(cx.theme().colors().background)
+                    .child(self.input_editor.clone()),
+            )
+            .into_any()
+    }
+
+    fn render_active_channel_messages(&self, _cx: &mut ViewContext<Self>) -> AnyElement {
+        if self.active_chat.is_some() {
+            list(self.message_list.clone()).full().into_any_element()
+        } else {
+            div().into_any_element()
+        }
+    }
+
+    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
+        let active_chat = &self.active_chat.as_ref().unwrap().0;
+        let (message, is_continuation, is_admin) = active_chat.update(cx, |active_chat, cx| {
+            let is_admin = self
+                .channel_store
+                .read(cx)
+                .is_channel_admin(active_chat.channel_id);
+
+            let last_message = active_chat.message(ix.saturating_sub(1));
+            let this_message = active_chat.message(ix).clone();
+            let is_continuation = last_message.id != this_message.id
+                && this_message.sender.id == last_message.sender.id;
+
+            if let ChannelMessageId::Saved(id) = this_message.id {
+                if this_message
+                    .mentions
+                    .iter()
+                    .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
+                {
+                    active_chat.acknowledge_message(id);
+                }
+            }
+
+            (this_message, is_continuation, is_admin)
+        });
+
+        let _is_pending = message.is_pending();
+        let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+            Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
+        });
+
+        let now = OffsetDateTime::now_utc();
+
+        let belongs_to_user = Some(message.sender.id) == self.client.user_id();
+        let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
+            (message.id, belongs_to_user || is_admin)
+        {
+            Some(id)
+        } else {
+            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()
+        }
+    }
+
+    fn render_markdown_with_mentions(
+        language_registry: &Arc<LanguageRegistry>,
+        current_user_id: u64,
+        message: &channel::ChannelMessage,
+    ) -> RichText {
+        let mentions = message
+            .mentions
+            .iter()
+            .map(|(range, user_id)| rich_text::Mention {
+                range: range.clone(),
+                is_self_mention: *user_id == current_user_id,
+            })
+            .collect::<Vec<_>>();
+
+        rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
+    }
+
+    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        Button::new("sign-in", "Sign in to use chat")
+            .on_click(cx.listener(move |this, _, cx| {
+                let client = this.client.clone();
+                cx.spawn(|this, mut cx| async move {
+                    if client
+                        .authenticate_and_connect(true, &cx)
+                        .log_err()
+                        .await
+                        .is_some()
+                    {
+                        this.update(&mut cx, |_, cx| {
+                            cx.focus_self();
+                        })
+                        .ok();
+                    }
+                })
+                .detach();
+            }))
+            .into_any_element()
+    }
+
+    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            let message = self
+                .input_editor
+                .update(cx, |editor, cx| editor.take_message(cx));
+
+            if let Some(task) = chat
+                .update(cx, |chat, cx| chat.send_message(message, cx))
+                .log_err()
+            {
+                task.detach();
+            }
+        }
+    }
+
+    fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
+        }
+    }
+
+    fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |channel, cx| {
+                if let Some(task) = channel.load_more_messages(cx) {
+                    task.detach();
+                }
+            })
+        }
+    }
+
+    pub fn select_channel(
+        &mut self,
+        selected_channel_id: u64,
+        scroll_to_message_id: Option<u64>,
+        cx: &mut ViewContext<ChatPanel>,
+    ) -> Task<Result<()>> {
+        let open_chat = self
+            .active_chat
+            .as_ref()
+            .and_then(|(chat, _)| {
+                (chat.read(cx).channel_id == selected_channel_id)
+                    .then(|| Task::ready(anyhow::Ok(chat.clone())))
+            })
+            .unwrap_or_else(|| {
+                self.channel_store.update(cx, |store, cx| {
+                    store.open_channel_chat(selected_channel_id, cx)
+                })
+            });
+
+        cx.spawn(|this, mut cx| async move {
+            let chat = open_chat.await?;
+            this.update(&mut cx, |this, cx| {
+                this.set_active_chat(chat.clone(), cx);
+            })?;
+
+            if let Some(message_id) = scroll_to_message_id {
+                if let Some(item_ix) =
+                    ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
+                        .await
+                {
+                    this.update(&mut cx, |this, cx| {
+                        if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
+                            this.message_list.scroll_to(ListOffset {
+                                item_ix,
+                                offset_in_item: px(0.0),
+                            });
+                            cx.notify();
+                        }
+                    })?;
+                }
+            }
+
+            Ok(())
+        })
+    }
+
+    fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = &self.active_chat {
+            let channel_id = chat.read(cx).channel_id;
+            if let Some(workspace) = self.workspace.upgrade() {
+                ChannelView::open(channel_id, workspace, cx).detach();
+            }
+        }
+    }
+
+    fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = &self.active_chat {
+            let channel_id = chat.read(cx).channel_id;
+            ActiveCall::global(cx)
+                .update(cx, |call, cx| call.join_channel(channel_id, cx))
+                .detach_and_log_err(cx);
+        }
+    }
+}
+
+fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
+    if let Some(message_id) = message_id_to_remove {
+        IconButton::new(("remove", message_id), Icon::XCircle)
+            .on_click(cx.listener(move |this, _, cx| {
+                this.remove_message(message_id, cx);
+            }))
+            .into_any_element()
+    } else {
+        div().into_any_element()
+    }
+}
+
+impl EventEmitter<Event> for ChatPanel {}
+
+impl Render for ChatPanel {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .full()
+            .child(if self.client.user_id().is_some() {
+                self.render_channel(cx)
+            } else {
+                self.render_sign_in_prompt(cx)
+            })
+            .min_w(px(150.))
+    }
+}
+
+impl FocusableView for ChatPanel {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.input_editor.read(cx).focus_handle(cx)
+    }
+}
+
+impl Panel for ChatPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        ChatPanelSettings::get_global(cx).dock
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
+            settings.dock = Some(position)
+        });
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+        self.active = active;
+        if active {
+            self.acknowledge_last_message(cx);
+            if !is_channels_feature_enabled(cx) {
+                cx.emit(Event::Dismissed);
+            }
+        }
+    }
+
+    fn persistent_name() -> &'static str {
+        "ChatPanel"
+    }
+
+    fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
+        Some(ui::Icon::MessageBubbles)
+    }
+
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        Box::new(ToggleFocus)
+    }
+}
+
+impl EventEmitter<PanelEvent> for ChatPanel {}
+
+fn format_timestamp(
+    mut timestamp: OffsetDateTime,
+    mut now: OffsetDateTime,
+    local_timezone: UtcOffset,
+) -> String {
+    timestamp = timestamp.to_offset(local_timezone);
+    now = now.to_offset(local_timezone);
+
+    let today = now.date();
+    let date = timestamp.date();
+    let mut hour = timestamp.hour();
+    let mut part = "am";
+    if hour > 12 {
+        hour -= 12;
+        part = "pm";
+    }
+    if date == today {
+        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
+    } else if date.next_day() == Some(today) {
+        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
+    } else {
+        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::HighlightStyle;
+    use pretty_assertions::assert_eq;
+    use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+    use util::test::marked_text_ranges;
+
+    #[gpui::test]
+    fn test_render_markdown_with_mentions() {
+        let language_registry = Arc::new(LanguageRegistry::test());
+        let (body, ranges) = marked_text_ranges("*hi*, ยซ@abcยป, let's **call** ยซ@fghยป", false);
+        let message = channel::ChannelMessage {
+            id: ChannelMessageId::Saved(0),
+            body,
+            timestamp: OffsetDateTime::now_utc(),
+            sender: Arc::new(client::User {
+                github_login: "fgh".into(),
+                avatar: None,
+                id: 103,
+            }),
+            nonce: 5,
+            mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+        };
+
+        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("ยซhiยป, ยซ@abcยป, letโ€™s ยซcallยป ยซ@fghยป", false);
+        assert_eq!(message.text, body);
+        assert_eq!(
+            message.highlights,
+            vec![
+                (
+                    ranges[0].clone(),
+                    HighlightStyle {
+                        font_style: Some(gpui::FontStyle::Italic),
+                        ..Default::default()
+                    }
+                    .into()
+                ),
+                (ranges[1].clone(), Highlight::Mention),
+                (
+                    ranges[2].clone(),
+                    HighlightStyle {
+                        font_weight: Some(gpui::FontWeight::BOLD),
+                        ..Default::default()
+                    }
+                    .into()
+                ),
+                (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 ๐Ÿ”—

@@ -3,13 +3,14 @@ use client::UserId;
 use collections::HashMap;
 use editor::{AnchorRangeExt, Editor};
 use gpui::{
-    elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
+    ViewContext, WeakView,
 };
 use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 use lazy_static::lazy_static;
 use project::search::SearchQuery;
 use std::{sync::Arc, time::Duration};
+use workspace::item::ItemHandle;
 
 const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 
@@ -19,8 +20,8 @@ lazy_static! {
 }
 
 pub struct MessageEditor {
-    pub editor: ViewHandle<Editor>,
-    channel_store: ModelHandle<ChannelStore>,
+    pub editor: View<Editor>,
+    channel_store: Model<ChannelStore>,
     users: HashMap<String, UserId>,
     mentions: Vec<UserId>,
     mentions_task: Option<Task<()>>,
@@ -30,8 +31,8 @@ pub struct MessageEditor {
 impl MessageEditor {
     pub fn new(
         language_registry: Arc<LanguageRegistry>,
-        channel_store: ModelHandle<ChannelStore>,
-        editor: ViewHandle<Editor>,
+        channel_store: Model<ChannelStore>,
+        editor: View<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         editor.update(cx, |editor, cx| {
@@ -48,15 +49,13 @@ impl MessageEditor {
         cx.subscribe(&buffer, Self::on_buffer_event).detach();
 
         let markdown = language_registry.language_for_name("Markdown");
-        cx.app_context()
-            .spawn(|mut cx| async move {
-                let markdown = markdown.await?;
-                buffer.update(&mut cx, |buffer, cx| {
-                    buffer.set_language(Some(markdown), cx)
-                });
-                anyhow::Ok(())
+        cx.spawn(|_, mut cx| async move {
+            let markdown = markdown.await?;
+            buffer.update(&mut cx, |buffer, cx| {
+                buffer.set_language(Some(markdown), cx)
             })
-            .detach_and_log_err(cx);
+        })
+        .detach_and_log_err(cx);
 
         Self {
             editor,
@@ -71,7 +70,7 @@ impl MessageEditor {
     pub fn set_channel(
         &mut self,
         channel_id: u64,
-        channel_name: Option<String>,
+        channel_name: Option<SharedString>,
         cx: &mut ViewContext<Self>,
     ) {
         self.editor.update(cx, |editor, cx| {
@@ -132,26 +131,28 @@ impl MessageEditor {
 
     fn on_buffer_event(
         &mut self,
-        buffer: ModelHandle<Buffer>,
+        buffer: Model<Buffer>,
         event: &language::Event,
         cx: &mut ViewContext<Self>,
     ) {
         if let language::Event::Reparsed | language::Event::Edited = event {
             let buffer = buffer.read(cx).snapshot();
             self.mentions_task = Some(cx.spawn(|this, cx| async move {
-                cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+                cx.background_executor()
+                    .timer(MENTIONS_DEBOUNCE_INTERVAL)
+                    .await;
                 Self::find_mentions(this, buffer, cx).await;
             }));
         }
     }
 
     async fn find_mentions(
-        this: WeakViewHandle<MessageEditor>,
+        this: WeakView<MessageEditor>,
         buffer: BufferSnapshot,
-        mut cx: AsyncAppContext,
+        mut cx: AsyncWindowContext,
     ) {
         let (buffer, ranges) = cx
-            .background()
+            .background_executor()
             .spawn(async move {
                 let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
                 (buffer, ranges)
@@ -180,11 +181,7 @@ impl MessageEditor {
                 }
 
                 editor.clear_highlights::<Self>(cx);
-                editor.highlight_text::<Self>(
-                    anchor_ranges,
-                    theme::current(cx).chat_panel.rich_text.mention_highlight,
-                    cx,
-                )
+                editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
             });
 
             this.mentions = mentioned_user_ids;
@@ -192,116 +189,112 @@ impl MessageEditor {
         })
         .ok();
     }
-}
-
-impl Entity for MessageEditor {
-    type Event = ();
-}
-
-impl View for MessageEditor {
-    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
-        ChildView::new(&self.editor, cx).into_any()
-    }
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.editor);
-        }
+    pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
+        self.editor.read(cx).focus_handle(cx)
     }
 }
 
-#[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);
+impl Render for MessageEditor {
+    type Element = AnyView;
 
-        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, None, cx)),
-                cx,
-            )
-        });
-        cx.foreground().run_until_parked();
-        editor
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        self.editor.to_any()
     }
 }
+
+// #[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
+//     }
+// }

crates/collab_ui2/src/collab_panel.rs ๐Ÿ”—

@@ -192,6 +192,7 @@ use workspace::{
 };
 
 use crate::channel_view::ChannelView;
+use crate::chat_panel::ChatPanel;
 use crate::{face_pile::FacePile, CollaborationPanelSettings};
 
 use self::channel_modal::ChannelModal;
@@ -852,7 +853,7 @@ impl CollabPanel {
                     .extend(channel_store.ordered_channels().enumerate().map(
                         |(ix, (_, channel))| StringMatchCandidate {
                             id: ix,
-                            string: channel.name.clone(),
+                            string: channel.name.clone().into(),
                             char_bag: channel.name.chars().collect(),
                         },
                     ));
@@ -2102,14 +2103,13 @@ impl CollabPanel {
         };
         cx.window_context().defer(move |cx| {
             workspace.update(cx, |workspace, cx| {
-                todo!();
-                // if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-                //     panel.update(cx, |panel, cx| {
-                //         panel
-                //             .select_channel(channel_id, None, cx)
-                //             .detach_and_log_err(cx);
-                //     });
-                // }
+                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+                    panel.update(cx, |panel, cx| {
+                        panel
+                            .select_channel(channel_id, None, cx)
+                            .detach_and_log_err(cx);
+                    });
+                }
             });
         });
     }
@@ -2262,7 +2262,7 @@ impl CollabPanel {
                         }
                     };
 
-                    Some(channel.name.as_str())
+                    Some(channel.name.as_ref())
                 });
 
                 if let Some(name) = channel_name {
@@ -2603,9 +2603,14 @@ impl CollabPanel {
                                                     Color::Default
                                                 } else {
                                                     Color::Muted
+                                                })
+                                                .on_click(cx.listener(move |this, _, cx| {
+                                                    this.join_channel_chat(channel_id, cx)
+                                                }))
+                                                .tooltip(|cx| {
+                                                    Tooltip::text("Open channel chat", cx)
                                                 }),
-                                            )
-                                            .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
+                                            ),
                                     )
                                     .child(
                                         div()

crates/collab_ui2/src/collab_ui.rs ๐Ÿ”—

@@ -12,6 +12,7 @@ use std::{rc::Rc, sync::Arc};
 use call::{report_call_event_for_room, ActiveCall, Room};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use gpui::{
     actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
     WindowKind, WindowOptions,
@@ -157,6 +158,6 @@ fn notification_window_options(
 //         .into_any()
 // }
 
-// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-//     cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-// }
+fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+}

crates/gpui2/src/app.rs ๐Ÿ”—

@@ -13,6 +13,7 @@ use smallvec::SmallVec;
 use smol::future::FutureExt;
 #[cfg(any(test, feature = "test-support"))]
 pub use test_context::*;
+use time::UtcOffset;
 
 use crate::{
     current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
@@ -536,6 +537,10 @@ impl AppContext {
         self.platform.restart()
     }
 
+    pub fn local_timezone(&self) -> UtcOffset {
+        self.platform.local_timezone()
+    }
+
     pub(crate) fn push_effect(&mut self, effect: Effect) {
         match &effect {
             Effect::Notify { emitter } => {

crates/gpui2/src/elements/list.rs ๐Ÿ”—

@@ -0,0 +1,496 @@
+use crate::{
+    px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
+    Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext,
+};
+use collections::VecDeque;
+use refineable::Refineable as _;
+use std::{cell::RefCell, ops::Range, rc::Rc};
+use sum_tree::{Bias, SumTree};
+
+pub fn list(state: ListState) -> List {
+    List {
+        state,
+        style: StyleRefinement::default(),
+    }
+}
+
+pub struct List {
+    state: ListState,
+    style: StyleRefinement,
+}
+
+#[derive(Clone)]
+pub struct ListState(Rc<RefCell<StateInner>>);
+
+struct StateInner {
+    last_layout_width: Option<Pixels>,
+    render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
+    items: SumTree<ListItem>,
+    logical_scroll_top: Option<ListOffset>,
+    alignment: ListAlignment,
+    overdraw: Pixels,
+    #[allow(clippy::type_complexity)]
+    scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum ListAlignment {
+    Top,
+    Bottom,
+}
+
+pub struct ListScrollEvent {
+    pub visible_range: Range<usize>,
+    pub count: usize,
+}
+
+#[derive(Clone)]
+enum ListItem {
+    Unrendered,
+    Rendered { height: Pixels },
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+struct ListItemSummary {
+    count: usize,
+    rendered_count: usize,
+    unrendered_count: usize,
+    height: Pixels,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct RenderedCount(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct UnrenderedCount(usize);
+
+#[derive(Clone, Debug, Default)]
+struct Height(Pixels);
+
+impl ListState {
+    pub fn new<F>(
+        element_count: usize,
+        orientation: ListAlignment,
+        overdraw: Pixels,
+        render_item: F,
+    ) -> Self
+    where
+        F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
+    {
+        let mut items = SumTree::new();
+        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
+        Self(Rc::new(RefCell::new(StateInner {
+            last_layout_width: None,
+            render_item: Box::new(render_item),
+            items,
+            logical_scroll_top: None,
+            alignment: orientation,
+            overdraw,
+            scroll_handler: None,
+        })))
+    }
+
+    pub fn reset(&self, element_count: usize) {
+        let state = &mut *self.0.borrow_mut();
+        state.logical_scroll_top = None;
+        state.items = SumTree::new();
+        state
+            .items
+            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
+    }
+
+    pub fn item_count(&self) -> usize {
+        self.0.borrow().items.summary().count
+    }
+
+    pub fn splice(&self, old_range: Range<usize>, count: usize) {
+        let state = &mut *self.0.borrow_mut();
+
+        if let Some(ListOffset {
+            item_ix,
+            offset_in_item,
+        }) = state.logical_scroll_top.as_mut()
+        {
+            if old_range.contains(item_ix) {
+                *item_ix = old_range.start;
+                *offset_in_item = px(0.);
+            } else if old_range.end <= *item_ix {
+                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
+            }
+        }
+
+        let mut old_heights = state.items.cursor::<Count>();
+        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
+        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
+
+        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
+        new_heights.append(old_heights.suffix(&()), &());
+        drop(old_heights);
+        state.items = new_heights;
+    }
+
+    pub fn set_scroll_handler(
+        &self,
+        handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
+    ) {
+        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
+    }
+
+    pub fn logical_scroll_top(&self) -> ListOffset {
+        self.0.borrow().logical_scroll_top()
+    }
+
+    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
+        let state = &mut *self.0.borrow_mut();
+        let item_count = state.items.summary().count;
+        if scroll_top.item_ix >= item_count {
+            scroll_top.item_ix = item_count;
+            scroll_top.offset_in_item = px(0.);
+        }
+        state.logical_scroll_top = Some(scroll_top);
+    }
+}
+
+impl StateInner {
+    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
+        let mut cursor = self.items.cursor::<ListItemSummary>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        let start_y = cursor.start().height + scroll_top.offset_in_item;
+        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
+        scroll_top.item_ix..cursor.start().count + 1
+    }
+
+    fn scroll(
+        &mut self,
+        scroll_top: &ListOffset,
+        height: Pixels,
+        delta: Point<Pixels>,
+        cx: &mut WindowContext,
+    ) {
+        let scroll_max = (self.items.summary().height - height).max(px(0.));
+        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
+            .max(px(0.))
+            .min(scroll_max);
+
+        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
+            self.logical_scroll_top = None;
+        } else {
+            let mut cursor = self.items.cursor::<ListItemSummary>();
+            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
+            let item_ix = cursor.start().count;
+            let offset_in_item = new_scroll_top - cursor.start().height;
+            self.logical_scroll_top = Some(ListOffset {
+                item_ix,
+                offset_in_item,
+            });
+        }
+
+        if self.scroll_handler.is_some() {
+            let visible_range = self.visible_range(height, scroll_top);
+            self.scroll_handler.as_mut().unwrap()(
+                &ListScrollEvent {
+                    visible_range,
+                    count: self.items.summary().count,
+                },
+                cx,
+            );
+        }
+
+        cx.notify();
+    }
+
+    fn logical_scroll_top(&self) -> ListOffset {
+        self.logical_scroll_top
+            .unwrap_or_else(|| match self.alignment {
+                ListAlignment::Top => ListOffset {
+                    item_ix: 0,
+                    offset_in_item: px(0.),
+                },
+                ListAlignment::Bottom => ListOffset {
+                    item_ix: self.items.summary().count,
+                    offset_in_item: px(0.),
+                },
+            })
+    }
+
+    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
+        let mut cursor = self.items.cursor::<ListItemSummary>();
+        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
+        cursor.start().height + logical_scroll_top.offset_in_item
+    }
+}
+
+impl std::fmt::Debug for ListItem {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Unrendered => write!(f, "Unrendered"),
+            Self::Rendered { height, .. } => {
+                f.debug_struct("Rendered").field("height", height).finish()
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct ListOffset {
+    pub item_ix: usize,
+    pub offset_in_item: Pixels,
+}
+
+impl Element for List {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _state: Option<Self::State>,
+        cx: &mut crate::WindowContext,
+    ) -> (crate::LayoutId, Self::State) {
+        let mut style = Style::default();
+        style.refine(&self.style);
+        let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
+            cx.request_layout(&style, None)
+        });
+        (layout_id, ())
+    }
+
+    fn paint(
+        self,
+        bounds: crate::Bounds<crate::Pixels>,
+        _state: &mut Self::State,
+        cx: &mut crate::WindowContext,
+    ) {
+        let state = &mut *self.state.0.borrow_mut();
+
+        // If the width of the list has changed, invalidate all cached item heights
+        if state.last_layout_width != Some(bounds.size.width) {
+            state.items = SumTree::from_iter(
+                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
+                &(),
+            )
+        }
+
+        let old_items = state.items.clone();
+        let mut measured_items = VecDeque::new();
+        let mut item_elements = VecDeque::new();
+        let mut rendered_height = px(0.);
+        let mut scroll_top = state.logical_scroll_top();
+
+        let available_item_space = Size {
+            width: AvailableSpace::Definite(bounds.size.width),
+            height: AvailableSpace::MinContent,
+        };
+
+        // Render items after the scroll top, including those in the trailing overdraw
+        let mut cursor = old_items.cursor::<Count>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        for (ix, item) in cursor.by_ref().enumerate() {
+            let visible_height = rendered_height - scroll_top.offset_in_item;
+            if visible_height >= bounds.size.height + state.overdraw {
+                break;
+            }
+
+            // Use the previously cached height if available
+            let mut height = if let ListItem::Rendered { height } = item {
+                Some(*height)
+            } else {
+                None
+            };
+
+            // If we're within the visible area or the height wasn't cached, render and measure the item's element
+            if visible_height < bounds.size.height || height.is_none() {
+                let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
+                let element_size = element.measure(available_item_space, cx);
+                height = Some(element_size.height);
+                if visible_height < bounds.size.height {
+                    item_elements.push_back(element);
+                }
+            }
+
+            let height = height.unwrap();
+            rendered_height += height;
+            measured_items.push_back(ListItem::Rendered { height });
+        }
+
+        // Prepare to start walking upward from the item at the scroll top.
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+        // If the rendered items do not fill the visible region, then adjust
+        // the scroll top upward.
+        if rendered_height - scroll_top.offset_in_item < bounds.size.height {
+            while rendered_height < bounds.size.height {
+                cursor.prev(&());
+                if cursor.item().is_some() {
+                    let mut element = (state.render_item)(cursor.start().0, cx);
+                    let element_size = element.measure(available_item_space, cx);
+
+                    rendered_height += element_size.height;
+                    measured_items.push_front(ListItem::Rendered {
+                        height: element_size.height,
+                    });
+                    item_elements.push_front(element)
+                } else {
+                    break;
+                }
+            }
+
+            scroll_top = ListOffset {
+                item_ix: cursor.start().0,
+                offset_in_item: rendered_height - bounds.size.height,
+            };
+
+            match state.alignment {
+                ListAlignment::Top => {
+                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
+                    state.logical_scroll_top = Some(scroll_top);
+                }
+                ListAlignment::Bottom => {
+                    scroll_top = ListOffset {
+                        item_ix: cursor.start().0,
+                        offset_in_item: rendered_height - bounds.size.height,
+                    };
+                    state.logical_scroll_top = None;
+                }
+            };
+        }
+
+        // Measure items in the leading overdraw
+        let mut leading_overdraw = scroll_top.offset_in_item;
+        while leading_overdraw < state.overdraw {
+            cursor.prev(&());
+            if let Some(item) = cursor.item() {
+                let height = if let ListItem::Rendered { height } = item {
+                    *height
+                } else {
+                    let mut element = (state.render_item)(cursor.start().0, cx);
+                    element.measure(available_item_space, cx).height
+                };
+
+                leading_overdraw += height;
+                measured_items.push_front(ListItem::Rendered { height });
+            } else {
+                break;
+            }
+        }
+
+        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
+        let mut cursor = old_items.cursor::<Count>();
+        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
+        new_items.extend(measured_items, &());
+        cursor.seek(&Count(measured_range.end), Bias::Right, &());
+        new_items.append(cursor.suffix(&()), &());
+
+        // Paint the visible items
+        let mut item_origin = bounds.origin;
+        item_origin.y -= scroll_top.offset_in_item;
+        for mut item_element in item_elements {
+            let item_height = item_element.measure(available_item_space, cx).height;
+            item_element.draw(item_origin, available_item_space, cx);
+            item_origin.y += item_height;
+        }
+
+        state.items = new_items;
+        state.last_layout_width = Some(bounds.size.width);
+
+        let list_state = self.state.clone();
+        let height = bounds.size.height;
+        cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble {
+                list_state.0.borrow_mut().scroll(
+                    &scroll_top,
+                    height,
+                    event.delta.pixel_delta(px(20.)),
+                    cx,
+                )
+            }
+        });
+    }
+}
+
+impl IntoElement for List {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Styled for List {
+    fn style(&mut self) -> &mut StyleRefinement {
+        &mut self.style
+    }
+}
+
+impl sum_tree::Item for ListItem {
+    type Summary = ListItemSummary;
+
+    fn summary(&self) -> Self::Summary {
+        match self {
+            ListItem::Unrendered => ListItemSummary {
+                count: 1,
+                rendered_count: 0,
+                unrendered_count: 1,
+                height: px(0.),
+            },
+            ListItem::Rendered { height } => ListItemSummary {
+                count: 1,
+                rendered_count: 1,
+                unrendered_count: 0,
+                height: *height,
+            },
+        }
+    }
+}
+
+impl sum_tree::Summary for ListItemSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.count += summary.count;
+        self.rendered_count += summary.rendered_count;
+        self.unrendered_count += summary.unrendered_count;
+        self.height += summary.height;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.rendered_count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.unrendered_count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.height;
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
+    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
+        self.0.partial_cmp(&other.count).unwrap()
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
+    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
+        self.0.partial_cmp(&other.height).unwrap()
+    }
+}

crates/gpui2/src/elements/mod.rs ๐Ÿ”—

@@ -1,6 +1,7 @@
 mod canvas;
 mod div;
 mod img;
+mod list;
 mod overlay;
 mod svg;
 mod text;
@@ -9,6 +10,7 @@ mod uniform_list;
 pub use canvas::*;
 pub use div::*;
 pub use img::*;
+pub use list::*;
 pub use overlay::*;
 pub use svg::*;
 pub use text::*;

crates/gpui2/src/elements/uniform_list.rs ๐Ÿ”—

@@ -131,7 +131,7 @@ impl Element for UniformList {
                                         }
                                     });
                             let height = match available_space.height {
-                                AvailableSpace::Definite(x) => desired_height.min(x),
+                                AvailableSpace::Definite(height) => desired_height.min(height),
                                 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
                                     desired_height
                                 }

crates/gpui2/src/gpui2.rs ๐Ÿ”—

@@ -1,6 +1,7 @@
 #[macro_use]
 mod action;
 mod app;
+
 mod assets;
 mod color;
 mod element;
@@ -15,6 +16,7 @@ mod keymap;
 mod platform;
 pub mod prelude;
 mod scene;
+mod shared_string;
 mod style;
 mod styled;
 mod subscription;
@@ -57,6 +59,7 @@ pub use scene::*;
 pub use serde;
 pub use serde_derive;
 pub use serde_json;
+pub use shared_string::*;
 pub use smallvec;
 pub use smol::Timer;
 pub use style::*;
@@ -71,10 +74,9 @@ pub use util::arc_cow::ArcCow;
 pub use view::*;
 pub use window::*;
 
-use derive_more::{Deref, DerefMut};
 use std::{
     any::{Any, TypeId},
-    borrow::{Borrow, BorrowMut},
+    borrow::BorrowMut,
 };
 use taffy::TaffyLayoutEngine;
 
@@ -209,42 +211,3 @@ impl<T> Flatten<T> for Result<T> {
         self
     }
 }
-
-#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
-pub struct SharedString(ArcCow<'static, str>);
-
-impl Default for SharedString {
-    fn default() -> Self {
-        Self(ArcCow::Owned("".into()))
-    }
-}
-
-impl AsRef<str> for SharedString {
-    fn as_ref(&self) -> &str {
-        &self.0
-    }
-}
-
-impl Borrow<str> for SharedString {
-    fn borrow(&self) -> &str {
-        self.as_ref()
-    }
-}
-
-impl std::fmt::Debug for SharedString {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl std::fmt::Display for SharedString {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0.as_ref())
-    }
-}
-
-impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
-    fn from(value: T) -> Self {
-        Self(value.into())
-    }
-}

crates/gpui2/src/key_dispatch.rs ๐Ÿ”—

@@ -149,13 +149,19 @@ impl DispatchTree {
     }
 
     pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
-        let mut actions = Vec::new();
+        let mut actions = Vec::<Box<dyn Action>>::new();
         for node_id in self.dispatch_path(target) {
             let node = &self.nodes[node_id.0];
             for DispatchActionListener { action_type, .. } in &node.action_listeners {
-                // Intentionally silence these errors without logging.
-                // If an action cannot be built by default, it's not available.
-                actions.extend(self.action_registry.build_action_type(action_type).ok());
+                if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
+                {
+                    // Intentionally silence these errors without logging.
+                    // If an action cannot be built by default, it's not available.
+                    let action = self.action_registry.build_action_type(action_type).ok();
+                    if let Some(action) = action {
+                        actions.insert(ix, action);
+                    }
+                }
             }
         }
         actions

crates/gpui2/src/shared_string.rs ๐Ÿ”—

@@ -0,0 +1,101 @@
+use derive_more::{Deref, DerefMut};
+use serde::{Deserialize, Serialize};
+use std::{borrow::Borrow, sync::Arc};
+use util::arc_cow::ArcCow;
+
+#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
+pub struct SharedString(ArcCow<'static, str>);
+
+impl Default for SharedString {
+    fn default() -> Self {
+        Self(ArcCow::Owned("".into()))
+    }
+}
+
+impl AsRef<str> for SharedString {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl Borrow<str> for SharedString {
+    fn borrow(&self) -> &str {
+        self.as_ref()
+    }
+}
+
+impl std::fmt::Debug for SharedString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl std::fmt::Display for SharedString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.as_ref())
+    }
+}
+
+impl PartialEq<String> for SharedString {
+    fn eq(&self, other: &String) -> bool {
+        self.as_ref() == other
+    }
+}
+
+impl PartialEq<SharedString> for String {
+    fn eq(&self, other: &SharedString) -> bool {
+        self == other.as_ref()
+    }
+}
+
+impl PartialEq<str> for SharedString {
+    fn eq(&self, other: &str) -> bool {
+        self.as_ref() == other
+    }
+}
+
+impl<'a> PartialEq<&'a str> for SharedString {
+    fn eq(&self, other: &&'a str) -> bool {
+        self.as_ref() == *other
+    }
+}
+
+impl Into<Arc<str>> for SharedString {
+    fn into(self) -> Arc<str> {
+        match self.0 {
+            ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
+            ArcCow::Owned(owned) => owned.clone(),
+        }
+    }
+}
+
+impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
+    fn from(value: T) -> Self {
+        Self(value.into())
+    }
+}
+
+impl Into<String> for SharedString {
+    fn into(self) -> String {
+        self.0.to_string()
+    }
+}
+
+impl Serialize for SharedString {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.as_ref())
+    }
+}
+
+impl<'de> Deserialize<'de> for SharedString {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(SharedString::from(s))
+    }
+}

crates/rich_text2/src/rich_text.rs ๐Ÿ”—

@@ -2,7 +2,7 @@ use std::{ops::Range, sync::Arc};
 
 use anyhow::bail;
 use futures::FutureExt;
-use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle};
+use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle, WindowContext};
 use language::{HighlightId, Language, LanguageRegistry};
 use util::RangeExt;
 
@@ -56,12 +56,7 @@ pub struct Mention {
 }
 
 impl RichText {
-    pub fn element(
-        &self,
-        // syntax: Arc<SyntaxTheme>,
-        //  style: RichTextStyle,
-        // cx: &mut ViewContext<V>,
-    ) -> AnyElement {
+    pub fn element(&self, _cx: &mut WindowContext) -> AnyElement {
         todo!();
 
         // let mut region_id = 0;

crates/workspace2/src/status_bar.rs ๐Ÿ”—

@@ -1,12 +1,10 @@
-use std::any::TypeId;
-
 use crate::{ItemHandle, Pane};
 use gpui::{
     div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
     WindowContext,
 };
-use ui::h_stack;
-use ui::prelude::*;
+use std::any::TypeId;
+use ui::{h_stack, prelude::*};
 use util::ResultExt;
 
 pub trait StatusItemView: Render {
@@ -47,8 +45,8 @@ impl Render for StatusBar {
             .w_full()
             .h_8()
             .bg(cx.theme().colors().status_bar_background)
-            .child(h_stack().gap_1().child(self.render_left_tools(cx)))
-            .child(h_stack().gap_4().child(self.render_right_tools(cx)))
+            .child(self.render_left_tools(cx))
+            .child(self.render_right_tools(cx))
     }
 }
 

crates/zed2/src/zed2.rs ๐Ÿ”—

@@ -160,8 +160,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
             let channels_panel =
                 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-            // let chat_panel =
-            //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+            let chat_panel =
+                collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
             // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
             //     workspace_handle.clone(),
             //     cx.clone(),
@@ -171,14 +171,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 terminal_panel,
                 assistant_panel,
                 channels_panel,
-                //     chat_panel,
+                chat_panel,
                 //     notification_panel,
             ) = futures::try_join!(
                 project_panel,
                 terminal_panel,
                 assistant_panel,
                 channels_panel,
-                //     chat_panel,
+                chat_panel,
                 //     notification_panel,
             )?;
 
@@ -188,7 +188,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 workspace.add_panel(terminal_panel, cx);
                 workspace.add_panel(assistant_panel, cx);
                 workspace.add_panel(channels_panel, cx);
-                //     workspace.add_panel(chat_panel, cx);
+                workspace.add_panel(chat_panel, cx);
                 //     workspace.add_panel(notification_panel, cx);
 
                 // if !was_deserialized