From 213ed2028cefb212ac039ec9a22585e8245f92a6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 7 Dec 2023 21:05:07 -0700 Subject: [PATCH] Get more of chat panel compiling, but lots of todos --- crates/channel2/src/channel_store.rs | 11 +- .../src/channel_store/channel_index.rs | 8 +- crates/collab2/src/tests/channel_tests.rs | 64 +- .../src/tests/random_channel_buffer_tests.rs | 8 +- crates/collab_ui2/src/chat_panel.rs | 1548 +++++++++-------- .../src/chat_panel/message_editor.rs | 225 +-- crates/collab_ui2/src/collab_panel.rs | 4 +- crates/collab_ui2/src/collab_ui.rs | 7 +- crates/gpui2/src/app.rs | 5 + crates/gpui2/src/elements/list.rs | 37 +- crates/gpui2/src/gpui2.rs | 62 +- crates/gpui2/src/shared_string.rs | 101 ++ 12 files changed, 1073 insertions(+), 1007 deletions(-) create mode 100644 crates/gpui2/src/shared_string.rs diff --git a/crates/channel2/src/channel_store.rs b/crates/channel2/src/channel_store.rs index 5fcc2b95668f423eac3d1874cb15c444e63bb0dd..1bd987274c6b9d7b5267458c7cc59d2e75be2be6 100644 --- a/crates/channel2/src/channel_store.rs +++ b/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, diff --git a/crates/channel2/src/channel_store/channel_index.rs b/crates/channel2/src/channel_store/channel_index.rs index 97b2ab6318630113a90e674313a3dba9fd66d79e..2682cf6ae290b5729672645f9cdfc1a7e25b4933 100644 --- a/crates/channel2/src/channel_store/channel_index.rs +++ b/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) } diff --git a/crates/collab2/src/tests/channel_tests.rs b/crates/collab2/src/tests/channel_tests.rs index 8ce5d99b80d3c630a81181e5f03f78d385186a10..49e7060301a4279adda4439aa39784b066ed76dd 100644 --- a/crates/collab2/src/tests/channel_tests.rs +++ b/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, }) diff --git a/crates/collab2/src/tests/random_channel_buffer_tests.rs b/crates/collab2/src/tests/random_channel_buffer_tests.rs index 14b5da028795ace3b0c779353f4a7cf092c954a5..f980f7d9086a15329983243590911e482864532c 100644 --- a/crates/collab2/src/tests/random_channel_buffer_tests.rs +++ b/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, Arc)>, }, Noop, diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index 0f6d6c3cd1a9085d3e848b821bdf93dda6d5ba5e..a7883529a8bc5cec4ff1e9c1c60d9d47e957adce 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -1,777 +1,785 @@ -// 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, px, serde_json, AnyElement, AnyView, AppContext, AsyncAppContext, Div, -// Entity, EventEmitter, FocusableView, ListOffset, ListScrollHandle, Model, Orientation, Render, -// Subscription, Task, View, ViewContext, 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 time::{OffsetDateTime, UtcOffset}; -// use ui::{h_stack, v_stack, Avatar, Button, Label}; -// 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, -// channel_store: Model, -// languages: Arc, -// list_scroll: ListScrollHandle, -// active_chat: Option<(Model, Subscription)>, -// input_editor: View, -// local_timezone: UtcOffset, -// fs: Arc, -// width: Option, -// active: bool, -// pending_serialization: Task>, -// subscriptions: Vec, -// workspace: WeakView, -// is_scrolled_to_bottom: bool, -// has_focus: bool, -// markdown_data: HashMap, -// } - -// #[derive(Serialize, Deserialize)] -// struct SerializedChatPanel { -// width: Option, -// } - -// #[derive(Debug)] -// pub enum Event { -// DockPositionChanged, -// Focus, -// Dismissed, -// } - -// actions!(LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall); +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, + Div, EventEmitter, FocusableView, ListState, Model, Render, 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 time::{OffsetDateTime, UtcOffset}; +use ui::prelude::WindowContext; +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, + channel_store: Model, + languages: Arc, + message_list: ListState, + active_chat: Option<(Model, Subscription)>, + input_editor: View, + local_timezone: UtcOffset, + fs: Arc, + width: Option, + active: bool, + pending_serialization: Task>, + subscriptions: Vec, + workspace: WeakView, + is_scrolled_to_bottom: bool, + has_focus: bool, + markdown_data: HashMap, +} + +#[derive(Serialize, Deserialize)] +struct SerializedChatPanel { + width: Option, +} + +#[derive(Debug)] +pub enum Event { + DockPositionChanged, + Focus, + Dismissed, +} + +actions!(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) -> View { -// 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, 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 = &cx.theme().chat_panel.channel_select; -// // SelectStyle { -// // header: Default::default(), -// // menu: style.menu, -// // } -// // }) -// // }); - -// // let mut message_list = ListState::new(0, Orientation::Bottom, 10., move |this, ix, cx| { -// // this.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; -// // })); - -// cx.add_view(|cx| { -// let mut this = Self { -// fs, -// client, -// channel_store, -// languages, -// list_scroll: ListScrollHandle::new(), -// active_chat: Default::default(), -// pending_serialization: Task::ready(None), -// input_editor, -// 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::(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> { -// self.active_chat.as_ref().map(|(chat, _)| chat.clone()) -// } - -// pub fn load(workspace: WeakView, cx: AsyncAppContext) -> Task>> { -// 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::(&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) { -// 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) { -// 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: Model, cx: &mut ViewContext) { -// 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, -// _: Model, -// event: &ChannelChatEvent, -// cx: &mut ViewContext, -// ) { -// 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) -> AnyElement { -// v_stack() -// .child(Label::new( -// self.active_chat.map_or(Default::default(), |c| { -// c.0.read(cx).channel(cx)?.name.into() -// }), -// )) -// .child(self.render_active_channel_messages(cx)) -// .child(self.input_editor.to_any()) -// .into_any() -// } - -// fn render_active_channel_messages(&self, cx: &mut ViewContext) -> AnyElement { -// if self.active_chat.is_some() { -// list( -// Orientation::Bottom, -// 10., -// cx.listener(move |this, ix, cx| this.render_message(ix, cx)), -// ) -// .into_any_element() -// } else { -// div().into_any_element() -// } -// } - -// fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { -// 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 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 -// }; - -// if is_continuation { -// h_stack() -// .child(text.element(cx)) -// .child(render_remove(message_id_to_remove, cx)) -// .mb_1() -// .into_any() -// } else { -// v_stack() -// .child( -// h_stack() -// .child(Avatar::data(message.sender.avatar.clone())) -// .child(Label::new(message.sender.github_login.clone())) -// .child( -// Label::new(format_timestamp( -// message.timestamp, -// now, -// self.local_timezone, -// )) -// .flex(1., true), -// ) -// .child(render_remove(message_id_to_remove, cx)) -// .align_children_center(), -// ) -// .child( -// h_stack() -// .child(text.element(cx)) -// .child(render_remove(None, cx)), -// ) -// .mb_1() -// .into_any() -// } -// } - -// fn render_markdown_with_mentions( -// language_registry: &Arc, -// 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::>(); - -// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) -// } - -// // fn render_channel_name( -// // channel_store: &Model, -// // ix: usize, -// // item_type: ItemType, -// // is_hovered: bool, -// // workspace: WeakView, -// // cx: &mut ViewContext { -// // 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::(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::( -// // channel_id as usize, -// // "Open Notes", -// // Some(Box::new(OpenChannelNotes)), -// // tooltip_style.clone(), -// // cx, -// // ) -// // .flex_float(), -// // MouseEventHandler::new::(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::( -// // 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, cx: &mut ViewContext) -> AnyElement { -// enum SignInPromptLabel {} - -// Button::new("sign-in", "Sign in to use chat") -// .on_click(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) { -// 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) { -// 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) { -// 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, -// cx: &mut ViewContext, -// ) -> Task> { -// 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.list_scroll.scroll_to(ListOffset { -// item_ix, -// offset_in_item: px(0.0), -// }); -// cx.notify(); -// } -// })?; -// } -// } - -// Ok(()) -// }) -// } - -// fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext) { -// 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) { -// 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, cx: &mut ViewContext) -> AnyElement { -// enum DeleteMessage {} - -// message_id_to_remove -// .map(|id| { -// MouseEventHandler::new::(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 EventEmitter for ChatPanel {} - -// impl Render for ChatPanel { -// type Element = Div; - -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// div() -// .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) { -// settings::update_settings_file::(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, cx: &mut ViewContext) { -// self.width = size; -// self.serialize(cx); -// cx.notify(); -// } - -// fn set_active(&mut self, active: bool, cx: &mut ViewContext) { -// 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 { -// todo!() -// } - -// fn icon(&self, cx: &ui::prelude::WindowContext) -> Option { -// Some(ui::Icon::MessageBubbles) -// } - -// fn toggle_action(&self) -> Box { -// todo!() -// } -// } - -// 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()) -// } +// cx.add_action(ChatPanel::send); +// cx.add_action(ChatPanel::load_more_messages); +// cx.add_action(ChatPanel::open_notes); +// cx.add_action(ChatPanel::join_call); // } -// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { -// 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) +impl ChatPanel { + pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + 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(); + + // let channel_select = cx.build_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 = &cx.theme().chat_panel.channel_select; + // SelectStyle { + // header: Default::default(), + // menu: style.menu, + // } + // }) + // }); + + cx.build_view(|cx| { + let view: View = 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(), + 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::( + 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> { + self.active_chat.as_ref().map(|(chat, _)| chat.clone()) + } + + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + 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::(&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) { + 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 update_channel_count(&mut self, cx: &mut ViewContext) { + // 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: Model, cx: &mut ViewContext) { + 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, + _: Model, + event: &ChannelChatEvent, + cx: &mut ViewContext, + ) { + 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) { + 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) -> AnyElement { + todo!() + // v_stack() + // .child(Label::new( + // self.active_chat.map_or(Default::default(), |c| { + // c.0.read(cx).channel(cx)?.name.clone() + // }), + // )) + // .child(self.render_active_channel_messages(cx)) + // .child(self.input_editor.clone()) + // .into_any() + } + + fn render_active_channel_messages(&self, cx: &mut ViewContext) -> AnyElement { + if self.active_chat.is_some() { + list(self.message_list.clone()).into_any_element() + } else { + div().into_any_element() + } + } + + fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + todo!() + // 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 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 + // }; + + // if is_continuation { + // h_stack() + // .child(text.element(cx)) + // .child(render_remove(message_id_to_remove, cx)) + // .mb_1() + // .into_any() + // } else { + // v_stack() + // .child( + // h_stack() + // .child(Avatar::data(message.sender.avatar.clone())) + // .child(Label::new(message.sender.github_login.clone())) + // .child( + // Label::new(format_timestamp( + // message.timestamp, + // now, + // self.local_timezone, + // )) + // .flex(1., true), + // ) + // .child(render_remove(message_id_to_remove, cx)) + // .align_children_center(), + // ) + // .child( + // h_stack() + // .child(text.element(cx)) + // .child(render_remove(None, cx)), + // ) + // .mb_1() + // .into_any() + // } + } + + fn render_markdown_with_mentions( + language_registry: &Arc, + 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::>(); + + rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) + } + + // fn render_channel_name( + // channel_store: &Model, + // ix: usize, + // item_type: ItemType, + // is_hovered: bool, + // workspace: WeakView, + // cx: &mut ViewContext { + // 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::(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::( + // channel_id as usize, + // "Open Notes", + // Some(Box::new(OpenChannelNotes)), + // tooltip_style.clone(), + // cx, + // ) + // .flex_float(), + // MouseEventHandler::new::(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::( + // 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, cx: &mut ViewContext) -> AnyElement { + todo!() + // enum SignInPromptLabel {} + + // Button::new("sign-in", "Sign in to use chat") + // .on_click(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) { + 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) { + 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) { + 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, + cx: &mut ViewContext, + ) -> Task> { + todo!() + // 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, _: &OpenChannelNotes, cx: &mut ViewContext) { + 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, _: &JoinCall, cx: &mut ViewContext) { + 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, cx: &mut ViewContext) -> AnyElement { + todo!() + // enum DeleteMessage {} + + // message_id_to_remove + // .map(|id| { + // MouseEventHandler::new::(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 EventEmitter for ChatPanel {} + +impl Render for ChatPanel { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div() + .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) { + settings::update_settings_file::(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, cx: &mut ViewContext) { + self.width = size; + self.serialize(cx); + cx.notify(); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + 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 { + todo!() + } + + fn icon(&self, _cx: &WindowContext) -> Option { + Some(ui::Icon::MessageBubbles) + } + + fn toggle_action(&self) -> Box { + todo!() + } +} + +impl EventEmitter 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()) + } +} + +// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { +// todo!() +// // 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)] diff --git a/crates/collab_ui2/src/chat_panel/message_editor.rs b/crates/collab_ui2/src/chat_panel/message_editor.rs index db9601564dc8899fd153aae06356b98f118f5d31..6583818481bb0b5ce1b02f64f8ae4c902dadbcb2 100644 --- a/crates/collab_ui2/src/chat_panel/message_editor.rs +++ b/crates/collab_ui2/src/chat_panel/message_editor.rs @@ -2,7 +2,9 @@ use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams}; use client::UserId; use collections::HashMap; use editor::{AnchorRangeExt, Editor}; -use gpui::{AnyView, AsyncAppContext, Model, Render, Task, View, ViewContext, WeakView}; +use gpui::{ + AnyView, AsyncWindowContext, Model, Render, SharedString, Task, View, ViewContext, WeakView, +}; use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry}; use lazy_static::lazy_static; use project::search::SearchQuery; @@ -46,15 +48,14 @@ 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(()) - }) - .detach_and_log_err(cx); + cx.spawn(|_, mut cx| async move { + let markdown = markdown.await?; + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); Self { editor, @@ -69,7 +70,7 @@ impl MessageEditor { pub fn set_channel( &mut self, channel_id: u64, - channel_name: Option, + channel_name: Option, cx: &mut ViewContext, ) { self.editor.update(cx, |editor, cx| { @@ -137,7 +138,9 @@ impl MessageEditor { 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; })); } @@ -146,10 +149,10 @@ impl MessageEditor { async fn find_mentions( this: WeakView, 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) @@ -186,6 +189,10 @@ impl MessageEditor { }) .ok(); } + + pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + todo!() + } } impl Render for MessageEditor { @@ -196,98 +203,98 @@ impl Render for MessageEditor { } } -#[cfg(test)] -mod tests { - use super::*; - use client::{Client, User, UserStore}; - use gpui::{TestAppContext, WindowHandle}; - use language::{Language, LanguageConfig}; - use rpc::proto; - use settings::SettingsStore; - use util::{http::FakeHttpClient, test::marked_text_ranges}; - - #[gpui::test] - async fn test_message_editor(cx: &mut TestAppContext) { - let editor = init_test(cx); - let editor = editor.root(cx); - - editor.update(cx, |editor, cx| { - editor.set_members( - vec![ - ChannelMembership { - user: Arc::new(User { - github_login: "a-b".into(), - id: 101, - avatar: None, - }), - kind: proto::channel_member::Kind::Member, - role: proto::ChannelRole::Member, - }, - ChannelMembership { - user: Arc::new(User { - github_login: "C_D".into(), - id: 102, - avatar: None, - }), - kind: proto::channel_member::Kind::Member, - role: proto::ChannelRole::Member, - }, - ], - cx, - ); - - editor.editor.update(cx, |editor, cx| { - editor.set_text("Hello, @a-b! Have you met @C_D?", cx) - }); - }); - - cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL); - - editor.update(cx, |editor, cx| { - let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false); - assert_eq!( - editor.take_message(cx), - MessageParams { - text, - mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], - } - ); - }); - } - - fn init_test(cx: &mut TestAppContext) -> WindowHandle { - cx.foreground().forbid_parking(); - - cx.update(|cx| { - let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); - language::init(cx); - editor::init(cx); - client::init(&client, cx); - channel::init(&client, user_store, cx); - }); - - let language_registry = Arc::new(LanguageRegistry::test()); - language_registry.add(Arc::new(Language::new( - LanguageConfig { - name: "Markdown".into(), - ..Default::default() - }, - Some(tree_sitter_markdown::language()), - ))); - - let editor = cx.add_window(|cx| { - MessageEditor::new( - language_registry, - ChannelStore::global(cx), - cx.add_view(|cx| Editor::auto_height(4, cx)), - cx, - ) - }); - cx.foreground().run_until_parked(); - editor - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use client::{Client, User, UserStore}; +// use gpui::{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 { +// 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 +// } +// } diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index bfef193cf73cc3b4129815bf305d88653d7e88ba..ec6018cd684f375bea180e27f0525adc2d9090dd 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -852,7 +852,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(), }, )); @@ -2262,7 +2262,7 @@ impl CollabPanel { } }; - Some(channel.name.as_str()) + Some(channel.name.as_ref()) }); if let Some(name) = channel_name { diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index fba76932fc01749eaa8ddee7c19264aaecae455e..3f571c13eed5450103ac2e25978311898088a3b6 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/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::() -// } +fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool { + cx.is_staff() || cx.has_flag::() +} diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 7d9ca68a82e05279b35e2a23c5c87b25be4315e5..842ac0b7044a6b88e68a7fb8c331f6c0db9ebb31 100644 --- a/crates/gpui2/src/app.rs +++ b/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 } => { diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 22666f8f24094b44bf256a29bb13f3fd777ce344..ab26c6f485e2496398192aaad6f2b028ab5f33dd 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -1,6 +1,6 @@ use crate::{ px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels, - Point, ScrollWheelEvent, Size, Style, StyleRefinement, ViewContext, WindowContext, + Point, ScrollWheelEvent, Size, Style, StyleRefinement, WindowContext, }; use collections::VecDeque; use std::{cell::RefCell, ops::Range, rc::Rc}; @@ -26,14 +26,14 @@ struct StateInner { render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, - orientation: Orientation, + alignment: ListAlignment, overdraw: Pixels, #[allow(clippy::type_complexity)] scroll_handler: Option>, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Orientation { +pub enum ListAlignment { Top, Bottom, } @@ -70,28 +70,23 @@ struct UnrenderedCount(usize); struct Height(Pixels); impl ListState { - pub fn new( + pub fn new( element_count: usize, - orientation: Orientation, + orientation: ListAlignment, overdraw: Pixels, - cx: &mut ViewContext, - mut render_item: F, + render_item: F, ) -> Self where - F: 'static + FnMut(&mut V, usize, &mut ViewContext) -> AnyElement, - V: 'static, + F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement, { let mut items = SumTree::new(); items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); - let view = cx.view().clone(); Self(Rc::new(RefCell::new(StateInner { last_layout_width: None, - render_item: Box::new(move |ix, cx| { - view.update(cx, |view, cx| render_item(view, ix, cx)) - }), + render_item: Box::new(render_item), items, logical_scroll_top: None, - orientation, + alignment: orientation, overdraw, scroll_handler: None, }))) @@ -179,7 +174,7 @@ impl StateInner { .max(px(0.)) .min(scroll_max); - if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max { + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { self.logical_scroll_top = None; } else { let mut cursor = self.items.cursor::(); @@ -208,12 +203,12 @@ impl StateInner { fn logical_scroll_top(&self) -> ListOffset { self.logical_scroll_top - .unwrap_or_else(|| match self.orientation { - Orientation::Top => ListOffset { + .unwrap_or_else(|| match self.alignment { + ListAlignment::Top => ListOffset { item_ix: 0, offset_in_item: px(0.), }, - Orientation::Bottom => ListOffset { + ListAlignment::Bottom => ListOffset { item_ix: self.items.summary().count, offset_in_item: px(0.), }, @@ -344,12 +339,12 @@ impl Element for List { offset_in_item: rendered_height - bounds.size.height, }; - match state.orientation { - Orientation::Top => { + 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); } - Orientation::Bottom => { + ListAlignment::Bottom => { scroll_top = ListOffset { item_ix: cursor.start().0, offset_in_item: rendered_height - bounds.size.height, diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 2c5c73e839bc4274c1b50c5e346d41e56566ff56..fedea33400f1ef774ebfcff5602f38000f0e16f4 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/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::*; @@ -76,6 +79,7 @@ use serde::{Deserialize, Serialize}; use std::{ any::{Any, TypeId}, borrow::{Borrow, BorrowMut}, + sync::Arc, }; use taffy::TaffyLayoutEngine; @@ -210,61 +214,3 @@ impl Flatten for Result { 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 for SharedString { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Borrow 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>> From for SharedString { - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Serialize for SharedString { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(self.as_ref()) - } -} - -impl<'de> Deserialize<'de> for SharedString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Ok(SharedString::from(s)) - } -} diff --git a/crates/gpui2/src/shared_string.rs b/crates/gpui2/src/shared_string.rs new file mode 100644 index 0000000000000000000000000000000000000000..c19d439e316f2de5956b249ae762e0d168f72a82 --- /dev/null +++ b/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 for SharedString { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Borrow 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 for SharedString { + fn eq(&self, other: &String) -> bool { + self.as_ref() == other + } +} + +impl PartialEq for String { + fn eq(&self, other: &SharedString) -> bool { + self == other.as_ref() + } +} + +impl PartialEq 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> for SharedString { + fn into(self) -> Arc { + match self.0 { + ArcCow::Borrowed(borrowed) => Arc::from(borrowed), + ArcCow::Owned(owned) => owned.clone(), + } + } +} + +impl>> From for SharedString { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +impl Into for SharedString { + fn into(self) -> String { + self.0.to_string() + } +} + +impl Serialize for SharedString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +impl<'de> Deserialize<'de> for SharedString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(SharedString::from(s)) + } +}