Cargo.lock 🔗
@@ -1529,6 +1529,7 @@ dependencies = [
"futures 0.3.28",
"fuzzy",
"gpui",
+ "language",
"log",
"menu",
"picker",
Mikayla and Max created
co-authored-by: Max <max@zed.dev>
Cargo.lock | 1
crates/channel/src/channel_buffer.rs | 14 +++
crates/channel/src/channel_store.rs | 10 ++
crates/collab/src/tests/channel_buffer_tests.rs | 19 +++--
crates/collab_ui/Cargo.toml | 1
crates/collab_ui/src/channel_view.rs | 69 +++++++++++++++++++
crates/collab_ui/src/collab_panel.rs | 46 +++++++++++
crates/collab_ui/src/collab_ui.rs | 1
crates/gpui/src/app.rs | 3
9 files changed, 150 insertions(+), 14 deletions(-)
@@ -1529,6 +1529,7 @@ dependencies = [
"futures 0.3.28",
"fuzzy",
"gpui",
+ "language",
"log",
"menu",
"picker",
@@ -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<proto::Collaborator>,
buffer: ModelHandle<language::Buffer>,
+ channel_store: ModelHandle<ChannelStore>,
client: Arc<Client>,
_subscription: client::Subscription,
}
@@ -33,7 +34,8 @@ impl Entity for ChannelBuffer {
}
impl ChannelBuffer {
- pub fn join_channel(
+ pub(crate) fn new(
+ channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
client: Arc<Client>,
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<Arc<Channel>> {
+ self.channel_store
+ .read(cx)
+ .channel_for_id(self.channel_id)
+ .cloned()
+ }
}
@@ -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<Self>,
+ ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
+ 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) {
@@ -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]
@@ -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" }
@@ -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<Editor>,
+ channel_buffer: ModelHandle<ChannelBuffer>,
+}
+
+impl ChannelView {
+ pub fn new(
+ channel_buffer: ModelHandle<ChannelBuffer>,
+ language: Arc<Language>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self> {
+ ChildView::new(self.editor.as_any(), cx).into_any()
+ }
+}
+
+impl Item for ChannelView {
+ fn tab_content<V: 'static>(
+ &self,
+ _: Option<usize>,
+ style: &theme::Tab,
+ cx: &gpui::AppContext,
+ ) -> AnyElement<V> {
+ 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()
+ }
+}
@@ -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<Client>, cx: &mut AppContext) {
settings::register::<panel_settings::CollaborationPanelSettings>(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<Client>, 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<Self>) {
+ 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<Self>) {
let Some(channel) = self.selected_channel() else {
return;
@@ -1,3 +1,4 @@
+pub mod channel_view;
pub mod collab_panel;
mod collab_titlebar_item;
mod contact_notification;
@@ -4687,12 +4687,13 @@ impl AnyWeakModelHandle {
}
}
-#[derive(Copy)]
pub struct WeakViewHandle<T> {
any_handle: AnyWeakViewHandle,
view_type: PhantomData<T>,
}
+impl<T> Copy for WeakViewHandle<T> {}
+
impl<T> Debug for WeakViewHandle<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(&format!("WeakViewHandle<{}>", type_name::<T>()))