Add following into channel notes

Mikayla and max created

co-authored-by: max <max@zed.dev>

Change summary

crates/collab_ui/src/channel_view.rs | 108 ++++++++++++++++++++++++++++-
crates/editor/src/editor_tests.rs    |   4 
crates/editor/src/items.rs           |   3 
crates/rpc/proto/zed.proto           |   5 +
crates/workspace/src/item.rs         |   2 
crates/workspace/src/workspace.rs    |  23 +++--
6 files changed, 128 insertions(+), 17 deletions(-)

Detailed changes

crates/collab_ui/src/channel_view.rs 🔗

@@ -1,27 +1,34 @@
 use channel::channel_buffer::ChannelBuffer;
+use client::proto;
 use clock::ReplicaId;
 use collections::HashMap;
 use editor::Editor;
 use gpui::{
     actions,
     elements::{ChildView, Label},
-    AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle,
+    AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, View,
+    ViewContext, ViewHandle,
 };
 use language::Language;
 use project::Project;
 use std::sync::Arc;
-use workspace::item::{Item, ItemHandle};
+use workspace::{
+    item::{FollowableItem, Item, ItemHandle},
+    register_followable_item, ViewId,
+};
 
 actions!(channel_view, [Deploy]);
 
 pub(crate) fn init(cx: &mut AppContext) {
-    // TODO
+    register_followable_item::<ChannelView>(cx)
 }
 
 pub struct ChannelView {
     editor: ViewHandle<Editor>,
     project: ModelHandle<Project>,
     channel_buffer: ModelHandle<ChannelBuffer>,
+    remote_id: Option<ViewId>,
+    _editor_event_subscription: Subscription,
 }
 
 impl ChannelView {
@@ -34,14 +41,19 @@ impl ChannelView {
         let buffer = channel_buffer.read(cx).buffer();
         buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
         let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+        let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+
         let this = Self {
             editor,
             project,
             channel_buffer,
+            remote_id: None,
+            _editor_event_subscription,
         };
         let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx);
         this.editor
             .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx));
+
         this
     }
 
@@ -82,9 +94,15 @@ impl View for ChannelView {
         "ChannelView"
     }
 
-    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         ChildView::new(self.editor.as_any(), cx).into_any()
     }
+
+    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(self.editor.as_any())
+        }
+    }
 }
 
 impl Item for ChannelView {
@@ -104,3 +122,85 @@ impl Item for ChannelView {
         Label::new(channel_name, style.label.to_owned()).into_any()
     }
 }
+
+impl FollowableItem for ChannelView {
+    fn remote_id(&self) -> Option<workspace::ViewId> {
+        self.remote_id
+    }
+
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        self.channel_buffer.read(cx).channel(cx).map(|channel| {
+            proto::view::Variant::ChannelView(proto::view::ChannelView {
+                channel_id: channel.id,
+            })
+        })
+    }
+
+    fn from_state_proto(
+        _: ViewHandle<workspace::Pane>,
+        workspace: ViewHandle<workspace::Workspace>,
+        remote_id: workspace::ViewId,
+        state_proto: &mut Option<proto::view::Variant>,
+        cx: &mut AppContext,
+    ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+        let Some(proto::view::Variant::ChannelView(_)) = state_proto else { return None };
+        let Some(proto::view::Variant::ChannelView(state)) = state_proto.take() else { unreachable!() };
+
+        let channel_store = &workspace.read(cx).app_state().channel_store.clone();
+        let open_channel_buffer = channel_store.update(cx, |store, cx| {
+            store.open_channel_buffer(state.channel_id, cx)
+        });
+        let project = workspace.read(cx).project().to_owned();
+        let language = workspace.read(cx).app_state().languages.clone();
+        let get_markdown = language.language_for_name("Markdown");
+
+        Some(cx.spawn(|mut cx| async move {
+            let channel_buffer = open_channel_buffer.await?;
+            let markdown = get_markdown.await?;
+
+            let this = workspace
+                .update(&mut cx, move |_, cx| {
+                    cx.add_view(|cx| {
+                        let mut this = Self::new(project, channel_buffer, Some(markdown), cx);
+                        this.remote_id = Some(remote_id);
+                        this
+                    })
+                })
+                .ok_or_else(|| anyhow::anyhow!("workspace droppped"))?;
+
+            Ok(this)
+        }))
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        _: &Self::Event,
+        _: &mut Option<proto::update_view::Variant>,
+        _: &AppContext,
+    ) -> bool {
+        false
+    }
+
+    fn apply_update_proto(
+        &mut self,
+        _: &ModelHandle<Project>,
+        _: proto::update_view::Variant,
+        _: &mut ViewContext<Self>,
+    ) -> gpui::Task<anyhow::Result<()>> {
+        gpui::Task::ready(Ok(()))
+    }
+
+    fn set_leader_replica_id(
+        &mut self,
+        leader_replica_id: Option<u16>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_leader_replica_id(leader_replica_id, cx)
+        })
+    }
+
+    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
+        Editor::should_unfollow_on_event(event, cx)
+    }
+}

crates/editor/src/editor_tests.rs 🔗

@@ -6384,7 +6384,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
         .update(|cx| {
             Editor::from_state_proto(
                 pane.clone(),
-                project.clone(),
+                workspace.clone(),
                 ViewId {
                     creator: Default::default(),
                     id: 0,
@@ -6479,7 +6479,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
         .update(|cx| {
             Editor::from_state_proto(
                 pane.clone(),
-                project.clone(),
+                workspace.clone(),
                 ViewId {
                     creator: Default::default(),
                     id: 0,

crates/editor/src/items.rs 🔗

@@ -49,11 +49,12 @@ impl FollowableItem for Editor {
 
     fn from_state_proto(
         pane: ViewHandle<workspace::Pane>,
-        project: ModelHandle<Project>,
+        workspace: ViewHandle<Workspace>,
         remote_id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut AppContext,
     ) -> Option<Task<Result<ViewHandle<Self>>>> {
+        let project = workspace.read(cx).project().to_owned();
         let Some(proto::view::Variant::Editor(_)) = state else { return None };
         let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
 

crates/rpc/proto/zed.proto 🔗

@@ -1120,6 +1120,7 @@ message View {
 
     oneof variant {
         Editor editor = 3;
+        ChannelView channel_view = 4;
     }
 
     message Editor {
@@ -1132,6 +1133,10 @@ message View {
         float scroll_x = 7;
         float scroll_y = 8;
     }
+
+    message ChannelView {
+        uint64 channel_id = 1;
+    }
 }
 
 message Collaborator {

crates/workspace/src/item.rs 🔗

@@ -674,7 +674,7 @@ pub trait FollowableItem: Item {
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
         pane: ViewHandle<Pane>,
-        project: ModelHandle<Project>,
+        project: ViewHandle<Workspace>,
         id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut AppContext,

crates/workspace/src/workspace.rs 🔗

@@ -345,7 +345,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
 
 type FollowableItemBuilder = fn(
     ViewHandle<Pane>,
-    ModelHandle<Project>,
+    ViewHandle<Workspace>,
     ViewId,
     &mut Option<proto::view::Variant>,
     &mut AppContext,
@@ -362,8 +362,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
         builders.insert(
             TypeId::of::<I>(),
             (
-                |pane, project, id, state, cx| {
-                    I::from_state_proto(pane, project, id, state, cx).map(|task| {
+                |pane, workspace, id, state, cx| {
+                    I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
                         cx.foreground()
                             .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
                     })
@@ -2848,7 +2848,13 @@ impl Workspace {
         views: Vec<proto::View>,
         cx: &mut AsyncAppContext,
     ) -> Result<()> {
-        let project = this.read_with(cx, |this, _| this.project.clone())?;
+        let this = this
+            .upgrade(cx)
+            .ok_or_else(|| anyhow!("workspace dropped"))?;
+        let project = this
+            .read_with(cx, |this, _| this.project.clone())
+            .ok_or_else(|| anyhow!("window dropped"))?;
+
         let replica_id = project
             .read_with(cx, |project, _| {
                 project
@@ -2874,12 +2880,11 @@ impl Workspace {
                 let id = ViewId::from_proto(id.clone())?;
                 let mut variant = view.variant.clone();
                 if variant.is_none() {
-                    Err(anyhow!("missing variant"))?;
+                    Err(anyhow!("missing view variant"))?;
                 }
                 for build_item in &item_builders {
-                    let task = cx.update(|cx| {
-                        build_item(pane.clone(), project.clone(), id, &mut variant, cx)
-                    });
+                    let task = cx
+                        .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
                     if let Some(task) = task {
                         item_tasks.push(task);
                         leader_view_ids.push(id);
@@ -2907,7 +2912,7 @@ impl Workspace {
                 }
 
                 Some(())
-            })?;
+            });
         }
         Ok(())
     }