WIP: Add channel DAG related RPC messages, change update message

Mikayla created

Change summary

crates/channel/src/channel_store.rs         |  26 +++-
crates/channel/src/channel_store_tests.rs   |   2 
crates/collab/src/db/queries/channels.rs    | 124 ++++++++++++--------
crates/collab/src/db/tests/channel_tests.rs |  99 ++++++++++++---
crates/collab/src/rpc.rs                    |  75 +++++++++++-
crates/collab/src/tests/channel_tests.rs    | 139 ++++++++++++++++++++++
crates/rpc/proto/zed.proto                  |  30 +++-
crates/rpc/src/proto.rs                     |   5 
8 files changed, 402 insertions(+), 98 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -323,6 +323,18 @@ impl ChannelStore {
         })
     }
 
+
+    pub fn move_channel(&mut self, channel_id: ChannelId, from_parent: Option<ChannelId>, to: Option<ChannelId>, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let client = self.client.clone();
+        cx.spawn(|_, _| async move {
+            let _ = client
+                .request(proto::MoveChannel { channel_id, from_parent, to })
+                .await?;
+
+           Ok(())
+        })
+    }
+
     pub fn invite_member(
         &mut self,
         channel_id: ChannelId,
@@ -502,7 +514,7 @@ impl ChannelStore {
     pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
         let client = self.client.clone();
         async move {
-            client.request(proto::RemoveChannel { channel_id }).await?;
+            client.request(proto::DeleteChannel { channel_id }).await?;
             Ok(())
         }
     }
@@ -690,17 +702,17 @@ impl ChannelStore {
             }
         }
 
-        let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
+        let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty();
         if channels_changed {
-            if !payload.remove_channels.is_empty() {
+            if !payload.delete_channels.is_empty() {
                 self.channels_by_id
-                    .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+                    .retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
                 self.channel_participants
-                    .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+                    .retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
                 self.channels_with_admin_privileges
-                    .retain(|channel_id| !payload.remove_channels.contains(channel_id));
+                    .retain(|channel_id| !payload.delete_channels.contains(channel_id));
 
-                for channel_id in &payload.remove_channels {
+                for channel_id in &payload.delete_channels {
                     let channel_id = *channel_id;
                     if let Some(OpenedModelHandle::Open(buffer)) =
                         self.opened_buffers.remove(&channel_id)

crates/channel/src/channel_store_tests.rs 🔗

@@ -122,7 +122,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
     update_channels(
         &channel_store,
         proto::UpdateChannels {
-            remove_channels: vec![1, 2],
+            delete_channels: vec![1, 2],
             ..Default::default()
         },
         cx,

crates/collab/src/db/queries/channels.rs 🔗

@@ -1,5 +1,7 @@
 use super::*;
 
+type ChannelDescendants = HashMap<ChannelId, HashSet<ChannelId>>;
+
 impl Database {
     #[cfg(test)]
     pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
@@ -68,7 +70,6 @@ impl Database {
                     ],
                 );
                 tx.execute(channel_paths_stmt).await?;
-
             } else {
                 channel_path::Entity::insert(channel_path::ActiveModel {
                     channel_id: ActiveValue::Set(channel.id),
@@ -101,7 +102,7 @@ impl Database {
         .await
     }
 
-    pub async fn remove_channel(
+    pub async fn delete_channel(
         &self,
         channel_id: ChannelId,
         user_id: UserId,
@@ -159,9 +160,7 @@ impl Database {
             let channel_paths_stmt = Statement::from_sql_and_values(
                 self.pool.get_database_backend(),
                 sql,
-                [
-                    channel_id.to_proto().into(),
-                ],
+                [channel_id.to_proto().into()],
             );
             tx.execute(channel_paths_stmt).await?;
 
@@ -335,6 +334,43 @@ impl Database {
         .await
     }
 
+    async fn get_all_channels(
+        &self,
+        parents_by_child_id: ChannelDescendants,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<Channel>> {
+        let mut channels = Vec::with_capacity(parents_by_child_id.len());
+        {
+            let mut rows = channel::Entity::find()
+                .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
+                .stream(&*tx)
+                .await?;
+            while let Some(row) = rows.next().await {
+                let row = row?;
+
+                // As these rows are pulled from the map's keys, this unwrap is safe.
+                let parents = parents_by_child_id.get(&row.id).unwrap();
+                if parents.len() > 0 {
+                    for parent in parents {
+                        channels.push(Channel {
+                            id: row.id,
+                            name: row.name.clone(),
+                            parent_id: Some(*parent),
+                        });
+                    }
+                } else {
+                    channels.push(Channel {
+                        id: row.id,
+                        name: row.name,
+                        parent_id: None,
+                    });
+                }
+            }
+        }
+
+        Ok(channels)
+    }
+
     pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
         self.transaction(|tx| async move {
             let tx = tx;
@@ -352,40 +388,12 @@ impl Database {
                 .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
                 .await?;
 
-
             let channels_with_admin_privileges = channel_memberships
                 .iter()
                 .filter_map(|membership| membership.admin.then_some(membership.channel_id))
                 .collect();
 
-            let mut channels = Vec::with_capacity(parents_by_child_id.len());
-            {
-                let mut rows = channel::Entity::find()
-                    .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(row) = rows.next().await {
-                    let row = row?;
-
-                    // As these rows are pulled from the map's keys, this unwrap is safe.
-                    let parents = parents_by_child_id.get(&row.id).unwrap();
-                    if parents.len() > 0 {
-                        for parent in parents {
-                            channels.push(Channel {
-                                id: row.id,
-                                name: row.name.clone(),
-                                parent_id: Some(*parent),
-                            });
-                        }
-                    } else {
-                        channels.push(Channel {
-                            id: row.id,
-                            name: row.name,
-                            parent_id: None,
-                        });
-                    }
-                }
-            }
+            let channels = self.get_all_channels(parents_by_child_id, &tx).await?;
 
             #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
             enum QueryUserIdsAndChannelIds {
@@ -632,7 +640,7 @@ impl Database {
         &self,
         channel_ids: impl IntoIterator<Item = ChannelId>,
         tx: &DatabaseTransaction,
-    ) -> Result<HashMap<ChannelId, HashSet<ChannelId>>> {
+    ) -> Result<ChannelDescendants> {
         let mut values = String::new();
         for id in channel_ids {
             if !values.is_empty() {
@@ -659,7 +667,7 @@ impl Database {
 
         let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
 
-        let mut parents_by_child_id: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
+        let mut parents_by_child_id: ChannelDescendants = HashMap::default();
         let mut paths = channel_path::Entity::find()
             .from_raw_sql(stmt)
             .stream(tx)
@@ -758,7 +766,7 @@ impl Database {
         from: ChannelId,
         to: ChannelId,
         tx: &DatabaseTransaction,
-    ) -> Result<()> {
+    ) -> Result<ChannelDescendants> {
         let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
         let from_descendants = self.get_channel_descendants([from], &*tx).await?;
         for ancestor in to_ancestors {
@@ -767,8 +775,6 @@ impl Database {
             }
         }
 
-
-
         let sql = r#"
                 INSERT INTO channel_paths
                 (id_path, channel_id)
@@ -806,8 +812,7 @@ impl Database {
             }
         }
 
-
-        Ok(())
+        Ok(from_descendants)
     }
 
     async fn remove_channel_from_parent(
@@ -816,8 +821,6 @@ impl Database {
         parent: ChannelId,
         tx: &DatabaseTransaction,
     ) -> Result<()> {
-
-
         let sql = r#"
                 DELETE FROM channel_paths
                 WHERE
@@ -826,14 +829,10 @@ impl Database {
         let channel_paths_stmt = Statement::from_sql_and_values(
             self.pool.get_database_backend(),
             sql,
-            [
-                parent.to_proto().into(),
-                from.to_proto().into(),
-            ],
+            [parent.to_proto().into(), from.to_proto().into()],
         );
         tx.execute(channel_paths_stmt).await?;
 
-
         Ok(())
     }
 
@@ -846,19 +845,22 @@ impl Database {
     /// - (`None`, `Some(id)`) Link the channel without removing it from any of it's parents
     /// - (`Some(id)`, `None`) Remove a channel from a given parent, and leave other parents
     /// - (`Some(id)`, `Some(id)`) Move channel from one parent to another, leaving other parents
+    ///
+    /// Returns the channel that was moved + it's sub channels
     pub async fn move_channel(
         &self,
         user: UserId,
         from: ChannelId,
         from_parent: Option<ChannelId>,
         to: Option<ChannelId>,
-    ) -> Result<()> {
+    ) -> Result<Vec<Channel>> {
         self.transaction(|tx| async move {
             // Note that even with these maxed permissions, this linking operation
             // is still insecure because you can't remove someone's permissions to a
             // channel if they've linked the channel to one where they're an admin.
             self.check_user_is_channel_admin(from, user, &*tx).await?;
 
+            let mut channel_descendants = None;
             if let Some(from_parent) = from_parent {
                 self.check_user_is_channel_admin(from_parent, user, &*tx)
                     .await?;
@@ -870,10 +872,30 @@ impl Database {
             if let Some(to) = to {
                 self.check_user_is_channel_admin(to, user, &*tx).await?;
 
-                self.link_channel(from, to, &*tx).await?;
+                channel_descendants = Some(self.link_channel(from, to, &*tx).await?);
             }
 
-            Ok(())
+            let mut channel_descendants = match channel_descendants {
+                Some(channel_descendants) => channel_descendants,
+                None => self.get_channel_descendants([from], &*tx).await?,
+            };
+
+            // Repair the parent ID of the channel in case it was from a cached call
+            if let Some(channel) = channel_descendants.get_mut(&from) {
+                if let Some(from_parent) = from_parent {
+                    channel.remove(&from_parent);
+                }
+                if let Some(to) = to {
+                    channel.insert(to);
+                }
+            }
+
+
+            let channels = self
+                .get_all_channels(channel_descendants, &*tx)
+                .await?;
+
+            Ok(channels)
         })
         .await
     }

crates/collab/src/db/tests/channel_tests.rs 🔗

@@ -181,11 +181,11 @@ async fn test_channels(db: &Arc<Database>) {
     );
 
     // Remove a single channel
-    db.remove_channel(crdb_id, a_id).await.unwrap();
+    db.delete_channel(crdb_id, a_id).await.unwrap();
     assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
 
     // Remove a channel tree
-    let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
+    let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
     channel_ids.sort();
     assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
     assert_eq!(user_ids, &[a_id]);
@@ -647,7 +647,8 @@ async fn test_channels_moving(db: &Arc<Database>) {
     );
 
     // Make a link
-    db.move_channel(a_id, livestreaming_dag_sub_id, None, Some(livestreaming_id))
+    let channels = db
+        .move_channel(a_id, livestreaming_dag_sub_id, None, Some(livestreaming_id))
         .await
         .unwrap();
 
@@ -655,6 +656,24 @@ async fn test_channels_moving(db: &Arc<Database>) {
     //    /- gpui2                /---------------------\
     // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
     //    \--------/
+
+    // make sure we're getting the new link
+    pretty_assertions::assert_eq!(
+        channels,
+        vec![
+            Channel {
+                id: livestreaming_dag_sub_id,
+                name: "livestreaming_dag_sub".to_string(),
+                parent_id: Some(livestreaming_id),
+            },
+            Channel {
+                id: livestreaming_dag_sub_id,
+                name: "livestreaming_dag_sub".to_string(),
+                parent_id: Some(livestreaming_dag_id),
+            },
+        ]
+    );
+
     let result = db.get_channels_for_user(a_id).await.unwrap();
     pretty_assertions::assert_eq!(
         result.channels,
@@ -703,7 +722,7 @@ async fn test_channels_moving(db: &Arc<Database>) {
     );
 
     // Make another link
-    db.move_channel(a_id, livestreaming_id, None, Some(gpui2_id))
+    let channels = db.move_channel(a_id, livestreaming_id, None, Some(gpui2_id))
         .await
         .unwrap();
 
@@ -711,6 +730,40 @@ async fn test_channels_moving(db: &Arc<Database>) {
     //    /- gpui2 -\             /---------------------\
     // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id
     //    \---------/
+
+    // Make sure that we're correctly getting the full sub-dag
+    pretty_assertions::assert_eq!(channels,
+        vec![Channel {
+            id: livestreaming_id,
+            name: "livestreaming".to_string(),
+            parent_id: Some(gpui2_id),
+        },
+        Channel {
+            id: livestreaming_id,
+            name: "livestreaming".to_string(),
+            parent_id: Some(zed_id),
+        },
+        Channel {
+            id: livestreaming_id,
+            name: "livestreaming".to_string(),
+            parent_id: Some(crdb_id),
+        },
+        Channel {
+            id: livestreaming_dag_id,
+            name: "livestreaming_dag".to_string(),
+            parent_id: Some(livestreaming_id),
+        },
+        Channel {
+            id: livestreaming_dag_sub_id,
+            name: "livestreaming_dag_sub".to_string(),
+            parent_id: Some(livestreaming_id),
+        },
+        Channel {
+            id: livestreaming_dag_sub_id,
+            name: "livestreaming_dag_sub".to_string(),
+            parent_id: Some(livestreaming_dag_id),
+        }]);
+
     let result = db.get_channels_for_user(a_id).await.unwrap();
     pretty_assertions::assert_eq!(
         result.channels,
@@ -764,7 +817,7 @@ async fn test_channels_moving(db: &Arc<Database>) {
     );
 
     // Remove that inner link
-    db.move_channel(a_id, livestreaming_dag_sub_id, Some(livestreaming_id), None)
+    let channels = db.move_channel(a_id, livestreaming_dag_sub_id, Some(livestreaming_id), None)
         .await
         .unwrap();
 
@@ -772,6 +825,20 @@ async fn test_channels_moving(db: &Arc<Database>) {
     //    /- gpui2 -\
     // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
     //    \---------/
+
+    // Make sure the recently removed link isn't returned
+    pretty_assertions::assert_eq!(
+        channels,
+        vec![
+            Channel {
+                id: livestreaming_dag_sub_id,
+                name: "livestreaming_dag_sub".to_string(),
+                parent_id: Some(livestreaming_dag_id),
+            },
+        ]
+    );
+
+
     let result = db.get_channels_for_user(a_id).await.unwrap();
     pretty_assertions::assert_eq!(
         result.channels,
@@ -824,24 +891,10 @@ async fn test_channels_moving(db: &Arc<Database>) {
         .await
         .unwrap();
 
-
     // DAG is now:
     //    /- gpui2
     // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
     //    \---------/
-    //
-    // zed/gpui2
-    // zed/crdb
-    // zed/crdb/livestreaming
-    //
-    // zed/crdb/livestreaming
-    // zed/crdb/livestreaming/livestreaming_dag
-    // zed/crdb/livestreaming/livestreaming_dag/livestreaming_dag_sub
-
-    // zed/livestreaming
-    // zed/livestreaming/livestreaming_dag
-    // zed/livestreaming/livestreaming_dag/livestreaming_dag_sub
-    //
     let result = db.get_channels_for_user(a_id).await.unwrap();
     pretty_assertions::assert_eq!(
         result.channels,
@@ -936,7 +989,7 @@ async fn test_channels_moving(db: &Arc<Database>) {
     );
 
     // Deleting a channel should not delete children that still have other parents
-    db.remove_channel(gpui2_id, a_id).await.unwrap();
+    db.delete_channel(gpui2_id, a_id).await.unwrap();
 
     // DAG is now:
     // zed - crdb
@@ -974,12 +1027,14 @@ async fn test_channels_moving(db: &Arc<Database>) {
     );
 
     // But deleting a parent of a DAG should delete the whole DAG:
-    db.move_channel(a_id, livestreaming_id, None, Some(crdb_id)).await.unwrap();
+    db.move_channel(a_id, livestreaming_id, None, Some(crdb_id))
+        .await
+        .unwrap();
     // DAG is now:
     // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub
     //    \--------/
 
-    db.remove_channel(zed_id, a_id).await.unwrap();
+    db.delete_channel(zed_id, a_id).await.unwrap();
     let result = db.get_channels_for_user(a_id).await.unwrap();
     assert!(result.channels.is_empty())
 }

crates/collab/src/rpc.rs 🔗

@@ -250,7 +250,7 @@ impl Server {
             .add_request_handler(remove_contact)
             .add_request_handler(respond_to_contact_request)
             .add_request_handler(create_channel)
-            .add_request_handler(remove_channel)
+            .add_request_handler(delete_channel)
             .add_request_handler(invite_channel_member)
             .add_request_handler(remove_channel_member)
             .add_request_handler(set_channel_member_admin)
@@ -267,6 +267,7 @@ impl Server {
             .add_request_handler(send_channel_message)
             .add_request_handler(remove_channel_message)
             .add_request_handler(get_channel_messages)
+            .add_request_handler(move_channel)
             .add_request_handler(follow)
             .add_message_handler(unfollow)
             .add_message_handler(update_followers)
@@ -2230,23 +2231,23 @@ async fn create_channel(
     Ok(())
 }
 
-async fn remove_channel(
-    request: proto::RemoveChannel,
-    response: Response<proto::RemoveChannel>,
+async fn delete_channel(
+    request: proto::DeleteChannel,
+    response: Response<proto::DeleteChannel>,
     session: Session,
 ) -> Result<()> {
     let db = session.db().await;
 
     let channel_id = request.channel_id;
     let (removed_channels, member_ids) = db
-        .remove_channel(ChannelId::from_proto(channel_id), session.user_id)
+        .delete_channel(ChannelId::from_proto(channel_id), session.user_id)
         .await?;
     response.send(proto::Ack {})?;
 
     // Notify members of removed channels
     let mut update = proto::UpdateChannels::default();
     update
-        .remove_channels
+        .delete_channels
         .extend(removed_channels.into_iter().map(|id| id.to_proto()));
 
     let connection_pool = session.connection_pool().await;
@@ -2306,7 +2307,7 @@ async fn remove_channel_member(
         .await?;
 
     let mut update = proto::UpdateChannels::default();
-    update.remove_channels.push(channel_id.to_proto());
+    update.delete_channels.push(channel_id.to_proto());
 
     for connection_id in session
         .connection_pool()
@@ -2390,6 +2391,66 @@ async fn rename_channel(
     Ok(())
 }
 
+async fn move_channel(
+    request: proto::MoveChannel,
+    response: Response<proto::MoveChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let from_parent = request.from_parent.map(ChannelId::from_proto);
+    let to = request.to.map(ChannelId::from_proto);
+    let channels = db
+        .move_channel(
+            session.user_id,
+            channel_id,
+            from_parent,
+            to,
+        )
+        .await?;
+
+
+    if let Some(from_parent) = from_parent {
+        let members = db.get_channel_members(from_parent).await?;
+        let update = proto::UpdateChannels {
+            delete_channel_edge: vec![proto::ChannelEdge {
+                channel_id: channel_id.to_proto(),
+                parent_id: from_parent.to_proto(),
+            }],
+            ..Default::default()
+        };
+        let connection_pool = session.connection_pool().await;
+        for member_id in members {
+            for connection_id in connection_pool.user_connection_ids(member_id) {
+                session.peer.send(connection_id, update.clone())?;
+            }
+        }
+
+    }
+
+    if let Some(to) = to {
+        let members = db.get_channel_members(to).await?;
+        let connection_pool = session.connection_pool().await;
+        let update = proto::UpdateChannels {
+            channels: channels.into_iter().map(|channel| proto::Channel {
+                id: channel.id.to_proto(),
+                name: channel.name,
+                parent_id: channel.parent_id.map(ChannelId::to_proto),
+            }).collect(),
+            ..Default::default()
+        };
+        for member_id in members {
+            for connection_id in connection_pool.user_connection_ids(member_id) {
+                session.peer.send(connection_id, update.clone())?;
+            }
+        }
+    }
+
+    response.send(Ack {})?;
+
+    Ok(())
+}
+
 async fn get_channel_members(
     request: proto::GetChannelMembers,
     response: Response<proto::GetChannelMembers>,

crates/collab/src/tests/channel_tests.rs 🔗

@@ -874,6 +874,143 @@ async fn test_lost_channel_creation(
     );
 }
 
+#[gpui::test]
+async fn test_channel_moving(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+
+    let channel_a_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("channel-a", None, cx)
+        })
+        .await
+        .unwrap();
+    let channel_b_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("channel-b", Some(channel_a_id), cx)
+        })
+        .await
+        .unwrap();
+    let channel_c_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("channel-c", Some(channel_b_id), cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape:
+    // a - b - c
+    deterministic.run_until_parked();
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                depth: 0,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_c_id,
+                name: "channel-c".to_string(),
+                depth: 2,
+                user_is_admin: true,
+            },
+        ],
+    );
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.move_channel(channel_c_id, Some(channel_b_id), Some(channel_a_id), cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape:
+    //   /- c
+    // a -- b
+    deterministic.run_until_parked();
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                depth: 0,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_c_id,
+                name: "channel-c".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+        ],
+    );
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.move_channel(channel_c_id, None, Some(channel_b_id), cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape:
+    //   /------\
+    // a -- b -- c
+    deterministic.run_until_parked();
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                depth: 0,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_c_id,
+                name: "channel-c".to_string(),
+                depth: 2,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_c_id,
+                name: "channel-c".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+        ],
+    );
+}
+
 #[derive(Debug, PartialEq)]
 struct ExpectedChannel {
     depth: usize,
@@ -920,5 +1057,5 @@ fn assert_channels(
             })
             .collect::<Vec<_>>()
     });
-    assert_eq!(actual, expected_channels);
+    pretty_assertions::assert_eq!(actual, expected_channels);
 }

crates/rpc/proto/zed.proto 🔗

@@ -141,7 +141,7 @@ message Envelope {
         RespondToChannelInvite respond_to_channel_invite = 123;
         UpdateChannels update_channels = 124;
         JoinChannel join_channel = 125;
-        RemoveChannel remove_channel = 126;
+        DeleteChannel delete_channel = 126;
         GetChannelMembers get_channel_members = 127;
         GetChannelMembersResponse get_channel_members_response = 128;
         SetChannelMemberAdmin set_channel_member_admin = 129;
@@ -165,7 +165,9 @@ message Envelope {
         ChannelMessageSent channel_message_sent = 147;
         GetChannelMessages get_channel_messages = 148;
         GetChannelMessagesResponse get_channel_messages_response = 149;
-        RemoveChannelMessage remove_channel_message = 150; // Current max
+        RemoveChannelMessage remove_channel_message = 150;
+
+        MoveChannel move_channel = 151; // Current max
     }
 }
 
@@ -955,11 +957,17 @@ message LspDiskBasedDiagnosticsUpdated {}
 
 message UpdateChannels {
     repeated Channel channels = 1;
-    repeated uint64 remove_channels = 2;
-    repeated Channel channel_invitations = 3;
-    repeated uint64 remove_channel_invitations = 4;
-    repeated ChannelParticipants channel_participants = 5;
-    repeated ChannelPermission channel_permissions = 6;
+    repeated ChannelEdge delete_channel_edge = 2;
+    repeated uint64 delete_channels = 3;
+    repeated Channel channel_invitations = 4;
+    repeated uint64 remove_channel_invitations = 5;
+    repeated ChannelParticipants channel_participants = 6;
+    repeated ChannelPermission channel_permissions = 7;
+}
+
+message ChannelEdge {
+    uint64 channel_id = 1;
+    uint64 parent_id = 2;
 }
 
 message ChannelPermission {
@@ -976,7 +984,7 @@ message JoinChannel {
     uint64 channel_id = 1;
 }
 
-message RemoveChannel {
+message DeleteChannel {
     uint64 channel_id = 1;
 }
 
@@ -1074,6 +1082,12 @@ message GetChannelMessagesResponse {
     bool done = 2;
 }
 
+message MoveChannel {
+    uint64 channel_id = 1;
+    optional uint64 from_parent = 2;
+    optional uint64 to = 3;
+}
+
 message JoinChannelBuffer {
     uint64 channel_id = 1;
 }

crates/rpc/src/proto.rs 🔗

@@ -246,7 +246,8 @@ messages!(
     (UpdateBuffer, Foreground),
     (UpdateBufferFile, Foreground),
     (UpdateContacts, Foreground),
-    (RemoveChannel, Foreground),
+    (DeleteChannel, Foreground),
+    (MoveChannel, Foreground),
     (UpdateChannels, Foreground),
     (UpdateDiagnosticSummary, Foreground),
     (UpdateFollowers, Foreground),
@@ -329,8 +330,10 @@ request_messages!(
     (JoinChannel, JoinRoomResponse),
     (RemoveChannel, Ack),
     (RemoveChannelMessage, Ack),
+    (DeleteChannel, Ack),
     (RenameProjectEntry, ProjectEntryResponse),
     (RenameChannel, ChannelResponse),
+    (MoveChannel, Ack),
     (SaveBuffer, BufferSaved),
     (SearchProject, SearchProjectResponse),
     (ShareProject, ShareProjectResponse),