diff --git a/Cargo.lock b/Cargo.lock index a40aa7d89c..deed9a176e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "futures 0.3.28", "fuzzy", "gpui", + "language", "log", "menu", "picker", diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a59fec1553..aa99d5c10b 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::ChannelId; +use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::Client; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; @@ -16,6 +16,7 @@ pub struct ChannelBuffer { channel_id: ChannelId, collaborators: Vec, buffer: ModelHandle, + channel_store: ModelHandle, client: Arc, _subscription: client::Subscription, } @@ -33,7 +34,8 @@ impl Entity for ChannelBuffer { } impl ChannelBuffer { - pub fn join_channel( + pub(crate) fn new( + channel_store: ModelHandle, channel_id: ChannelId, client: Arc, cx: &mut AppContext, @@ -65,6 +67,7 @@ impl ChannelBuffer { buffer, client, channel_id, + channel_store, collaborators, _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), } @@ -161,4 +164,11 @@ impl ChannelBuffer { pub fn collaborators(&self) -> &[proto::Collaborator] { &self.collaborators } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() + } } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index b9b2c98acd..a6aad19d03 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -13,6 +13,8 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; +use crate::channel_buffer::ChannelBuffer; + pub type ChannelId = u64; pub struct ChannelStore { @@ -151,6 +153,14 @@ impl ChannelStore { self.channels_by_id.get(&channel_id) } + pub fn open_channel_buffer( + &self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + ChannelBuffer::new(cx.handle(), channel_id, self.client.clone(), cx) + } + pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { self.channel_paths.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index d9880496f6..db98c6abdc 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,6 +1,5 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -use channel::channel_buffer::ChannelBuffer; use client::UserId; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; @@ -22,8 +21,9 @@ async fn test_core_channel_buffers( .await; // Client A joins the channel buffer - let channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) .await .unwrap(); @@ -45,8 +45,9 @@ async fn test_core_channel_buffers( assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); // Client B joins the channel buffer - let channel_buffer_b = cx_b - .update(|cx| ChannelBuffer::join_channel(zed_id, client_b.client().to_owned(), cx)) + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx)) .await .unwrap(); @@ -79,8 +80,9 @@ async fn test_core_channel_buffers( }); // Client A rejoins the channel buffer - let _channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channels, cx| channels.open_channel_buffer(zed_id, cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -104,7 +106,8 @@ async fn test_core_channel_buffers( }); // TODO: - // - Test synchronizing offline updates, what happens to A's channel buffer? + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open } #[track_caller] diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index e0177f6609..1ecb4b8422 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -34,6 +34,7 @@ editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +language = { path = "../language" } menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs new file mode 100644 index 0000000000..27a2d678f5 --- /dev/null +++ b/crates/collab_ui/src/channel_view.rs @@ -0,0 +1,69 @@ +use channel::channel_buffer::ChannelBuffer; +use editor::Editor; +use gpui::{ + actions, + elements::{ChildView, Label}, + AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, +}; +use language::Language; +use std::sync::Arc; +use workspace::item::{Item, ItemHandle}; + +actions!(channel_view, [Deploy]); + +pub(crate) fn init(cx: &mut AppContext) { + // TODO +} + +pub struct ChannelView { + editor: ViewHandle, + channel_buffer: ModelHandle, +} + +impl ChannelView { + pub fn new( + channel_buffer: ModelHandle, + language: Arc, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + Self { + editor, + channel_buffer, + } + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } +} + +impl Item for ChannelView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let channel_name = self + .channel_buffer + .read(cx) + .channel(cx) + .map_or("[Deleted channel]".to_string(), |channel| { + format!("#{}", channel.name) + }); + Label::new(channel_name, style.label.to_owned()).into_any() + } +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ab692dd166..0eb6a65984 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -42,7 +42,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + channel_view::{self, ChannelView}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -77,6 +80,11 @@ struct RenameChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct OpenChannelBuffer { + channel_id: u64, +} + actions!( collab_panel, [ @@ -96,7 +104,8 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapse + ToggleCollapse, + OpenChannelBuffer ] ); @@ -106,6 +115,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); + channel_view::init(cx); cx.add_action(CollabPanel::cancel); cx.add_action(CollabPanel::select_next); @@ -121,7 +131,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); cx.add_action(CollabPanel::collapse_selected_channel); - cx.add_action(CollabPanel::expand_selected_channel) + cx.add_action(CollabPanel::expand_selected_channel); + cx.add_action(CollabPanel::open_channel_buffer); } #[derive(Debug)] @@ -1888,6 +1899,7 @@ impl CollabPanel { vec![ ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), ContextMenuItem::Separator, @@ -2207,6 +2219,34 @@ impl CollabPanel { } } + fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { + let workspace = self.workspace; + let open = self.channel_store.update(cx, |channel_store, cx| { + channel_store.open_channel_buffer(action.channel_id, cx) + }); + + cx.spawn(|_, mut cx| async move { + let channel_buffer = open.await?; + + let markdown = workspace + .read_with(&cx, |workspace, _| { + workspace + .app_state() + .languages + .language_for_name("Markdown") + })? + .await?; + + workspace.update(&mut cx, |workspace, cx| { + let channel_view = cx.add_view(|cx| ChannelView::new(channel_buffer, markdown, cx)); + workspace.add_item(Box::new(channel_view), cx); + })?; + + anyhow::Ok(()) + }) + .detach(); + } + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { return; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 5420dd1db5..04644b62d9 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +pub mod channel_view; pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 03625c80e7..890bd55a7f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4687,12 +4687,13 @@ impl AnyWeakModelHandle { } } -#[derive(Copy)] pub struct WeakViewHandle { any_handle: AnyWeakViewHandle, view_type: PhantomData, } +impl Copy for WeakViewHandle {} + impl Debug for WeakViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("WeakViewHandle<{}>", type_name::()))