From aa9a9be7e9668f155bf077731a375e7d85becc72 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 31 Aug 2023 17:48:51 -0700 Subject: [PATCH 01/26] Add channel moving test --- crates/collab/src/db/queries/channels.rs | 10 + crates/collab/src/db/tests.rs | 1 + crates/collab/src/db/tests/channel_tests.rs | 559 ++++++++++++++++++++ crates/collab/src/db/tests/db_tests.rs | 4 +- 4 files changed, 572 insertions(+), 2 deletions(-) create mode 100644 crates/collab/src/db/tests/channel_tests.rs diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 5da4dd14646aa6fc1280ddcc608a530fad08f60d..4705cc9415fb1ba5f741491847d6d43a0ca57a17 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -703,6 +703,16 @@ impl Database { }) .await } + + pub async fn move_channel( + &self, + user: UserId, + from: ChannelId, + to: Option, + link: bool, + ) -> Result<()> { + self.transaction(|tx| async move { todo!() }).await + } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 1d6c550865098a9afb2c5ca63e7357c64e46eaf8..3ba5af8518e4568fb0c9abe20fdfa2c468fd242c 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,4 +1,5 @@ mod buffer_tests; +mod channel_tests; mod db_tests; mod feature_flag_tests; mod message_tests; diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..04304ec84844fdfb2f68fd9311cbd39a9fbd057f --- /dev/null +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -0,0 +1,559 @@ +use rpc::{proto, ConnectionId}; + +use crate::{ + db::{Channel, Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); + +async fn test_channels(db: &Arc) { + let a_id = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let b_id = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + // Make sure that people cannot read channels they haven't been invited to + assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + let crdb_id = db + .create_channel("crdb", Some(zed_id), "2", a_id) + .await + .unwrap(); + let livestreaming_id = db + .create_channel("livestreaming", Some(zed_id), "3", a_id) + .await + .unwrap(); + let replace_id = db + .create_channel("replace", Some(zed_id), "4", a_id) + .await + .unwrap(); + + let mut members = db.get_channel_members(replace_id).await.unwrap(); + members.sort(); + assert_eq!(members, &[a_id, b_id]); + + let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); + let cargo_id = db + .create_channel("cargo", Some(rust_id), "6", a_id) + .await + .unwrap(); + + let cargo_ra_id = db + .create_channel("cargo-ra", Some(cargo_id), "7", a_id) + .await + .unwrap(); + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + }, + Channel { + id: cargo_id, + name: "cargo".to_string(), + parent_id: Some(rust_id), + }, + Channel { + id: cargo_ra_id, + name: "cargo-ra".to_string(), + parent_id: Some(cargo_id), + } + ] + ); + + let result = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + ] + ); + + // Update member permissions + let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; + assert!(set_subchannel_admin.is_err()); + let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; + assert!(set_channel_admin.is_ok()); + + let result = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + ] + ); + + // Remove a single channel + db.remove_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(); + channel_ids.sort(); + assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); + assert_eq!(user_ids, &[a_id]); + + assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); +} + +test_both_dbs!( + test_joining_channels, + test_joining_channels_postgres, + test_joining_channels_sqlite +); + +async fn test_joining_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); + + // can join a room with membership to its channel + let joined_room = db + .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(joined_room.room.participants.len(), 1); + + drop(joined_room); + // cannot join a room without membership to its channel + assert!(db + .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .await + .is_err()); +} + +test_both_dbs!( + test_channel_invites, + test_channel_invites_postgres, + test_channel_invites_sqlite +); + +async fn test_channel_invites(db: &Arc) { + db.create_server("test").await.unwrap(); + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_3 = db + .create_user( + "user3@example.com", + false, + NewUserParams { + github_login: "user3".into(), + github_user_id: 7, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); + + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, true) + .await + .unwrap(); + + let user_2_invites = db + .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + + let user_3_invites = db + .get_channel_invites_for_user(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_3_invites, &[channel_1_1]); + + let members = db + .get_channel_member_details(channel_1_1, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: false, + }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: true, + }, + ] + ); + + db.respond_to_channel_invite(channel_1_1, user_2, true) + .await + .unwrap(); + + let channel_1_3 = db + .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .await + .unwrap(); + + let members = db + .get_channel_member_details(channel_1_3, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + admin: false, + }, + ] + ); +} + +test_both_dbs!( + test_channel_renames, + test_channel_renames_postgres, + test_channel_renames_sqlite +); + +async fn test_channel_renames(db: &Arc) { + db.create_server("test").await.unwrap(); + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + + db.rename_channel(zed_id, user_1, "#zed-archive") + .await + .unwrap(); + + let zed_archive_id = zed_id; + + let (channel, _) = db + .get_channel(zed_archive_id, user_1) + .await + .unwrap() + .unwrap(); + assert_eq!(channel.name, "zed-archive"); + + let non_permissioned_rename = db + .rename_channel(zed_archive_id, user_2, "hacked-lol") + .await; + assert!(non_permissioned_rename.is_err()); + + let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; + assert!(bad_name_rename.is_err()) +} + +test_both_dbs!( + test_channels_moving, + test_channels_moving_postgres, + test_channels_moving_sqlite +); + +async fn test_channels_moving(db: &Arc) { + let a_id = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + let crdb_id = db + .create_channel("crdb", Some(zed_id), "2", a_id) + .await + .unwrap(); + + let livestreaming_id = db + .create_channel("livestreaming", Some(crdb_id), "3", a_id) + .await + .unwrap(); + + // sanity check + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(crdb_id), + }, + ] + ); + + // Move channel up + db.move_channel(a_id, livestreaming_id, Some(zed_id), false) + .await + .unwrap(); + + // Attempt to make a cycle + assert!(db + .move_channel(a_id, zed_id, Some(livestreaming_id), false) + .await + .is_err()); + + // Make a link + db.move_channel(a_id, crdb_id, Some(livestreaming_id), true) + .await + .unwrap(); + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_eq!( + result.channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(livestreaming_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + ] + ); +} diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 0e6a0529c4f72636069d5e1dab43db05d0f4de9c..b710d6460bb3c12223e22c8d7818c8952e251321 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -794,11 +794,11 @@ async fn test_joining_channels(db: &Arc) { github_login: "user2".into(), github_user_id: 6, invite_count: 0, + }, ) .await - .unwrap() - .user_id; + .unwrap() .user_id; let channel_1 = db .create_root_channel("channel_1", "1", user_1) From d5512fad0d7dc6478357f198701f358557095dee Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 7 Sep 2023 11:58:00 -0700 Subject: [PATCH 02/26] Add channel linking operation --- crates/collab/Cargo.toml | 2 +- crates/collab/src/db/queries/channels.rs | 102 +++++++- crates/collab/src/db/tests/channel_tests.rs | 266 +++++++++++++++++++- 3 files changed, 348 insertions(+), 22 deletions(-) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 792c65b075fca464f502ee1571e775c7aee623ec..c580e911bcf67fa55952380d00fc992762f14ce8 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -72,7 +72,6 @@ fs = { path = "../fs", features = ["test-support"] } git = { path = "../git", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } -pretty_assertions.workspace = true project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } @@ -81,6 +80,7 @@ workspace = { path = "../workspace", features = ["test-support"] } collab_ui = { path = "../collab_ui", features = ["test-support"] } async-trait.workspace = true +pretty_assertions.workspace = true ctor.workspace = true env_logger.workspace = true indoc.workspace = true diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 4705cc9415fb1ba5f741491847d6d43a0ca57a17..f31a1cde5dde738d2d9abfebe353e4a636a9b6a6 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -68,6 +68,8 @@ impl Database { ], ); tx.execute(channel_paths_stmt).await?; + + dbg!(channel_path::Entity::find().all(&*tx).await?); } else { channel_path::Entity::insert(channel_path::ActiveModel { channel_id: ActiveValue::Set(channel.id), @@ -336,6 +338,8 @@ impl Database { .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; + dbg!(&parents_by_child_id); + let channels_with_admin_privileges = channel_memberships .iter() .filter_map(|membership| membership.admin.then_some(membership.channel_id)) @@ -349,11 +353,24 @@ impl Database { .await?; while let Some(row) = rows.next().await { let row = row?; - channels.push(Channel { - id: row.id, - name: row.name, - parent_id: parents_by_child_id.get(&row.id).copied().flatten(), - }); + + // 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, + }); + } } } @@ -559,6 +576,7 @@ impl Database { Ok(()) } + /// Returns the channel ancestors, deepest first pub async fn get_channel_ancestors( &self, channel_id: ChannelId, @@ -566,6 +584,7 @@ impl Database { ) -> Result> { let paths = channel_path::Entity::find() .filter(channel_path::Column::ChannelId.eq(channel_id)) + .order_by(channel_path::Column::IdPath, sea_query::Order::Desc) .all(tx) .await?; let mut channel_ids = Vec::new(); @@ -586,7 +605,7 @@ impl Database { &self, channel_ids: impl IntoIterator, tx: &DatabaseTransaction, - ) -> Result>> { + ) -> Result>> { let mut values = String::new(); for id in channel_ids { if !values.is_empty() { @@ -613,7 +632,7 @@ impl Database { let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - let mut parents_by_child_id = HashMap::default(); + let mut parents_by_child_id: HashMap> = HashMap::default(); let mut paths = channel_path::Entity::find() .from_raw_sql(stmt) .stream(tx) @@ -632,7 +651,10 @@ impl Database { parent_id = Some(id); } } - parents_by_child_id.insert(path.channel_id, parent_id); + let entry = parents_by_child_id.entry(path.channel_id).or_default(); + if let Some(parent_id) = parent_id { + entry.insert(parent_id); + } } Ok(parents_by_child_id) @@ -704,12 +726,74 @@ impl Database { .await } + pub async fn link_channel(&self, user: UserId, from: ChannelId, to: ChannelId) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(to, user, &*tx).await?; + + // TODO: Downgrade this check once our permissions system isn't busted + // You should be able to safely link a member channel for your own uses. See: + // https://zed.dev/blog/this-week-at-zed-15 > Mikayla's section + // + // Note that even with these higher 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 to_ancestors = self.get_channel_ancestors(to, &*tx).await?; + let from_descendants = self.get_channel_descendants([from], &*tx).await?; + for ancestor in to_ancestors { + if from_descendants.contains_key(&ancestor) { + return Err(anyhow!("Cannot create a channel cycle").into()); + } + } + + let sql = r#" + INSERT INTO channel_paths + (id_path, channel_id) + SELECT + id_path || $1 || '/', $2 + FROM + channel_paths + WHERE + channel_id = $3 + ON CONFLICT (id_path) DO NOTHING; + "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + from.to_proto().into(), + from.to_proto().into(), + to.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + + for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id == &&from) { + for to_id in to_ids { + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + from_id.to_proto().into(), + from_id.to_proto().into(), + to_id.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + } + } + + Ok(()) + }) + .await + } + pub async fn move_channel( &self, user: UserId, from: ChannelId, to: Option, - link: bool, ) -> Result<()> { self.transaction(|tx| async move { todo!() }).await } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 04304ec84844fdfb2f68fd9311cbd39a9fbd057f..e077950a3a94f8127e64a3d1c6164be5cbb43e1d 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -486,14 +486,24 @@ async fn test_channels_moving(db: &Arc) { .await .unwrap(); + let gpui2_id = db + .create_channel("gpui2", Some(zed_id), "3", a_id) + .await + .unwrap(); + let livestreaming_id = db - .create_channel("livestreaming", Some(crdb_id), "3", a_id) + .create_channel("livestreaming", Some(crdb_id), "4", a_id) + .await + .unwrap(); + + let livestreaming_dag_id = db + .create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id) .await .unwrap(); // sanity check let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_eq!( + pretty_assertions::assert_eq!( result.channels, vec![ Channel { @@ -506,33 +516,93 @@ async fn test_channels_moving(db: &Arc) { name: "crdb".to_string(), parent_id: Some(zed_id), }, + Channel { + id: gpui2_id, + name: "gpui2".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), + }, ] ); + // Initial DAG: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag - // Move channel up - db.move_channel(a_id, livestreaming_id, Some(zed_id), false) - .await - .unwrap(); - - // Attempt to make a cycle + // Attemp to make a cycle assert!(db - .move_channel(a_id, zed_id, Some(livestreaming_id), false) + .link_channel(a_id, zed_id, livestreaming_id) .await .is_err()); // Make a link - db.move_channel(a_id, crdb_id, Some(livestreaming_id), true) + db.link_channel(a_id, livestreaming_id, zed_id) .await .unwrap(); + // DAG is now: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_eq!( - result.channels, + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_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), + }, + ] + ); + + let livestreaming_dag_sub_id = db + .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), "6", a_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // \---------/ + + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), vec![ Channel { id: zed_id, @@ -544,16 +614,188 @@ async fn test_channels_moving(db: &Arc) { name: "crdb".to_string(), parent_id: Some(zed_id), }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_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_dag_id), + }, + ] + ); + + // Make a link + db.link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 /---------------------\ + // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // \--------/ + + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, Channel { id: crdb_id, name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_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), + }, + ] + ); + + // Make another link + db.link_channel(a_id, livestreaming_id, gpui2_id) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 -\ /---------------------\ + // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // \---------/ + + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_id), + }, + 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), + }, ] ); + + // // Attempt to make a cycle + // assert!(db + // .move_channel(a_id, zed_id, Some(livestreaming_id)) + // .await + // .is_err()); + + // // Move channel up + // db.move_channel(a_id, livestreaming_id, Some(zed_id)) + // .await + // .unwrap(); + + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // pretty_assertions::assert_eq!( + // result.channels, + // vec![ + // Channel { + // id: zed_id, + // name: "zed".to_string(), + // parent_id: None, + // }, + // Channel { + // id: crdb_id, + // name: "crdb".to_string(), + // parent_id: Some(zed_id), + // }, + // Channel { + // id: crdb_id, + // name: "crdb".to_string(), + // parent_id: Some(livestreaming_id), + // }, + // Channel { + // id: livestreaming_id, + // name: "livestreaming".to_string(), + // parent_id: Some(zed_id), + // }, + // ] + // ); } From fc78db39efa8f022faa0c1d9241837c948672940 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 7 Sep 2023 13:38:23 -0700 Subject: [PATCH 03/26] Expand DAG tests to include more complex tree operations and removal behavior --- crates/collab/src/db/queries/channels.rs | 24 +- crates/collab/src/db/tests/channel_tests.rs | 289 ++++++++++++++++---- 2 files changed, 263 insertions(+), 50 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index f31a1cde5dde738d2d9abfebe353e4a636a9b6a6..0ebd4dd6e1d4a4c90f20c36b3a8d528347f156fd 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -726,7 +726,7 @@ impl Database { .await } - pub async fn link_channel(&self, user: UserId, from: ChannelId, to: ChannelId) -> Result<()> { + async fn link_channel(&self, user: UserId, from: ChannelId, to: ChannelId) -> Result<()> { self.transaction(|tx| async move { self.check_user_is_channel_admin(to, user, &*tx).await?; @@ -789,13 +789,33 @@ impl Database { .await } + async fn remove_channel_from_parent(&self, user: UserId, from: ChannelId, parent: ChannelId) -> Result<()> { + todo!() + } + + /// Move a channel from one parent to another. + /// Note that this requires a valid parent_id in the 'from_parent' field. + /// As channels are a DAG, we need to know which parent to remove the channel from. + /// Here's a list of the parameters to this function and their behavior: + /// + /// - (`None`, `None`) Noop + /// - (`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 pub async fn move_channel( &self, user: UserId, from: ChannelId, + from_parent: Option, to: Option, ) -> Result<()> { - self.transaction(|tx| async move { todo!() }).await + if let Some(to) = to { + self.link_channel(user, from, to).await?; + } + if let Some(from_parent) = from_parent { + self.remove_channel_from_parent(user, from, from_parent).await?; + } + Ok(()) } } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index e077950a3a94f8127e64a3d1c6164be5cbb43e1d..95405d4358ab18bf8b88897c305231f77cfc567e 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -502,6 +502,9 @@ async fn test_channels_moving(db: &Arc) { .unwrap(); // sanity check + // Initial DAG: + // /- gpui2 + // zed -- crdb - livestreaming - livestreaming_dag let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( result.channels, @@ -533,18 +536,33 @@ async fn test_channels_moving(db: &Arc) { }, ] ); - // Initial DAG: - // /- gpui2 - // zed -- crdb - livestreaming - livestreaming_dag // Attemp to make a cycle assert!(db - .link_channel(a_id, zed_id, livestreaming_id) + .move_channel(a_id, zed_id, None, Some(livestreaming_id)) + .await + .is_err()); + + // Attemp to remove an edge that doesn't exist + assert!(db + .move_channel(a_id, crdb_id, Some(gpui2_id), None) + .await + .is_err()); + + // Attemp to move to a channel that doesn't exist + assert!(db + .move_channel(a_id, crdb_id, Some(crate::db::ChannelId(1000)), None) + .await + .is_err()); + + // Attemp to remove an edge that doesn't exist + assert!(db + .move_channel(a_id, crdb_id, None, Some(crate::db::ChannelId(1000))) .await .is_err()); // Make a link - db.link_channel(a_id, livestreaming_id, zed_id) + db.move_channel(a_id, livestreaming_id, None, Some(zed_id)) .await .unwrap(); @@ -552,7 +570,6 @@ async fn test_channels_moving(db: &Arc) { // /- gpui2 // zed -- crdb - livestreaming - livestreaming_dag // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( dbg!(result.channels), @@ -590,8 +607,14 @@ async fn test_channels_moving(db: &Arc) { ] ); + // Create a new channel below a channel with multiple parents let livestreaming_dag_sub_id = db - .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), "6", a_id) + .create_channel( + "livestreaming_dag_sub", + Some(livestreaming_dag_id), + "6", + a_id, + ) .await .unwrap(); @@ -599,7 +622,6 @@ async fn test_channels_moving(db: &Arc) { // /- gpui2 // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( dbg!(result.channels), @@ -643,7 +665,7 @@ async fn test_channels_moving(db: &Arc) { ); // Make a link - db.link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) + db.move_channel(a_id, livestreaming_dag_sub_id, None, Some(livestreaming_id)) .await .unwrap(); @@ -651,7 +673,6 @@ async fn test_channels_moving(db: &Arc) { // /- gpui2 /---------------------\ // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id // \--------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( dbg!(result.channels), @@ -700,7 +721,7 @@ async fn test_channels_moving(db: &Arc) { ); // Make another link - db.link_channel(a_id, livestreaming_id, gpui2_id) + db.move_channel(a_id, livestreaming_id, None, Some(gpui2_id)) .await .unwrap(); @@ -708,7 +729,123 @@ async fn test_channels_moving(db: &Arc) { // /- gpui2 -\ /---------------------\ // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_id), + }, + 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), + }, + ] + ); + + // Remove that inner link + db.move_channel(a_id, livestreaming_dag_sub_id, Some(livestreaming_id), None) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 -\ + // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_id), + }, + 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_dag_id), + }, + ] + ); + // Remove that outer link + db.move_channel(a_id, livestreaming_id, Some(gpui2_id), None) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 + // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( dbg!(result.channels), @@ -751,6 +888,90 @@ async fn test_channels_moving(db: &Arc) { Channel { id: livestreaming_dag_sub_id, name: "livestreaming_dag_sub".to_string(), + parent_id: Some(livestreaming_dag_id), + }, + ] + ); + + // Move livestreaming to be below gpui2 + db.move_channel(a_id, livestreaming_id, Some(crdb_id), Some(gpui2_id)) + .await + .unwrap(); + + // DAG is now: + // /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // zed - crdb / + // \---------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: gpui2_id, + name: "gpui2".to_string(), + parent_id: Some(zed_id), + }, + 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_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_dag_id), + }, + ] + ); + + // Deleting a channel should not delete children that still have other parents + db.remove_channel(gpui2_id, a_id).await.unwrap(); + + // DAG is now: + // zed - crdb + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + let result = db.get_channels_for_user(a_id).await.unwrap(); + pretty_assertions::assert_eq!( + dbg!(result.channels), + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_dag_id, + name: "livestreaming_dag".to_string(), parent_id: Some(livestreaming_id), }, Channel { @@ -761,41 +982,13 @@ async fn test_channels_moving(db: &Arc) { ] ); - // // Attempt to make a cycle - // assert!(db - // .move_channel(a_id, zed_id, Some(livestreaming_id)) - // .await - // .is_err()); - - // // Move channel up - // db.move_channel(a_id, livestreaming_id, Some(zed_id)) - // .await - // .unwrap(); - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // pretty_assertions::assert_eq!( - // result.channels, - // vec![ - // Channel { - // id: zed_id, - // name: "zed".to_string(), - // parent_id: None, - // }, - // Channel { - // id: crdb_id, - // name: "crdb".to_string(), - // parent_id: Some(zed_id), - // }, - // Channel { - // id: crdb_id, - // name: "crdb".to_string(), - // parent_id: Some(livestreaming_id), - // }, - // Channel { - // id: livestreaming_id, - // name: "livestreaming".to_string(), - // parent_id: Some(zed_id), - // }, - // ] - // ); + // 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(); + // DAG is now: + // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub + // \--------/ + + db.remove_channel(zed_id, a_id).await.unwrap(); + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert!(result.channels.is_empty()) } From bd9e964a696da53e41a034d716a2cc7d68457c88 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 7 Sep 2023 15:16:17 -0700 Subject: [PATCH 04/26] Add removing of previous channel channel, allowing for channel moving operations --- crates/collab/src/db/queries/channels.rs | 162 +++++++++++++------- crates/collab/src/db/tests/channel_tests.rs | 59 +++---- 2 files changed, 129 insertions(+), 92 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0ebd4dd6e1d4a4c90f20c36b3a8d528347f156fd..01f47b194008a03e4e3411eabb8fa811d435e5cb 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -69,7 +69,6 @@ impl Database { ); tx.execute(channel_paths_stmt).await?; - dbg!(channel_path::Entity::find().all(&*tx).await?); } else { channel_path::Entity::insert(channel_path::ActiveModel { channel_id: ActiveValue::Set(channel.id), @@ -338,7 +337,6 @@ impl Database { .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; - dbg!(&parents_by_child_id); let channels_with_admin_privileges = channel_memberships .iter() @@ -601,6 +599,20 @@ impl Database { Ok(channel_ids) } + /// Returns the channel descendants, + /// Structured as a map from child ids to their parent ids + /// For example, the descendants of 'a' in this DAG: + /// + /// /- b -\ + /// a -- c -- d + /// + /// would be: + /// { + /// a: [], + /// b: [a], + /// c: [a], + /// d: [a, c], + /// } async fn get_channel_descendants( &self, channel_ids: impl IntoIterator, @@ -726,28 +738,23 @@ impl Database { .await } - async fn link_channel(&self, user: UserId, from: ChannelId, to: ChannelId) -> Result<()> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(to, user, &*tx).await?; + async fn link_channel( + &self, + from: ChannelId, + to: ChannelId, + tx: &DatabaseTransaction, + ) -> Result<()> { + 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 { + if from_descendants.contains_key(&ancestor) { + return Err(anyhow!("Cannot create a channel cycle").into()); + } + } - // TODO: Downgrade this check once our permissions system isn't busted - // You should be able to safely link a member channel for your own uses. See: - // https://zed.dev/blog/this-week-at-zed-15 > Mikayla's section - // - // Note that even with these higher 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 to_ancestors = self.get_channel_ancestors(to, &*tx).await?; - let from_descendants = self.get_channel_descendants([from], &*tx).await?; - for ancestor in to_ancestors { - if from_descendants.contains_key(&ancestor) { - return Err(anyhow!("Cannot create a channel cycle").into()); - } - } - let sql = r#" + let sql = r#" INSERT INTO channel_paths (id_path, channel_id) SELECT @@ -758,39 +765,61 @@ impl Database { channel_id = $3 ON CONFLICT (id_path) DO NOTHING; "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [ - from.to_proto().into(), - from.to_proto().into(), - to.to_proto().into(), - ], - ); - tx.execute(channel_paths_stmt).await?; - - for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id == &&from) { - for to_id in to_ids { - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [ - from_id.to_proto().into(), - from_id.to_proto().into(), - to_id.to_proto().into(), - ], - ); - tx.execute(channel_paths_stmt).await?; - } + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + from.to_proto().into(), + from.to_proto().into(), + to.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + + for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&from) { + for to_id in to_ids { + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + from_id.to_proto().into(), + from_id.to_proto().into(), + to_id.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; } + } - Ok(()) - }) - .await + + Ok(()) } - async fn remove_channel_from_parent(&self, user: UserId, from: ChannelId, parent: ChannelId) -> Result<()> { - todo!() + async fn remove_channel_from_parent( + &self, + from: ChannelId, + parent: ChannelId, + tx: &DatabaseTransaction, + ) -> Result<()> { + + + let sql = r#" + DELETE FROM channel_paths + WHERE + id_path LIKE '%' || $1 || '/' || $2 || '%' + "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + parent.to_proto().into(), + from.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + + + Ok(()) } /// Move a channel from one parent to another. @@ -798,7 +827,7 @@ impl Database { /// As channels are a DAG, we need to know which parent to remove the channel from. /// Here's a list of the parameters to this function and their behavior: /// - /// - (`None`, `None`) Noop + /// - (`None`, `None`) No op /// - (`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 @@ -809,13 +838,30 @@ impl Database { from_parent: Option, to: Option, ) -> Result<()> { - if let Some(to) = to { - self.link_channel(user, from, to).await?; - } - if let Some(from_parent) = from_parent { - self.remove_channel_from_parent(user, from, from_parent).await?; - } - Ok(()) + 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?; + + if let Some(to) = to { + self.check_user_is_channel_admin(to, user, &*tx).await?; + + self.link_channel(from, to, &*tx).await?; + } + // The removal must come after the linking so that we don't leave + // sub channels stranded + if let Some(from_parent) = from_parent { + self.check_user_is_channel_admin(from_parent, user, &*tx) + .await?; + + self.remove_channel_from_parent(from, from_parent, &*tx) + .await?; + } + + Ok(()) + }) + .await } } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 95405d4358ab18bf8b88897c305231f77cfc567e..ec8f3b56e6518f1fec671180e9083f206d3cdadd 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -537,30 +537,12 @@ async fn test_channels_moving(db: &Arc) { ] ); - // Attemp to make a cycle + // Attempt to make a cycle assert!(db .move_channel(a_id, zed_id, None, Some(livestreaming_id)) .await .is_err()); - // Attemp to remove an edge that doesn't exist - assert!(db - .move_channel(a_id, crdb_id, Some(gpui2_id), None) - .await - .is_err()); - - // Attemp to move to a channel that doesn't exist - assert!(db - .move_channel(a_id, crdb_id, Some(crate::db::ChannelId(1000)), None) - .await - .is_err()); - - // Attemp to remove an edge that doesn't exist - assert!(db - .move_channel(a_id, crdb_id, None, Some(crate::db::ChannelId(1000))) - .await - .is_err()); - // Make a link db.move_channel(a_id, livestreaming_id, None, Some(zed_id)) .await @@ -572,7 +554,7 @@ async fn test_channels_moving(db: &Arc) { // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -624,7 +606,7 @@ async fn test_channels_moving(db: &Arc) { // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -675,7 +657,7 @@ async fn test_channels_moving(db: &Arc) { // \--------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -731,7 +713,7 @@ async fn test_channels_moving(db: &Arc) { // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -792,7 +774,7 @@ async fn test_channels_moving(db: &Arc) { // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -842,13 +824,27 @@ async fn test_channels_moving(db: &Arc) { .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!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -865,11 +861,6 @@ async fn test_channels_moving(db: &Arc) { name: "gpui2".to_string(), parent_id: Some(zed_id), }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(gpui2_id), - }, Channel { id: livestreaming_id, name: "livestreaming".to_string(), @@ -904,7 +895,7 @@ async fn test_channels_moving(db: &Arc) { // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, @@ -924,12 +915,12 @@ async fn test_channels_moving(db: &Arc) { Channel { id: livestreaming_id, name: "livestreaming".to_string(), - parent_id: Some(gpui2_id), + parent_id: Some(zed_id), }, Channel { id: livestreaming_id, name: "livestreaming".to_string(), - parent_id: Some(zed_id), + parent_id: Some(gpui2_id), }, Channel { id: livestreaming_dag_id, @@ -952,7 +943,7 @@ async fn test_channels_moving(db: &Arc) { // \- livestreaming - livestreaming_dag - livestreaming_dag_sub let result = db.get_channels_for_user(a_id).await.unwrap(); pretty_assertions::assert_eq!( - dbg!(result.channels), + result.channels, vec![ Channel { id: zed_id, From 49fbb27ce943fc3c99ca360da3cd079883cd4488 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 7 Sep 2023 15:23:37 -0700 Subject: [PATCH 05/26] Improve channel deletion to be DAG aware --- crates/collab/src/db/queries/channels.rs | 28 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 01f47b194008a03e4e3411eabb8fa811d435e5cb..0eb6c45fe6232b0e8adedee0211bc8d48106025e 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -150,6 +150,21 @@ impl Database { .exec(&*tx) .await?; + // Delete any other paths that incldue this channel + let sql = r#" + DELETE FROM channel_paths + WHERE + id_path LIKE '%' || $1 || '%' + "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + channel_id.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + Ok((channels_to_remove.into_keys().collect(), members_to_notify)) }) .await @@ -844,13 +859,6 @@ impl Database { // channel if they've linked the channel to one where they're an admin. self.check_user_is_channel_admin(from, user, &*tx).await?; - if let Some(to) = to { - self.check_user_is_channel_admin(to, user, &*tx).await?; - - self.link_channel(from, to, &*tx).await?; - } - // The removal must come after the linking so that we don't leave - // sub channels stranded if let Some(from_parent) = from_parent { self.check_user_is_channel_admin(from_parent, user, &*tx) .await?; @@ -859,6 +867,12 @@ impl Database { .await?; } + if let Some(to) = to { + self.check_user_is_channel_admin(to, user, &*tx).await?; + + self.link_channel(from, to, &*tx).await?; + } + Ok(()) }) .await From 9e68d4a8eaaf485b6edb09b98ee8a50bc492f992 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 8 Sep 2023 11:38:00 -0700 Subject: [PATCH 06/26] WIP: Add channel DAG related RPC messages, change update message --- 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(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index e61e520b471029e692ecdc29bac58777917d92ba..ec4267af860fbcb620e1be1e177583ea5c7f6fe4 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -323,6 +323,18 @@ impl ChannelStore { }) } + + pub fn move_channel(&mut self, channel_id: ChannelId, from_parent: Option, to: Option, cx: &mut ModelContext) -> Task> { + 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> { 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) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 22174f161b4139033b15d004b42ad3388b02fb26..1d3694866fcc0094112ae360ebbf163ef7cdefa8 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/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, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0eb6c45fe6232b0e8adedee0211bc8d48106025e..fa1e28546a0dfe5db05fe9e37b1f32059f86ba0f 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,5 +1,7 @@ use super::*; +type ChannelDescendants = HashMap>; + impl Database { #[cfg(test)] pub async fn all_channels(&self) -> Result> { @@ -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> { + 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 { 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, tx: &DatabaseTransaction, - ) -> Result>> { + ) -> Result { 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> = 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 { 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, to: Option, - ) -> Result<()> { + ) -> Result> { 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 } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index ec8f3b56e6518f1fec671180e9083f206d3cdadd..be0b0f20e671e5b5a1a802d0cca56e96974a320e 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -181,11 +181,11 @@ async fn test_channels(db: &Arc) { ); // 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) { ); // 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) { // /- 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) { ); // 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) { // /- 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) { ); // 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) { // /- 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) { .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) { ); // 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) { ); // 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()) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 3289daf6ca3feb4a75ada7f60ca27f56fbd80528..068a69fde161b3ceee558f6bc5b4d6f750557eb0 100644 --- a/crates/collab/src/rpc.rs +++ b/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, +async fn delete_channel( + request: proto::DeleteChannel, + response: Response, 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, + 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, diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index b54b4d349ba54e5c23e048cd81b292a10566445d..77045d91743482989b25cf65647d94516c257c0d 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/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, 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::>() }); - assert_eq!(actual, expected_channels); + pretty_assertions::assert_eq!(actual, expected_channels); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 855588a2d8507d9e0e52af3326a798e400568b08..f252efaa14c4f19365a19edf52e649016ff9bc6e 100644 --- a/crates/rpc/proto/zed.proto +++ b/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; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 240daed1b2c3c53d5a7ca4934f4037c9f7657f32..121f49b9660c8a882c822fd9ae93b9e66697b04a 100644 --- a/crates/rpc/src/proto.rs +++ b/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), From 3a62d2988a39f9ecc4c41055cffe3e4ee7152b91 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 8 Sep 2023 15:58:08 -0700 Subject: [PATCH 07/26] Finish integration tests for channel moving Refactor channel store to combine the channels_by_id and channel_paths into a 'ChannelIndex' --- crates/channel/src/channel_store.rs | 96 ++++------- .../src/channel_store/channel_index.rs | 151 ++++++++++++++++++ crates/collab/src/db/queries/channels.rs | 42 ++--- crates/collab/src/db/tests/channel_tests.rs | 29 +--- crates/collab/src/rpc.rs | 4 +- 5 files changed, 214 insertions(+), 108 deletions(-) create mode 100644 crates/channel/src/channel_store/channel_index.rs diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index ec4267af860fbcb620e1be1e177583ea5c7f6fe4..4415a10625a301c9ab0aee5c6b98f800235f6861 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,3 +1,5 @@ +mod channel_index; + use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; @@ -8,13 +10,14 @@ use rpc::{proto, TypedEnvelope}; use std::{mem, sync::Arc, time::Duration}; use util::ResultExt; +use self::channel_index::ChannelIndex; + pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub type ChannelId = u64; pub struct ChannelStore { - channels_by_id: HashMap>, - channel_paths: Vec>, + channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, channels_with_admin_privileges: HashSet, @@ -82,9 +85,8 @@ impl ChannelStore { }); Self { - channels_by_id: HashMap::default(), channel_invitations: Vec::default(), - channel_paths: Vec::default(), + channel_index: ChannelIndex::default(), channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), @@ -116,7 +118,7 @@ impl ChannelStore { } pub fn has_children(&self, channel_id: ChannelId) -> bool { - self.channel_paths.iter().any(|path| { + self.channel_index.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { path.len() > ix + 1 } else { @@ -126,7 +128,7 @@ impl ChannelStore { } pub fn channel_count(&self) -> usize { - self.channel_paths.len() + self.channel_index.len() } pub fn index_of_channel(&self, channel_id: ChannelId) -> Option { @@ -136,7 +138,7 @@ impl ChannelStore { } pub fn channels(&self) -> impl '_ + Iterator)> { - self.channel_paths.iter().map(move |path| { + self.channel_index.iter().map(move |path| { let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); (path.len() - 1, channel) @@ -144,7 +146,7 @@ impl ChannelStore { } pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc)> { - let path = self.channel_paths.get(ix)?; + let path = self.channel_index.get(ix)?; let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); Some((path.len() - 1, channel)) @@ -155,7 +157,7 @@ impl ChannelStore { } pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> { - self.channels_by_id.get(&channel_id) + self.channel_index.by_id().get(&channel_id) } pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool { @@ -268,7 +270,7 @@ impl ChannelStore { } pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { - self.channel_paths.iter().any(|path| { + self.channel_index.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { path[..=ix] .iter() @@ -323,15 +325,24 @@ impl ChannelStore { }) } - - pub fn move_channel(&mut self, channel_id: ChannelId, from_parent: Option, to: Option, cx: &mut ModelContext) -> Task> { + pub fn move_channel( + &mut self, + channel_id: ChannelId, + from_parent: Option, + to: Option, + cx: &mut ModelContext, + ) -> Task> { let client = self.client.clone(); cx.spawn(|_, _| async move { let _ = client - .request(proto::MoveChannel { channel_id, from_parent, to }) + .request(proto::MoveChannel { + channel_id, + from_parent, + to, + }) .await?; - Ok(()) + Ok(()) }) } @@ -651,11 +662,11 @@ impl ChannelStore { } fn handle_disconnect(&mut self, cx: &mut ModelContext) { - self.channels_by_id.clear(); + self.channel_index.clear(); self.channel_invitations.clear(); self.channel_participants.clear(); self.channels_with_admin_privileges.clear(); - self.channel_paths.clear(); + self.channel_index.clear(); self.outgoing_invites.clear(); cx.notify(); @@ -705,8 +716,7 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { - self.channels_by_id - .retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); + self.channel_index.delete_channels(&payload.delete_channels); self.channel_participants .retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); self.channels_with_admin_privileges @@ -724,44 +734,12 @@ impl ChannelStore { } } - for channel_proto in payload.channels { - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { - Arc::make_mut(existing_channel).name = channel_proto.name; - } else { - let channel = Arc::new(Channel { - id: channel_proto.id, - name: channel_proto.name, - }); - self.channels_by_id.insert(channel.id, channel.clone()); - - if let Some(parent_id) = channel_proto.parent_id { - let mut ix = 0; - while ix < self.channel_paths.len() { - let path = &self.channel_paths[ix]; - if path.ends_with(&[parent_id]) { - let mut new_path = path.clone(); - new_path.push(channel.id); - self.channel_paths.insert(ix + 1, new_path); - ix += 1; - } - ix += 1; - } - } else { - self.channel_paths.push(vec![channel.id]); - } - } - } + self.channel_index.insert_channels(payload.channels); + } - self.channel_paths.sort_by(|a, b| { - let a = Self::channel_path_sorting_key(a, &self.channels_by_id); - let b = Self::channel_path_sorting_key(b, &self.channels_by_id); - a.cmp(b) - }); - self.channel_paths.dedup(); - self.channel_paths.retain(|path| { - path.iter() - .all(|channel_id| self.channels_by_id.contains_key(channel_id)) - }); + for edge in payload.delete_channel_edge { + self.channel_index + .remove_edge(edge.parent_id, edge.channel_id); } for permission in payload.channel_permissions { @@ -820,11 +798,5 @@ impl ChannelStore { })) } - fn channel_path_sorting_key<'a>( - path: &'a [ChannelId], - channels_by_id: &'a HashMap>, - ) -> impl 'a + Iterator> { - path.iter() - .map(|id| Some(channels_by_id.get(id)?.name.as_str())) - } + } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9398d099c6dcd6dff429217571b561bb99bf33e --- /dev/null +++ b/crates/channel/src/channel_store/channel_index.rs @@ -0,0 +1,151 @@ +use std::{ops::{Deref, DerefMut}, sync::Arc}; + +use collections::HashMap; +use rpc::proto; + +use crate::{ChannelId, Channel}; + +pub type ChannelPath = Vec; +pub type ChannelsById = HashMap>; + +#[derive(Default, Debug)] +pub struct ChannelIndex { + paths: Vec, + channels_by_id: ChannelsById, +} + + +impl ChannelIndex { + pub fn by_id(&self) -> &ChannelsById { + &self.channels_by_id + } + + /// Insert or update all of the given channels into the index + pub fn insert_channels(&mut self, channels: Vec) { + let mut insert = self.insert(); + + for channel_proto in channels { + if let Some(existing_channel) = insert.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; + + if let Some(parent_id) = channel_proto.parent_id { + insert.insert_edge(parent_id, channel_proto.id) + } + } else { + let channel = Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }); + insert.channels_by_id.insert(channel.id, channel.clone()); + + if let Some(parent_id) = channel_proto.parent_id { + insert.insert_edge(parent_id, channel.id); + } else { + insert.insert_root(channel.id); + } + } + } + } + + pub fn clear(&mut self) { + self.paths.clear(); + self.channels_by_id.clear(); + } + + /// Remove the given edge from this index. This will not remove the channel + /// and may result in dangling channels. + pub fn remove_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + self.paths.retain(|path| { + !path + .windows(2) + .any(|window| window == [parent_id, channel_id]) + }); + } + + /// Delete the given channels from this index. + pub fn delete_channels(&mut self, channels: &[ChannelId]) { + self.channels_by_id.retain(|channel_id, _| !channels.contains(channel_id)); + self.paths.retain(|channel_path| !channel_path.iter().any(|channel_id| {channels.contains(channel_id)})) + } + + fn insert(& mut self) -> ChannelPathsInsertGuard { + ChannelPathsInsertGuard { + paths: &mut self.paths, + channels_by_id: &mut self.channels_by_id, + } + } +} + +impl Deref for ChannelIndex { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.paths + } +} + +/// A guard for ensuring that the paths index maintains its sort and uniqueness +/// invariants after a series of insertions +struct ChannelPathsInsertGuard<'a> { + paths: &'a mut Vec, + channels_by_id: &'a mut ChannelsById, +} + +impl Deref for ChannelPathsInsertGuard<'_> { + type Target = ChannelsById; + + fn deref(&self) -> &Self::Target { + &self.channels_by_id + } +} + +impl DerefMut for ChannelPathsInsertGuard<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.channels_by_id + } +} + + +impl<'a> ChannelPathsInsertGuard<'a> { + pub fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + let mut ix = 0; + while ix < self.paths.len() { + let path = &self.paths[ix]; + if path.ends_with(&[parent_id]) { + let mut new_path = path.clone(); + new_path.push(channel_id); + self.paths.insert(ix + 1, new_path); + ix += 1; + } + ix += 1; + } + } + + pub fn insert_root(&mut self, channel_id: ChannelId) { + self.paths.push(vec![channel_id]); + } +} + +impl<'a> Drop for ChannelPathsInsertGuard<'a> { + fn drop(&mut self) { + self.paths.sort_by(|a, b| { + let a = channel_path_sorting_key(a, &self.channels_by_id); + let b = channel_path_sorting_key(b, &self.channels_by_id); + a.cmp(b) + }); + self.paths.dedup(); + self.paths.retain(|path| { + path.iter() + .all(|channel_id| self.channels_by_id.contains_key(channel_id)) + }); + } +} + + +fn channel_path_sorting_key<'a>( + path: &'a [ChannelId], + channels_by_id: &'a ChannelsById, +) -> impl 'a + Iterator> { + path.iter() + .map(|id| Some(channels_by_id.get(id)?.name.as_str())) +} diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index fa1e28546a0dfe5db05fe9e37b1f32059f86ba0f..d347be6c4f1a424f71c4c628cef4c43c9772a725 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -846,7 +846,8 @@ impl Database { /// - (`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 + /// Returns the channel that was moved + it's sub channels for use + /// by the members for `to` pub async fn move_channel( &self, user: UserId, @@ -861,14 +862,9 @@ impl Database { 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?; - - self.remove_channel_from_parent(from, from_parent, &*tx) - .await?; - } + // Note that we have to do the linking before the removal, so that we + // can leave the channel_path table in a consistent state. if let Some(to) = to { self.check_user_is_channel_admin(to, user, &*tx).await?; @@ -880,20 +876,28 @@ impl Database { 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); - } + if let Some(from_parent) = from_parent { + self.check_user_is_channel_admin(from_parent, user, &*tx) + .await?; + + self.remove_channel_from_parent(from, from_parent, &*tx) + .await?; } + let channels; + if let Some(to) = to { + if let Some(channel) = channel_descendants.get_mut(&from) { + // Remove the other parents + channel.clear(); + channel.insert(to); + } - let channels = self - .get_all_channels(channel_descendants, &*tx) - .await?; + channels = self + .get_all_channels(channel_descendants, &*tx) + .await?; + } else { + channels = vec![]; + } Ok(channels) }) diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index be0b0f20e671e5b5a1a802d0cca56e96974a320e..7f159ea0bd5e7154858dd80bbd940fd544795d0c 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -657,7 +657,7 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id // \--------/ - // make sure we're getting the new link + // make sure we're getting just the new link pretty_assertions::assert_eq!( channels, vec![ @@ -665,12 +665,7 @@ async fn test_channels_moving(db: &Arc) { 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), - }, + } ] ); @@ -738,16 +733,6 @@ async fn test_channels_moving(db: &Arc) { 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(), @@ -826,16 +811,10 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub // \---------/ - // Make sure the recently removed link isn't returned + // Since we're not moving it to anywhere, there's nothing to notify anyone about pretty_assertions::assert_eq!( channels, - vec![ - Channel { - id: livestreaming_dag_sub_id, - name: "livestreaming_dag_sub".to_string(), - parent_id: Some(livestreaming_dag_id), - }, - ] + vec![] ); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 068a69fde161b3ceee558f6bc5b4d6f750557eb0..d15bc704eff90b639ca77c354846ea12b051e152 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2400,7 +2400,7 @@ async fn move_channel( 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 + let channels_to_send = db .move_channel( session.user_id, channel_id, @@ -2432,7 +2432,7 @@ async fn move_channel( 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 { + channels: channels_to_send.into_iter().map(|channel| proto::Channel { id: channel.id.to_proto(), name: channel.name, parent_id: channel.parent_id.map(ChannelId::to_proto), From 8222102d0165773d543dc3ba643158db93e4f1c1 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 8 Sep 2023 18:47:59 -0700 Subject: [PATCH 08/26] Render the DAG --- crates/channel/src/channel_store.rs | 14 +- .../src/channel_store/channel_index.rs | 85 +++--- crates/collab_ui/src/collab_panel.rs | 250 +++++++++++++----- crates/rpc/src/proto.rs | 1 - 4 files changed, 227 insertions(+), 123 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 4415a10625a301c9ab0aee5c6b98f800235f6861..2aeff3afef871a5189b648af4bb9f039ae4a75a4 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -11,6 +11,7 @@ use std::{mem, sync::Arc, time::Duration}; use util::ResultExt; use self::channel_index::ChannelIndex; +pub use self::channel_index::ChannelPath; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -145,11 +146,13 @@ impl ChannelStore { }) } - pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc)> { + pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc, &Arc<[ChannelId]>)> { let path = self.channel_index.get(ix)?; let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); - Some((path.len() - 1, channel)) + + + Some((path.len() - 1, channel, path)) } pub fn channel_invitations(&self) -> &[Arc] { @@ -734,12 +737,15 @@ impl ChannelStore { } } - self.channel_index.insert_channels(payload.channels); + let mut channel_index = self.channel_index.start_upsert(); + for channel in payload.channels { + channel_index.upsert(channel) + } } for edge in payload.delete_channel_edge { self.channel_index - .remove_edge(edge.parent_id, edge.channel_id); + .delete_edge(edge.parent_id, edge.channel_id); } for permission in payload.channel_permissions { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index b9398d099c6dcd6dff429217571b561bb99bf33e..b52d7ba334eb442e881925f955fd157664ede06e 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,11 +1,11 @@ -use std::{ops::{Deref, DerefMut}, sync::Arc}; +use std::{ops::Deref, sync::Arc}; use collections::HashMap; use rpc::proto; use crate::{ChannelId, Channel}; -pub type ChannelPath = Vec; +pub type ChannelPath = Arc<[ChannelId]>; pub type ChannelsById = HashMap>; #[derive(Default, Debug)] @@ -20,33 +20,6 @@ impl ChannelIndex { &self.channels_by_id } - /// Insert or update all of the given channels into the index - pub fn insert_channels(&mut self, channels: Vec) { - let mut insert = self.insert(); - - for channel_proto in channels { - if let Some(existing_channel) = insert.channels_by_id.get_mut(&channel_proto.id) { - Arc::make_mut(existing_channel).name = channel_proto.name; - - if let Some(parent_id) = channel_proto.parent_id { - insert.insert_edge(parent_id, channel_proto.id) - } - } else { - let channel = Arc::new(Channel { - id: channel_proto.id, - name: channel_proto.name, - }); - insert.channels_by_id.insert(channel.id, channel.clone()); - - if let Some(parent_id) = channel_proto.parent_id { - insert.insert_edge(parent_id, channel.id); - } else { - insert.insert_root(channel.id); - } - } - } - } - pub fn clear(&mut self) { self.paths.clear(); self.channels_by_id.clear(); @@ -54,7 +27,7 @@ impl ChannelIndex { /// Remove the given edge from this index. This will not remove the channel /// and may result in dangling channels. - pub fn remove_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { self.paths.retain(|path| { !path .windows(2) @@ -68,8 +41,9 @@ impl ChannelIndex { self.paths.retain(|channel_path| !channel_path.iter().any(|channel_id| {channels.contains(channel_id)})) } - fn insert(& mut self) -> ChannelPathsInsertGuard { - ChannelPathsInsertGuard { + /// Upsert one or more channels into this index. + pub fn start_upsert(& mut self) -> ChannelPathsUpsertGuard { + ChannelPathsUpsertGuard { paths: &mut self.paths, channels_by_id: &mut self.channels_by_id, } @@ -86,47 +60,54 @@ impl Deref for ChannelIndex { /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions -struct ChannelPathsInsertGuard<'a> { +pub struct ChannelPathsUpsertGuard<'a> { paths: &'a mut Vec, channels_by_id: &'a mut ChannelsById, } -impl Deref for ChannelPathsInsertGuard<'_> { - type Target = ChannelsById; - - fn deref(&self) -> &Self::Target { - &self.channels_by_id - } -} +impl<'a> ChannelPathsUpsertGuard<'a> { + pub fn upsert(&mut self, channel_proto: proto::Channel) { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; -impl DerefMut for ChannelPathsInsertGuard<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.channels_by_id + if let Some(parent_id) = channel_proto.parent_id { + self.insert_edge(parent_id, channel_proto.id) + } + } else { + let channel = Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }); + self.channels_by_id.insert(channel.id, channel.clone()); + + if let Some(parent_id) = channel_proto.parent_id { + self.insert_edge(parent_id, channel.id); + } else { + self.insert_root(channel.id); + } + } } -} - -impl<'a> ChannelPathsInsertGuard<'a> { - pub fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { let mut ix = 0; while ix < self.paths.len() { let path = &self.paths[ix]; if path.ends_with(&[parent_id]) { - let mut new_path = path.clone(); + let mut new_path = path.to_vec(); new_path.push(channel_id); - self.paths.insert(ix + 1, new_path); + self.paths.insert(ix + 1, new_path.into()); ix += 1; } ix += 1; } } - pub fn insert_root(&mut self, channel_id: ChannelId) { - self.paths.push(vec![channel_id]); + fn insert_root(&mut self, channel_id: ChannelId) { + self.paths.push(Arc::from([channel_id])); } } -impl<'a> Drop for ChannelPathsInsertGuard<'a> { +impl<'a> Drop for ChannelPathsUpsertGuard<'a> { fn drop(&mut self) { self.paths.sort_by(|a, b| { let a = channel_path_sorting_key(a, &self.channels_by_id); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a258151bb805387b4614ff5886f5875235ede2d0..c9d5d97305ba1c1f82efa5efc95e42d75ccf2643 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Result; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath}; use channel_modal::ChannelModal; use client::{proto::PeerId, Client, Contact, User, UserStore}; use contact_finder::ContactFinder; @@ -40,7 +40,7 @@ use menu::{Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use std::{borrow::Cow, mem, sync::Arc}; +use std::{borrow::Cow, mem, sync::Arc, hash::Hash}; use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ @@ -51,32 +51,32 @@ use workspace::{ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { - channel_id: u64, + channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct ToggleCollapse { - channel_id: u64, + location: ChannelLocation<'static>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { - channel_id: u64, + location: ChannelLocation<'static>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct InviteMembers { - channel_id: u64, + channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct ManageMembers { - channel_id: u64, + channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RenameChannel { - channel_id: u64, + location: ChannelLocation<'static>, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -89,6 +89,26 @@ pub struct JoinChannelCall { pub channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct OpenChannelBuffer { + channel_id: ChannelId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct CopyChannel { + channel_id: ChannelId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct CutChannel { + channel_id: ChannelId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct PasteChannel { + channel_id: ChannelId, +} + actions!( collab_panel, [ @@ -111,12 +131,35 @@ impl_actions!( ToggleCollapse, OpenChannelNotes, JoinChannelCall, + OpenChannelBuffer, + CopyChannel, + CutChannel, + PasteChannel, ] ); const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ChannelLocation<'a> { + channel: ChannelId, + parent: Cow<'a, ChannelPath>, +} + +impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> { + fn from(value: (ChannelId, ChannelPath)) -> Self { + ChannelLocation { channel: value.0, parent: Cow::Owned(value.1) } + } +} + +impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> { + fn from(value: (ChannelId, &'a ChannelPath)) -> Self { + ChannelLocation { channel: value.0, parent: Cow::Borrowed(value.1) } + } +} + pub fn init(cx: &mut AppContext) { + settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); channel_view::init(cx); @@ -137,16 +180,37 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::collapse_selected_channel); cx.add_action(CollabPanel::expand_selected_channel); cx.add_action(CollabPanel::open_channel_notes); + cx.add_action(CollabPanel::open_channel_buffer); + + cx.add_action(|panel: &mut CollabPanel, action: &CopyChannel, _: &mut ViewContext| { + panel.copy = Some(ChannelCopy::Copy(action.channel_id)); + }); + + cx.add_action(|panel: &mut CollabPanel, action: &CutChannel, _: &mut ViewContext| { + // panel.copy = Some(ChannelCopy::Cut(action.channel_id)); + }); + + cx.add_action(|panel: &mut CollabPanel, action: &PasteChannel, cx: &mut ViewContext| { + if let Some(copy) = &panel.copy { + match copy { + ChannelCopy::Cut {..} => todo!(), + ChannelCopy::Copy(channel) => panel.channel_store.update(cx, |channel_store, cx| { + channel_store.move_channel(*channel, None, Some(action.channel_id), cx).detach_and_log_err(cx) + }), + } + } + }); + } #[derive(Debug)] pub enum ChannelEditingState { Create { - parent_id: Option, + location: Option>, pending_name: Option, }, Rename { - channel_id: u64, + location: ChannelLocation<'static>, pending_name: Option, }, } @@ -160,10 +224,19 @@ impl ChannelEditingState { } } +enum ChannelCopy { + Cut { + channel_id: u64, + parent_id: Option, + }, + Copy(u64), +} + pub struct CollabPanel { width: Option, fs: Arc, has_focus: bool, + copy: Option, pending_serialization: Task>, context_menu: ViewHandle, filter_editor: ViewHandle, @@ -179,7 +252,7 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, - collapsed_channels: Vec, + collapsed_channels: Vec>, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -187,7 +260,7 @@ pub struct CollabPanel { #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, - collapsed_channels: Option>, + collapsed_channels: Option>>, } #[derive(Debug)] @@ -231,6 +304,7 @@ enum ListEntry { Channel { channel: Arc, depth: usize, + path: Arc<[ChannelId]>, }, ChannelNotes { channel_id: ChannelId, @@ -348,10 +422,11 @@ impl CollabPanel { cx, ) } - ListEntry::Channel { channel, depth } => { + ListEntry::Channel { channel, depth, path } => { let channel_row = this.render_channel( &*channel, *depth, + path.to_owned(), &theme.collab_panel, is_selected, cx, @@ -420,6 +495,7 @@ impl CollabPanel { let mut this = Self { width: None, has_focus: false, + copy: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), @@ -700,7 +776,7 @@ impl CollabPanel { if matches!( state, ChannelEditingState::Create { - parent_id: None, + location: None, .. } ) { @@ -709,16 +785,18 @@ impl CollabPanel { } let mut collapse_depth = None; for mat in matches { - let (depth, channel) = + let (depth, channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap(); - if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + let location: ChannelLocation<'_> = (channel.id, path).into(); + + if collapse_depth.is_none() && self.is_channel_collapsed(&location) { collapse_depth = Some(depth); } else if let Some(collapsed_depth) = collapse_depth { if depth > collapsed_depth { continue; } - if self.is_channel_collapsed(channel.id) { + if self.is_channel_collapsed(&location) { collapse_depth = Some(depth); } else { collapse_depth = None; @@ -726,18 +804,19 @@ impl CollabPanel { } match &self.channel_editing_state { - Some(ChannelEditingState::Create { parent_id, .. }) - if *parent_id == Some(channel.id) => + Some(ChannelEditingState::Create { location: parent_id, .. }) + if *parent_id == Some(location) => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, + path: path.clone(), }); self.entries .push(ListEntry::ChannelEditor { depth: depth + 1 }); } - Some(ChannelEditingState::Rename { channel_id, .. }) - if *channel_id == channel.id => + Some(ChannelEditingState::Rename { location, .. }) + if location.channel == channel.id && location.parent == Cow::Borrowed(path) => { self.entries.push(ListEntry::ChannelEditor { depth }); } @@ -745,6 +824,7 @@ impl CollabPanel { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, + path: path.clone() }); } } @@ -1546,14 +1626,21 @@ impl CollabPanel { &self, channel: &Channel, depth: usize, + path: ChannelPath, theme: &theme::CollabPanel, is_selected: bool, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; let has_children = self.channel_store.read(cx).has_children(channel_id); - let disclosed = - has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + + let disclosed = { + let location = ChannelLocation { + channel: channel_id, + parent: Cow::Borrowed(&path), + }; + has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok()) + }; let is_active = iife!({ let call_channel = ActiveCall::global(cx) @@ -1569,7 +1656,7 @@ impl CollabPanel { enum ChannelCall {} - MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { + MouseEventHandler::new::(id(&path) as usize, cx, |state, cx| { let row_hovered = state.hovered(); Flex::::row() @@ -1637,8 +1724,8 @@ impl CollabPanel { ) .align_children_center() .styleable_component() - .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) - .with_id(channel_id as usize) + .disclosable(disclosed, Box::new(ToggleCollapse { location: (channel_id, path.clone()).into() })) + .with_id(id(&path) as usize) .with_style(theme.disclosure.clone()) .element() .constrained() @@ -1654,7 +1741,7 @@ impl CollabPanel { this.join_channel_chat(channel_id, cx); }) .on_click(MouseButton::Right, move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), channel_id, cx); + this.deploy_channel_context_menu(Some(e.position), &(channel_id, path.clone()).into(), cx); }) .with_cursor_style(CursorStyle::PointingHand) .into_any() @@ -1901,7 +1988,7 @@ impl CollabPanel { fn deploy_channel_context_menu( &mut self, position: Option, - channel_id: u64, + location: &ChannelLocation<'static>, cx: &mut ViewContext, ) { self.context_menu_on_selected = position.is_none(); @@ -1913,27 +2000,29 @@ impl CollabPanel { OverlayPositionMode::Window }); - let expand_action_name = if self.is_channel_collapsed(channel_id) { + let expand_action_name = if self.is_channel_collapsed(&location) { "Expand Subchannels" } else { "Collapse Subchannels" }; let mut items = vec![ - ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), - ContextMenuItem::action("Open Notes", OpenChannelNotes { channel_id }), + ContextMenuItem::action(expand_action_name, ToggleCollapse { location: location.clone() }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: location.channel }), ]; - if self.channel_store.read(cx).is_user_admin(channel_id) { + if self.channel_store.read(cx).is_user_admin(location.channel) { items.extend([ ContextMenuItem::Separator, - ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), - ContextMenuItem::action("Rename", RenameChannel { channel_id }), + ContextMenuItem::action("New Subchannel", NewChannel { location: location.clone() }), + ContextMenuItem::action("Rename", RenameChannel { location: location.clone() }), + ContextMenuItem::action("Copy", CopyChannel { channel_id: location.channel }), + ContextMenuItem::action("Paste", PasteChannel { channel_id: location.channel }), ContextMenuItem::Separator, - ContextMenuItem::action("Invite Members", InviteMembers { channel_id }), - ContextMenuItem::action("Manage Members", ManageMembers { channel_id }), + ContextMenuItem::action("Invite Members", InviteMembers { channel_id: location.channel }), + ContextMenuItem::action("Manage Members", ManageMembers { channel_id: location.channel }), ContextMenuItem::Separator, - ContextMenuItem::action("Delete", RemoveChannel { channel_id }), + ContextMenuItem::action("Delete", RemoveChannel { channel_id: location.channel }), ]); } @@ -2059,7 +2148,7 @@ impl CollabPanel { if let Some(editing_state) = &mut self.channel_editing_state { match editing_state { ChannelEditingState::Create { - parent_id, + location, pending_name, .. } => { @@ -2072,13 +2161,13 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.create_channel(&channel_name, *parent_id, cx) + channel_store.create_channel(&channel_name, location.as_ref().map(|location| location.channel), cx) }) .detach(); cx.notify(); } ChannelEditingState::Rename { - channel_id, + location, pending_name, } => { if pending_name.is_some() { @@ -2089,7 +2178,7 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.rename(*channel_id, &channel_name, cx) + channel_store.rename(location.channel, &channel_name, cx) }) .detach(); cx.notify(); @@ -2116,38 +2205,42 @@ impl CollabPanel { _: &CollapseSelectedChannel, cx: &mut ViewContext, ) { - let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else { return; }; - if self.is_channel_collapsed(channel_id) { + let path = path.to_owned(); + + if self.is_channel_collapsed(&(channel_id, path.clone()).into()) { return; } - self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx) } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else { return; }; - if !self.is_channel_collapsed(channel_id) { + let path = path.to_owned(); + + if !self.is_channel_collapsed(&(channel_id, path.clone()).into()) { return; } - self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx) } fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { - let channel_id = action.channel_id; + let location = action.location.clone(); - match self.collapsed_channels.binary_search(&channel_id) { + match self.collapsed_channels.binary_search(&location) { Ok(ix) => { self.collapsed_channels.remove(ix); } Err(ix) => { - self.collapsed_channels.insert(ix, channel_id); + self.collapsed_channels.insert(ix, location); } }; self.serialize(cx); @@ -2156,8 +2249,8 @@ impl CollabPanel { cx.focus_self(); } - fn is_channel_collapsed(&self, channel: ChannelId) -> bool { - self.collapsed_channels.binary_search(&channel).is_ok() + fn is_channel_collapsed(&self, location: &ChannelLocation) -> bool { + self.collapsed_channels.binary_search(location).is_ok() } fn leave_call(cx: &mut ViewContext) { @@ -2182,7 +2275,7 @@ impl CollabPanel { fn new_root_channel(&mut self, cx: &mut ViewContext) { self.channel_editing_state = Some(ChannelEditingState::Create { - parent_id: None, + location: None, pending_name: None, }); self.update_entries(false, cx); @@ -2200,9 +2293,9 @@ impl CollabPanel { fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { self.collapsed_channels - .retain(|&channel| channel != action.channel_id); + .retain(|channel| *channel != action.location); self.channel_editing_state = Some(ChannelEditingState::Create { - parent_id: Some(action.channel_id), + location: Some(action.location.to_owned()), pending_name: None, }); self.update_entries(false, cx); @@ -2220,16 +2313,16 @@ impl CollabPanel { } fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - if let Some(channel) = self.selected_channel() { + if let Some((channel, _)) = self.selected_channel() { self.remove_channel(channel.id, cx) } } fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - if let Some(channel) = self.selected_channel() { + if let Some((channel, parent)) = self.selected_channel() { self.rename_channel( &RenameChannel { - channel_id: channel.id, + location: (channel.id, parent.to_owned()).into(), }, cx, ); @@ -2238,12 +2331,12 @@ impl CollabPanel { fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); - if !channel_store.is_user_admin(action.channel_id) { + if !channel_store.is_user_admin(action.location.channel) { return; } - if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { + if let Some(channel) = channel_store.channel_for_id(action.location.channel).cloned() { self.channel_editing_state = Some(ChannelEditingState::Rename { - channel_id: action.channel_id, + location: action.location.to_owned(), pending_name: None, }); self.channel_name_editor.update(cx, |editor, cx| { @@ -2263,18 +2356,18 @@ impl CollabPanel { } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { - let Some(channel) = self.selected_channel() else { + let Some((channel, path)) = self.selected_channel() else { return; }; - self.deploy_channel_context_menu(None, channel.id, cx); + self.deploy_channel_context_menu(None, &(channel.id, path.to_owned()).into(), cx); } - fn selected_channel(&self) -> Option<&Arc> { + fn selected_channel(&self) -> Option<(&Arc, &ChannelPath)> { self.selection .and_then(|ix| self.entries.get(ix)) .and_then(|entry| match entry { - ListEntry::Channel { channel, .. } => Some(channel), + ListEntry::Channel { channel, path: parent, .. } => Some((channel, parent)), _ => None, }) } @@ -2657,13 +2750,15 @@ impl PartialEq for ListEntry { ListEntry::Channel { channel: channel_1, depth: depth_1, + path: parent_1, } => { if let ListEntry::Channel { channel: channel_2, depth: depth_2, + path: parent_2, } = other { - return channel_1.id == channel_2.id && depth_1 == depth_2; + return channel_1.id == channel_2.id && depth_1 == depth_2 && parent_1 == parent_2; } } ListEntry::ChannelNotes { channel_id } => { @@ -2726,3 +2821,26 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } + +/// Hash a channel path to a u64, for use as a mouse id +/// Based on the Fowler–Noll–Vo hash: +/// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function +fn id(path: &[ChannelId]) -> u64 { + // I probably should have done this, but I didn't + // let hasher = DefaultHasher::new(); + // let path = path.hash(&mut hasher); + // let x = hasher.finish(); + + const OFFSET: u64 = 14695981039346656037; + const PRIME: u64 = 1099511628211; + + let mut hash = OFFSET; + for id in path.iter() { + for id in id.to_ne_bytes() { + hash = hash ^ (id as u64); + hash = (hash as u128 * PRIME as u128) as u64; + } + } + + hash +} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 121f49b9660c8a882c822fd9ae93b9e66697b04a..ccff1526f067e1473b6bb71b74b7c055ba9c2dab 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -328,7 +328,6 @@ request_messages!( (GetChannelMessages, GetChannelMessagesResponse), (GetChannelMembers, GetChannelMembersResponse), (JoinChannel, JoinRoomResponse), - (RemoveChannel, Ack), (RemoveChannelMessage, Ack), (DeleteChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), From 77cdbdb12ae3edbdbc9fd711ff08f7aa8adff2fe Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 9 Sep 2023 12:10:18 -0700 Subject: [PATCH 09/26] remove extraneous depth field --- crates/channel/src/channel_store.rs | 5 ++--- .../src/channel_store/channel_index.rs | 22 +++++++++++-------- crates/collab_ui/src/collab_panel.rs | 3 ++- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 2aeff3afef871a5189b648af4bb9f039ae4a75a4..b7681c6ec7d45aaaec435dd6160db84c88445d38 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -146,13 +146,12 @@ impl ChannelStore { }) } - pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc, &Arc<[ChannelId]>)> { + pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc, &Arc<[ChannelId]>)> { let path = self.channel_index.get(ix)?; let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); - - Some((path.len() - 1, channel, path)) + Some((channel, path)) } pub fn channel_invitations(&self) -> &[Arc] { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index b52d7ba334eb442e881925f955fd157664ede06e..e64c5cff5cf1c1fa37db3b5b5923d3b2b5072b6b 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc}; +use std::sync::Arc; use collections::HashMap; use rpc::proto; @@ -25,6 +25,18 @@ impl ChannelIndex { self.channels_by_id.clear(); } + pub fn len(&self) -> usize { + self.paths.len() + } + + pub fn get(&self, idx: usize) -> Option<&ChannelPath> { + self.paths.get(idx) + } + + pub fn iter(&self) -> impl Iterator { + self.paths.iter() + } + /// Remove the given edge from this index. This will not remove the channel /// and may result in dangling channels. pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { @@ -50,14 +62,6 @@ impl ChannelIndex { } } -impl Deref for ChannelIndex { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.paths - } -} - /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions pub struct ChannelPathsUpsertGuard<'a> { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c9d5d97305ba1c1f82efa5efc95e42d75ccf2643..9c21633c86d62e705c46b3686a4e41e77683aa9f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -785,8 +785,9 @@ impl CollabPanel { } let mut collapse_depth = None; for mat in matches { - let (depth, channel, path) = + let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let depth = path.len() - 1; let location: ChannelLocation<'_> = (channel.id, path).into(); From 439f627d9a71400ba4cd3f5c531ca12c754b3ef5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 9 Sep 2023 13:24:04 -0700 Subject: [PATCH 10/26] Add move, link, and unlink operations --- crates/channel/src/channel_store.rs | 2 +- .../src/channel_store/channel_index.rs | 33 +- crates/collab_ui/src/collab_panel.rs | 315 ++++++++++++++---- 3 files changed, 278 insertions(+), 72 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index b7681c6ec7d45aaaec435dd6160db84c88445d38..1778e09a74f7238de8078fc5572e4db46fdcbe72 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -146,7 +146,7 @@ impl ChannelStore { }) } - pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc, &Arc<[ChannelId]>)> { + pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc, &ChannelPath)> { let path = self.channel_index.get(ix)?; let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index e64c5cff5cf1c1fa37db3b5b5923d3b2b5072b6b..a61109fa79d2d4d7392b0e555456b6b6a0f560a2 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,13 +1,38 @@ -use std::sync::Arc; +use std::{sync::Arc, ops::Deref}; use collections::HashMap; use rpc::proto; +use serde_derive::{Serialize, Deserialize}; use crate::{ChannelId, Channel}; -pub type ChannelPath = Arc<[ChannelId]>; pub type ChannelsById = HashMap>; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct ChannelPath(Arc<[ChannelId]>); + +impl Deref for ChannelPath { + type Target = [ChannelId]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ChannelPath { + pub fn parent_id(&self) -> Option { + self.0.len().checked_sub(2).map(|i| { + self.0[i] + }) + } +} + +impl Default for ChannelPath { + fn default() -> Self { + ChannelPath(Arc::from([])) + } +} + #[derive(Default, Debug)] pub struct ChannelIndex { paths: Vec, @@ -99,7 +124,7 @@ impl<'a> ChannelPathsUpsertGuard<'a> { if path.ends_with(&[parent_id]) { let mut new_path = path.to_vec(); new_path.push(channel_id); - self.paths.insert(ix + 1, new_path.into()); + self.paths.insert(ix + 1, ChannelPath(new_path.into())); ix += 1; } ix += 1; @@ -107,7 +132,7 @@ impl<'a> ChannelPathsUpsertGuard<'a> { } fn insert_root(&mut self, channel_id: ChannelId) { - self.paths.push(Arc::from([channel_id])); + self.paths.push(ChannelPath(Arc::from([channel_id]))); } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 9c21633c86d62e705c46b3686a4e41e77683aa9f..f0b10dffa4155f37f685f867ba209a7a52266b16 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -11,6 +11,7 @@ use anyhow::Result; use call::ActiveCall; use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath}; use channel_modal::ChannelModal; +use channel::{Channel, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use client::{proto::PeerId, Client, Contact, User, UserStore}; use contact_finder::ContactFinder; use context_menu::{ContextMenu, ContextMenuItem}; @@ -40,7 +41,7 @@ use menu::{Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use std::{borrow::Cow, mem, sync::Arc, hash::Hash}; +use std::{borrow::Cow, hash::Hash, mem, sync::Arc}; use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ @@ -95,18 +96,25 @@ struct OpenChannelBuffer { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct CopyChannel { +struct LinkChannel { channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct CutChannel { +struct MoveChannel { channel_id: ChannelId, + parent_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct PasteChannel { +struct PutChannel { + to: ChannelId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct UnlinkChannel { channel_id: ChannelId, + parent_id: ChannelId, } actions!( @@ -132,9 +140,10 @@ impl_actions!( OpenChannelNotes, JoinChannelCall, OpenChannelBuffer, - CopyChannel, - CutChannel, - PasteChannel, + LinkChannel, + MoveChannel, + PutChannel, + UnlinkChannel ] ); @@ -143,18 +152,24 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct ChannelLocation<'a> { channel: ChannelId, - parent: Cow<'a, ChannelPath>, + path: Cow<'a, ChannelPath>, } impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> { fn from(value: (ChannelId, ChannelPath)) -> Self { - ChannelLocation { channel: value.0, parent: Cow::Owned(value.1) } + ChannelLocation { + channel: value.0, + path: Cow::Owned(value.1), + } } } impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> { fn from(value: (ChannelId, &'a ChannelPath)) -> Self { - ChannelLocation { channel: value.0, parent: Cow::Borrowed(value.1) } + ChannelLocation { + channel: value.0, + path: Cow::Borrowed(value.1), + } } } @@ -182,25 +197,54 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::open_channel_notes); cx.add_action(CollabPanel::open_channel_buffer); - cx.add_action(|panel: &mut CollabPanel, action: &CopyChannel, _: &mut ViewContext| { - panel.copy = Some(ChannelCopy::Copy(action.channel_id)); - }); - - cx.add_action(|panel: &mut CollabPanel, action: &CutChannel, _: &mut ViewContext| { - // panel.copy = Some(ChannelCopy::Cut(action.channel_id)); - }); - - cx.add_action(|panel: &mut CollabPanel, action: &PasteChannel, cx: &mut ViewContext| { - if let Some(copy) = &panel.copy { - match copy { - ChannelCopy::Cut {..} => todo!(), - ChannelCopy::Copy(channel) => panel.channel_store.update(cx, |channel_store, cx| { - channel_store.move_channel(*channel, None, Some(action.channel_id), cx).detach_and_log_err(cx) - }), + cx.add_action( + |panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext| { + panel.copy = Some(ChannelCopy::Link(action.channel_id)); + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext| { + panel.copy = Some(ChannelCopy::Move { + channel_id: action.channel_id, + parent_id: action.parent_id, + }); + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext| { + if let Some(copy) = panel.copy.take() { + match copy { + ChannelCopy::Move { + channel_id, + parent_id, + } => panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .move_channel(channel_id, parent_id, Some(action.to), cx) + .detach_and_log_err(cx) + }), + ChannelCopy::Link(channel) => { + panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .move_channel(channel, None, Some(action.to), cx) + .detach_and_log_err(cx) + }) + } + } } - } - }); - + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext| { + panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .move_channel(action.channel_id, Some(action.parent_id), None, cx) + .detach_and_log_err(cx) + }) + }, + ); } #[derive(Debug)] @@ -224,12 +268,22 @@ impl ChannelEditingState { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ChannelCopy { - Cut { + Move { channel_id: u64, parent_id: Option, }, - Copy(u64), + Link(u64), +} + +impl ChannelCopy { + fn channel_id(&self) -> u64 { + match self { + ChannelCopy::Move { channel_id, .. } => *channel_id, + ChannelCopy::Link(channel_id) => *channel_id, + } + } } pub struct CollabPanel { @@ -304,7 +358,7 @@ enum ListEntry { Channel { channel: Arc, depth: usize, - path: Arc<[ChannelId]>, + path: ChannelPath, }, ChannelNotes { channel_id: ChannelId, @@ -422,7 +476,11 @@ impl CollabPanel { cx, ) } - ListEntry::Channel { channel, depth, path } => { + ListEntry::Channel { + channel, + depth, + path, + } => { let channel_row = this.render_channel( &*channel, *depth, @@ -583,7 +641,13 @@ impl CollabPanel { .log_err() .flatten() { - Some(serde_json::from_str::(&panel)?) + match serde_json::from_str::(&panel) { + Ok(panel) => Some(panel), + Err(err) => { + log::error!("Failed to deserialize collaboration panel: {}", err); + None + } + } } else { None }; @@ -773,20 +837,13 @@ impl CollabPanel { executor.clone(), )); if let Some(state) = &self.channel_editing_state { - if matches!( - state, - ChannelEditingState::Create { - location: None, - .. - } - ) { + if matches!(state, ChannelEditingState::Create { location: None, .. }) { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } let mut collapse_depth = None; for mat in matches { - let (channel, path) = - channel_store.channel_at_index(mat.candidate_id).unwrap(); + let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap(); let depth = path.len() - 1; let location: ChannelLocation<'_> = (channel.id, path).into(); @@ -805,9 +862,10 @@ impl CollabPanel { } match &self.channel_editing_state { - Some(ChannelEditingState::Create { location: parent_id, .. }) - if *parent_id == Some(location) => - { + Some(ChannelEditingState::Create { + location: parent_id, + .. + }) if *parent_id == Some(location) => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, @@ -817,7 +875,8 @@ impl CollabPanel { .push(ListEntry::ChannelEditor { depth: depth + 1 }); } Some(ChannelEditingState::Rename { location, .. }) - if location.channel == channel.id && location.parent == Cow::Borrowed(path) => + if location.channel == channel.id + && location.path == Cow::Borrowed(path) => { self.entries.push(ListEntry::ChannelEditor { depth }); } @@ -825,7 +884,7 @@ impl CollabPanel { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, - path: path.clone() + path: path.clone(), }); } } @@ -1638,7 +1697,7 @@ impl CollabPanel { let disclosed = { let location = ChannelLocation { channel: channel_id, - parent: Cow::Borrowed(&path), + path: Cow::Borrowed(&path), }; has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok()) }; @@ -1725,7 +1784,12 @@ impl CollabPanel { ) .align_children_center() .styleable_component() - .disclosable(disclosed, Box::new(ToggleCollapse { location: (channel_id, path.clone()).into() })) + .disclosable( + disclosed, + Box::new(ToggleCollapse { + location: (channel_id, path.clone()).into(), + }), + ) .with_id(id(&path) as usize) .with_style(theme.disclosure.clone()) .element() @@ -1742,7 +1806,11 @@ impl CollabPanel { this.join_channel_chat(channel_id, cx); }) .on_click(MouseButton::Right, move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), &(channel_id, path.clone()).into(), cx); + this.deploy_channel_context_menu( + Some(e.position), + &(channel_id, path.clone()).into(), + cx, + ); }) .with_cursor_style(CursorStyle::PointingHand) .into_any() @@ -1994,6 +2062,16 @@ impl CollabPanel { ) { self.context_menu_on_selected = position.is_none(); + let copy_channel = self + .copy + .as_ref() + .and_then(|copy| { + self.channel_store + .read(cx) + .channel_for_id(copy.channel_id()) + }) + .map(|channel| channel.name.clone()); + self.context_menu.update(cx, |context_menu, cx| { context_menu.set_position_mode(if self.context_menu_on_selected { OverlayPositionMode::Local @@ -2008,22 +2086,96 @@ impl CollabPanel { }; let mut items = vec![ - ContextMenuItem::action(expand_action_name, ToggleCollapse { location: location.clone() }), - ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: location.channel }), + ContextMenuItem::action( + expand_action_name, + ToggleCollapse { + location: location.clone(), + }, + ), + ContextMenuItem::action( + "Open Notes", + OpenChannelBuffer { + channel_id: location.channel, + }, + ), ]; if self.channel_store.read(cx).is_user_admin(location.channel) { + let parent_id = location.path.parent_id(); + items.extend([ ContextMenuItem::Separator, - ContextMenuItem::action("New Subchannel", NewChannel { location: location.clone() }), - ContextMenuItem::action("Rename", RenameChannel { location: location.clone() }), - ContextMenuItem::action("Copy", CopyChannel { channel_id: location.channel }), - ContextMenuItem::action("Paste", PasteChannel { channel_id: location.channel }), + ContextMenuItem::action( + "New Subchannel", + NewChannel { + location: location.clone(), + }, + ), + ContextMenuItem::action( + "Rename", + RenameChannel { + location: location.clone(), + }, + ), + ContextMenuItem::Separator, + ]); + + if let Some(parent) = parent_id { + items.push(ContextMenuItem::action( + "Unlink from parent", + UnlinkChannel { + channel_id: location.channel, + parent_id: parent, + }, + )) + } + + items.extend([ + ContextMenuItem::action( + "Link to new parent", + LinkChannel { + channel_id: location.channel, + }, + ), + ContextMenuItem::action( + "Move", + MoveChannel { + channel_id: location.channel, + parent_id, + }, + ), + ]); + + if let Some(copy_channel) = copy_channel { + items.push(ContextMenuItem::action( + format!("Put '#{}'", copy_channel), + PutChannel { + to: location.channel, + }, + )); + } + + items.extend([ ContextMenuItem::Separator, - ContextMenuItem::action("Invite Members", InviteMembers { channel_id: location.channel }), - ContextMenuItem::action("Manage Members", ManageMembers { channel_id: location.channel }), + ContextMenuItem::action( + "Invite Members", + InviteMembers { + channel_id: location.channel, + }, + ), + ContextMenuItem::action( + "Manage Members", + ManageMembers { + channel_id: location.channel, + }, + ), ContextMenuItem::Separator, - ContextMenuItem::action("Delete", RemoveChannel { channel_id: location.channel }), + ContextMenuItem::action( + "Delete", + RemoveChannel { + channel_id: location.channel, + }, + ), ]); } @@ -2162,7 +2314,11 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.create_channel(&channel_name, location.as_ref().map(|location| location.channel), cx) + channel_store.create_channel( + &channel_name, + location.as_ref().map(|location| location.channel), + cx, + ) }) .detach(); cx.notify(); @@ -2206,7 +2362,10 @@ impl CollabPanel { _: &CollapseSelectedChannel, cx: &mut ViewContext, ) { - let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else { + let Some((channel_id, path)) = self + .selected_channel() + .map(|(channel, parent)| (channel.id, parent)) + else { return; }; @@ -2216,11 +2375,19 @@ impl CollabPanel { return; } - self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx) + self.toggle_channel_collapsed( + &ToggleCollapse { + location: (channel_id, path).into(), + }, + cx, + ) } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else { + let Some((channel_id, path)) = self + .selected_channel() + .map(|(channel, parent)| (channel.id, parent)) + else { return; }; @@ -2230,7 +2397,12 @@ impl CollabPanel { return; } - self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx) + self.toggle_channel_collapsed( + &ToggleCollapse { + location: (channel_id, path).into(), + }, + cx, + ) } fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { @@ -2335,7 +2507,10 @@ impl CollabPanel { if !channel_store.is_user_admin(action.location.channel) { return; } - if let Some(channel) = channel_store.channel_for_id(action.location.channel).cloned() { + if let Some(channel) = channel_store + .channel_for_id(action.location.channel) + .cloned() + { self.channel_editing_state = Some(ChannelEditingState::Rename { location: action.location.to_owned(), pending_name: None, @@ -2368,7 +2543,11 @@ impl CollabPanel { self.selection .and_then(|ix| self.entries.get(ix)) .and_then(|entry| match entry { - ListEntry::Channel { channel, path: parent, .. } => Some((channel, parent)), + ListEntry::Channel { + channel, + path: parent, + .. + } => Some((channel, parent)), _ => None, }) } @@ -2759,7 +2938,9 @@ impl PartialEq for ListEntry { path: parent_2, } = other { - return channel_1.id == channel_2.id && depth_1 == depth_2 && parent_1 == parent_2; + return channel_1.id == channel_2.id + && depth_1 == depth_2 + && parent_1 == parent_2; } } ListEntry::ChannelNotes { channel_id } => { From cda54b8b5f8b3ccbc580713f981d246ff743d401 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 9 Sep 2023 18:20:14 -0700 Subject: [PATCH 11/26] Improve database and RPC API for moving and linking channels, improve test legibility --- crates/channel/src/channel_store.rs | 42 +- crates/collab/src/db/queries/channels.rs | 184 +++--- crates/collab/src/db/tests/channel_tests.rs | 599 +++++++------------- crates/collab/src/rpc.rs | 110 +++- crates/collab/src/tests/channel_tests.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 95 ++-- crates/rpc/proto/zed.proto | 18 +- crates/rpc/src/proto.rs | 4 + 8 files changed, 523 insertions(+), 533 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 1778e09a74f7238de8078fc5572e4db46fdcbe72..f95ae6bd9bacf927c1778083a9f1403a1d1db463 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -133,7 +133,7 @@ impl ChannelStore { } pub fn index_of_channel(&self, channel_id: ChannelId) -> Option { - self.channel_paths + self.channel_index .iter() .position(|path| path.ends_with(&[channel_id])) } @@ -327,11 +327,43 @@ impl ChannelStore { }) } + pub fn link_channel( + &mut self, + channel_id: ChannelId, + to: ChannelId, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::LinkChannel { channel_id, to }) + .await?; + + Ok(()) + }) + } + + pub fn unlink_channel( + &mut self, + channel_id: ChannelId, + from: Option, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::UnlinkChannel { channel_id, from }) + .await?; + + Ok(()) + }) + } + pub fn move_channel( &mut self, channel_id: ChannelId, - from_parent: Option, - to: Option, + from: Option, + to: ChannelId, cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); @@ -339,7 +371,7 @@ impl ChannelStore { let _ = client .request(proto::MoveChannel { channel_id, - from_parent, + from, to, }) .await?; @@ -802,6 +834,4 @@ impl ChannelStore { anyhow::Ok(()) })) } - - } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index d347be6c4f1a424f71c4c628cef4c43c9772a725..2bfbb7abdb9ba0ef20e983c63f35a6fb9c81650a 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -761,20 +761,41 @@ impl Database { .await } - async fn link_channel( + // Insert an edge from the given channel to the given other channel. + pub async fn link_channel( &self, - from: ChannelId, + user: UserId, + channel: ChannelId, + to: ChannelId, + ) -> Result> { + 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(channel, user, &*tx) + .await?; + + self.link_channel_internal(user, channel, to, &*tx).await + }) + .await + } + + pub async fn link_channel_internal( + &self, + user: UserId, + channel: ChannelId, to: ChannelId, tx: &DatabaseTransaction, - ) -> Result { + ) -> Result> { + self.check_user_is_channel_admin(to, user, &*tx).await?; + let to_ancestors = self.get_channel_ancestors(to, &*tx).await?; - let from_descendants = self.get_channel_descendants([from], &*tx).await?; + let mut from_descendants = self.get_channel_descendants([channel], &*tx).await?; for ancestor in to_ancestors { if from_descendants.contains_key(&ancestor) { return Err(anyhow!("Cannot create a channel cycle").into()); } } - let sql = r#" INSERT INTO channel_paths (id_path, channel_id) @@ -790,14 +811,13 @@ impl Database { self.pool.get_database_backend(), sql, [ - from.to_proto().into(), - from.to_proto().into(), + channel.to_proto().into(), + channel.to_proto().into(), to.to_proto().into(), ], ); tx.execute(channel_paths_stmt).await?; - - for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&from) { + for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&channel) { for to_id in to_ids { let channel_paths_stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), @@ -812,94 +832,116 @@ impl Database { } } - Ok(from_descendants) + if let Some(channel) = from_descendants.get_mut(&channel) { + // Remove the other parents + channel.clear(); + channel.insert(to); + } + + let channels = self.get_all_channels(from_descendants, &*tx).await?; + + Ok(channels) + } + + /// Unlink a channel from a given parent. This will add in a root edge if + /// the channel has no other parents after this operation. + pub async fn unlink_channel( + &self, + user: UserId, + channel: ChannelId, + from: Option, + ) -> Result<()> { + 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(channel, user, &*tx) + .await?; + + self.unlink_channel_internal(user, channel, from, &*tx) + .await?; + + Ok(()) + }) + .await } - async fn remove_channel_from_parent( + pub async fn unlink_channel_internal( &self, - from: ChannelId, - parent: ChannelId, + user: UserId, + channel: ChannelId, + from: Option, tx: &DatabaseTransaction, ) -> Result<()> { - let sql = r#" + if let Some(from) = from { + self.check_user_is_channel_admin(from, user, &*tx).await?; + + let sql = r#" DELETE FROM channel_paths WHERE id_path LIKE '%' || $1 || '/' || $2 || '%' "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [from.to_proto().into(), channel.to_proto().into()], + ); + tx.execute(channel_paths_stmt).await?; + } else { + let sql = r#" + DELETE FROM channel_paths + WHERE + id_path = '/' || $1 || '/' + "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [channel.to_proto().into()], + ); + tx.execute(channel_paths_stmt).await?; + } + + // Make sure that there is always at least one path to the channel + let sql = r#" + INSERT INTO channel_paths + (id_path, channel_id) + SELECT + '/' || $1 || '/', $2 + WHERE NOT EXISTS + (SELECT * + FROM channel_paths + WHERE channel_id = $2) + "#; + let channel_paths_stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, - [parent.to_proto().into(), from.to_proto().into()], + [channel.to_proto().into(), channel.to_proto().into()], ); tx.execute(channel_paths_stmt).await?; Ok(()) } - /// Move a channel from one parent to another. - /// Note that this requires a valid parent_id in the 'from_parent' field. - /// As channels are a DAG, we need to know which parent to remove the channel from. - /// Here's a list of the parameters to this function and their behavior: - /// - /// - (`None`, `None`) No op - /// - (`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 for use - /// by the members for `to` + /// Move a channel from one parent to another, returns the + /// Channels that were moved for notifying clients pub async fn move_channel( &self, user: UserId, - from: ChannelId, - from_parent: Option, - to: Option, + channel: ChannelId, + from: Option, + to: ChannelId, ) -> Result> { 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; - - // Note that we have to do the linking before the removal, so that we - // can leave the channel_path table in a consistent state. - if let Some(to) = to { - self.check_user_is_channel_admin(to, user, &*tx).await?; - - channel_descendants = Some(self.link_channel(from, to, &*tx).await?); - } - - let mut channel_descendants = match channel_descendants { - Some(channel_descendants) => channel_descendants, - None => self.get_channel_descendants([from], &*tx).await?, - }; - - if let Some(from_parent) = from_parent { - self.check_user_is_channel_admin(from_parent, user, &*tx) - .await?; - - self.remove_channel_from_parent(from, from_parent, &*tx) - .await?; - } + self.check_user_is_channel_admin(channel, user, &*tx) + .await?; - let channels; - if let Some(to) = to { - if let Some(channel) = channel_descendants.get_mut(&from) { - // Remove the other parents - channel.clear(); - channel.insert(to); - } + let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?; - channels = self - .get_all_channels(channel_descendants, &*tx) - .await?; - } else { - channels = vec![]; - } + self.unlink_channel_internal(user, channel, from, &*tx) + .await?; - Ok(channels) + Ok(moved_channels) }) .await } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 7f159ea0bd5e7154858dd80bbd940fd544795d0c..edf4bbef5a452526e9ebb3fc645c62e3dd9a1956 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1,7 +1,7 @@ use rpc::{proto, ConnectionId}; use crate::{ - db::{Channel, Database, NewUserParams}, + db::{Channel, ChannelId, Database, NewUserParams}, test_both_dbs, }; use std::sync::Arc; @@ -501,50 +501,32 @@ async fn test_channels_moving(db: &Arc) { .await .unwrap(); + // ======================================================================== // sanity check // Initial DAG: // /- gpui2 // zed -- crdb - livestreaming - livestreaming_dag let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( + assert_dag( result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".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), - }, - ] + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + ], ); // Attempt to make a cycle assert!(db - .move_channel(a_id, zed_id, None, Some(livestreaming_id)) + .link_channel(a_id, zed_id, livestreaming_id) .await .is_err()); + // ======================================================================== // Make a link - db.move_channel(a_id, livestreaming_id, None, Some(zed_id)) + db.link_channel(a_id, livestreaming_id, zed_id) .await .unwrap(); @@ -553,42 +535,16 @@ async fn test_channels_moving(db: &Arc) { // zed -- crdb - livestreaming - livestreaming_dag // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_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), - }, - ] - ); - + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + ]); + + // ======================================================================== // Create a new channel below a channel with multiple parents let livestreaming_dag_sub_id = db .create_channel( @@ -605,50 +561,20 @@ async fn test_channels_moving(db: &Arc) { // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_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_dag_id), - }, - ] - ); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); - // Make a link - let channels = db - .move_channel(a_id, livestreaming_dag_sub_id, None, Some(livestreaming_id)) + // ======================================================================== + // Test a complex DAG by making another link + let returned_channels = db + .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) .await .unwrap(); @@ -658,66 +584,32 @@ async fn test_channels_moving(db: &Arc) { // \--------/ // make sure we're getting just the new link + // Not using the assert_dag helper because we want to make sure we're returning the full data pretty_assertions::assert_eq!( - channels, - vec![ - Channel { - id: livestreaming_dag_sub_id, - name: "livestreaming_dag_sub".to_string(), - parent_id: Some(livestreaming_id), - } - ] + returned_channels, + vec![Channel { + id: livestreaming_dag_sub_id, + name: "livestreaming_dag_sub".to_string(), + parent_id: Some(livestreaming_id), + }] ); let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_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), - }, - ] - ); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); - // Make another link - let channels = db.move_channel(a_id, livestreaming_id, None, Some(gpui2_id)) + // ======================================================================== + // Test a complex DAG by making another link + let returned_channels = db + .link_channel(a_id, livestreaming_id, gpui2_id) .await .unwrap(); @@ -727,62 +619,14 @@ async fn test_channels_moving(db: &Arc) { // \---------/ // 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_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, + returned_channels, vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_id), - }, 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(), @@ -797,12 +641,31 @@ async fn test_channels_moving(db: &Arc) { id: livestreaming_dag_sub_id, name: "livestreaming_dag_sub".to_string(), parent_id: Some(livestreaming_dag_id), - }, + } ] ); - // Remove that inner link - let channels = db.move_channel(a_id, livestreaming_dag_sub_id, Some(livestreaming_id), None) + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); + + // ======================================================================== + // Test unlinking in a complex DAG by removing the inner link + db + .unlink_channel( + a_id, + livestreaming_dag_sub_id, + Some(livestreaming_id), + ) .await .unwrap(); @@ -811,62 +674,21 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub // \---------/ - // Since we're not moving it to anywhere, there's nothing to notify anyone about - pretty_assertions::assert_eq!( - channels, - vec![] - ); - - let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_id), - }, - 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_dag_id), - }, - ] - ); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); - // Remove that outer link - db.move_channel(a_id, livestreaming_id, Some(gpui2_id), None) + // ======================================================================== + // Test unlinking in a complex DAG by removing the inner link + db.unlink_channel(a_id, livestreaming_id, Some(gpui2_id)) .await .unwrap(); @@ -875,49 +697,19 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_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_dag_id), - }, - ] - ); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); - // Move livestreaming to be below gpui2 - db.move_channel(a_id, livestreaming_id, Some(crdb_id), Some(gpui2_id)) + // ======================================================================== + // Test moving DAG nodes by moving livestreaming to be below gpui2 + db.move_channel(a_id, livestreaming_id, Some(crdb_id), gpui2_id) .await .unwrap(); @@ -926,47 +718,17 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb / // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: gpui2_id, - name: "gpui2".to_string(), - parent_id: Some(zed_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(gpui2_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_dag_id), - }, - ] - ); - + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); + + // ======================================================================== // Deleting a channel should not delete children that still have other parents db.delete_channel(gpui2_id, a_id).await.unwrap(); @@ -974,46 +736,109 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb // \- livestreaming - livestreaming_dag - livestreaming_dag_sub let result = db.get_channels_for_user(a_id).await.unwrap(); - pretty_assertions::assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_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_dag_id), - }, - ] - ); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); + + // ======================================================================== + // Unlinking a channel from it's parent should automatically promote it to a root channel + db.unlink_channel(a_id, crdb_id, Some(zed_id)) + .await + .unwrap(); + + // DAG is now: + // crdb + // zed + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, None), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); - // But deleting a parent of a DAG should delete the whole DAG: - db.move_channel(a_id, livestreaming_id, None, Some(crdb_id)) + // ======================================================================== + // Unlinking a root channel should not have any effect + db.unlink_channel(a_id, crdb_id, None) .await .unwrap(); + + // DAG is now: + // crdb + // zed + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + // + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, None), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); + + // ======================================================================== + // You should be able to move a root channel into a non-root channel + db.move_channel(a_id, crdb_id, None, zed_id) + .await + .unwrap(); + + // DAG is now: + // zed - crdb + // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); + + + // ======================================================================== + // Moving a non-root channel without a parent id should be the equivalent of a link operation + db.move_channel(a_id, livestreaming_id, None, crdb_id) + .await + .unwrap(); + // DAG is now: // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub // \--------/ + let result = db.get_channels_for_user(a_id).await.unwrap(); + assert_dag(result.channels, &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ]); + + // ======================================================================== + // Deleting a parent of a DAG should delete the whole DAG: 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()) + assert!( + result.channels.is_empty() + ) +} + +#[track_caller] +fn assert_dag(actual: Vec, expected: &[(ChannelId, Option)]) { + let actual = actual + .iter() + .map(|channel| (channel.id, channel.parent_id)) + .collect::>(); + + pretty_assertions::assert_eq!(actual, expected) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index d15bc704eff90b639ca77c354846ea12b051e152..325d2e390be50253db5dc9fa12d88aa01052d7d1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,7 +3,7 @@ mod connection_pool; use crate::{ auth, db::{ - self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, + self, Channel, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, UserId, }, executor::Executor, @@ -267,6 +267,8 @@ impl Server { .add_request_handler(send_channel_message) .add_request_handler(remove_channel_message) .add_request_handler(get_channel_messages) + .add_request_handler(link_channel) + .add_request_handler(unlink_channel) .add_request_handler(move_channel) .add_request_handler(follow) .add_message_handler(unfollow) @@ -2391,26 +2393,51 @@ async fn rename_channel( Ok(()) } -async fn move_channel( - request: proto::MoveChannel, - response: Response, +async fn link_channel( + request: proto::LinkChannel, + response: Response, 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_to_send = db - .move_channel( - session.user_id, - channel_id, - from_parent, - to, - ) - .await?; + let to = ChannelId::from_proto(request.to); + let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?; + let members = db.get_channel_members(to).await?; + let connection_pool = session.connection_pool().await; + let update = proto::UpdateChannels { + channels: channels_to_send + .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())?; + } + } - if let Some(from_parent) = from_parent { + response.send(Ack {})?; + + Ok(()) +} + +async fn unlink_channel( + request: proto::UnlinkChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let from = request.from.map(ChannelId::from_proto); + db.unlink_channel(session.user_id, channel_id, from).await?; + + if let Some(from_parent) = from { let members = db.get_channel_members(from_parent).await?; let update = proto::UpdateChannels { delete_channel_edge: vec![proto::ChannelEdge { @@ -2425,20 +2452,36 @@ async fn move_channel( 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; + response.send(Ack {})?; + + Ok(()) +} + +async fn move_channel( + request: proto::MoveChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let from_parent = request.from.map(ChannelId::from_proto); + let to = ChannelId::from_proto(request.to); + let channels_to_send: Vec = 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 { - channels: channels_to_send.into_iter().map(|channel| proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - parent_id: channel.parent_id.map(ChannelId::to_proto), - }).collect(), + 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())?; @@ -2446,6 +2489,25 @@ async fn move_channel( } } + let members = db.get_channel_members(to).await?; + let connection_pool = session.connection_pool().await; + let update = proto::UpdateChannels { + channels: channels_to_send + .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(()) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 77045d91743482989b25cf65647d94516c257c0d..a0d480e57f80305aa9c711928706432a9eca7d96 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -933,7 +933,7 @@ async fn test_channel_moving(deterministic: Arc, cx_a: &mut TestA 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) + channel_store.move_channel(channel_c_id, Some(channel_b_id), channel_a_id, cx) }) .await .unwrap(); @@ -970,7 +970,7 @@ async fn test_channel_moving(deterministic: Arc, cx_a: &mut TestA client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_c_id, None, Some(channel_b_id), cx) + channel_store.link_channel(channel_c_id, channel_b_id, cx) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f0b10dffa4155f37f685f867ba209a7a52266b16..8914b7d9011a0ca51e706d1761cc88ada9b5df30 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -114,7 +114,7 @@ struct PutChannel { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct UnlinkChannel { channel_id: ChannelId, - parent_id: ChannelId, + parent_id: Option, } actions!( @@ -199,13 +199,13 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext| { - panel.copy = Some(ChannelCopy::Link(action.channel_id)); + panel.link_or_move = Some(ChannelCopy::Link(action.channel_id)); }, ); cx.add_action( |panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext| { - panel.copy = Some(ChannelCopy::Move { + panel.link_or_move = Some(ChannelCopy::Move { channel_id: action.channel_id, parent_id: action.parent_id, }); @@ -214,20 +214,20 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext| { - if let Some(copy) = panel.copy.take() { + if let Some(copy) = panel.link_or_move.take() { match copy { ChannelCopy::Move { channel_id, parent_id, } => panel.channel_store.update(cx, |channel_store, cx| { channel_store - .move_channel(channel_id, parent_id, Some(action.to), cx) + .move_channel(channel_id, parent_id, action.to, cx) .detach_and_log_err(cx) }), ChannelCopy::Link(channel) => { panel.channel_store.update(cx, |channel_store, cx| { channel_store - .move_channel(channel, None, Some(action.to), cx) + .link_channel(channel, action.to, cx) .detach_and_log_err(cx) }) } @@ -240,7 +240,7 @@ pub fn init(cx: &mut AppContext) { |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext| { panel.channel_store.update(cx, |channel_store, cx| { channel_store - .move_channel(action.channel_id, Some(action.parent_id), None, cx) + .unlink_channel(action.channel_id, action.parent_id, cx) .detach_and_log_err(cx) }) }, @@ -284,13 +284,20 @@ impl ChannelCopy { ChannelCopy::Link(channel_id) => *channel_id, } } + + fn is_move(&self) -> bool { + match self { + ChannelCopy::Move { .. } => true, + ChannelCopy::Link(_) => false, + } + } } pub struct CollabPanel { width: Option, fs: Arc, has_focus: bool, - copy: Option, + link_or_move: Option, pending_serialization: Task>, context_menu: ViewHandle, filter_editor: ViewHandle, @@ -553,7 +560,7 @@ impl CollabPanel { let mut this = Self { width: None, has_focus: false, - copy: None, + link_or_move: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), @@ -2062,15 +2069,14 @@ impl CollabPanel { ) { self.context_menu_on_selected = position.is_none(); - let copy_channel = self - .copy - .as_ref() - .and_then(|copy| { - self.channel_store - .read(cx) - .channel_for_id(copy.channel_id()) - }) - .map(|channel| channel.name.clone()); + let operation_details = self.link_or_move.as_ref().and_then(|link_or_move| { + let channel_name = self + .channel_store + .read(cx) + .channel_for_id(link_or_move.channel_id()) + .map(|channel| channel.name.clone())?; + Some((channel_name, link_or_move.is_move())) + }); self.context_menu.update(cx, |context_menu, cx| { context_menu.set_position_mode(if self.context_menu_on_selected { @@ -2079,13 +2085,29 @@ impl CollabPanel { OverlayPositionMode::Window }); + let mut items = Vec::new(); + + if let Some((channel_name, is_move)) = operation_details { + items.push(ContextMenuItem::action( + format!( + "{} '#{}' here", + if is_move { "Move" } else { "Link" }, + channel_name + ), + PutChannel { + to: location.channel, + }, + )); + items.push(ContextMenuItem::Separator) + } + let expand_action_name = if self.is_channel_collapsed(&location) { "Expand Subchannels" } else { "Collapse Subchannels" }; - let mut items = vec![ + items.extend([ ContextMenuItem::action( expand_action_name, ToggleCollapse { @@ -2098,7 +2120,7 @@ impl CollabPanel { channel_id: location.channel, }, ), - ]; + ]); if self.channel_store.read(cx).is_user_admin(location.channel) { let parent_id = location.path.parent_id(); @@ -2120,25 +2142,27 @@ impl CollabPanel { ContextMenuItem::Separator, ]); - if let Some(parent) = parent_id { - items.push(ContextMenuItem::action( - "Unlink from parent", - UnlinkChannel { - channel_id: location.channel, - parent_id: parent, - }, - )) - } + items.push(ContextMenuItem::action( + if parent_id.is_some() { + "Unlink from parent" + } else { + "Unlink from root" + }, + UnlinkChannel { + channel_id: location.channel, + parent_id, + }, + )); items.extend([ ContextMenuItem::action( - "Link to new parent", + "Link this channel", LinkChannel { channel_id: location.channel, }, ), ContextMenuItem::action( - "Move", + "Move this channel", MoveChannel { channel_id: location.channel, parent_id, @@ -2146,15 +2170,6 @@ impl CollabPanel { ), ]); - if let Some(copy_channel) = copy_channel { - items.push(ContextMenuItem::action( - format!("Put '#{}'", copy_channel), - PutChannel { - to: location.channel, - }, - )); - } - items.extend([ ContextMenuItem::Separator, ContextMenuItem::action( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f252efaa14c4f19365a19edf52e649016ff9bc6e..c12a55935504bf823285070dac26de985cf48f4e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -167,7 +167,9 @@ message Envelope { GetChannelMessagesResponse get_channel_messages_response = 149; RemoveChannelMessage remove_channel_message = 150; - MoveChannel move_channel = 151; // Current max + LinkChannel link_channel = 151; + UnlinkChannel unlink_channel = 152; + MoveChannel move_channel = 153; // Current max } } @@ -1082,10 +1084,20 @@ message GetChannelMessagesResponse { bool done = 2; } +message LinkChannel { + uint64 channel_id = 1; + uint64 to = 2; +} + +message UnlinkChannel { + uint64 channel_id = 1; + optional uint64 from = 2; +} + message MoveChannel { uint64 channel_id = 1; - optional uint64 from_parent = 2; - optional uint64 to = 3; + optional uint64 from = 2; + uint64 to = 3; } message JoinChannelBuffer { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index ccff1526f067e1473b6bb71b74b7c055ba9c2dab..0a2c4a9d7de0f977ff3b5b6dddb4fcd7ff7d7057 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -248,6 +248,8 @@ messages!( (UpdateContacts, Foreground), (DeleteChannel, Foreground), (MoveChannel, Foreground), + (LinkChannel, Foreground), + (UnlinkChannel, Foreground), (UpdateChannels, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateFollowers, Foreground), @@ -332,6 +334,8 @@ request_messages!( (DeleteChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), (RenameChannel, ChannelResponse), + (LinkChannel, Ack), + (UnlinkChannel, Ack), (MoveChannel, Ack), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), From 7fa68a9aa4178102d985057c62e365416cfadd51 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 10 Sep 2023 22:48:25 -0700 Subject: [PATCH 12/26] WIP: improve move and link handling around 'root paths', currently very incorrect and in need of a deeper rework --- .../src/channel_store/channel_index.rs | 66 ++-- crates/collab/src/db/queries/channels.rs | 38 +-- crates/collab/src/rpc.rs | 42 ++- .../collab/src/tests/channel_buffer_tests.rs | 10 +- crates/collab/src/tests/channel_tests.rs | 294 ++++++++++++------ crates/collab/src/tests/test_server.rs | 71 ++++- 6 files changed, 358 insertions(+), 163 deletions(-) diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index a61109fa79d2d4d7392b0e555456b6b6a0f560a2..90cde46e903d983d6029946fb41ab859102a8280 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,10 +1,11 @@ -use std::{sync::Arc, ops::Deref}; +use std::{ops::Deref, sync::Arc}; use collections::HashMap; use rpc::proto; -use serde_derive::{Serialize, Deserialize}; +use serde_derive::{Deserialize, Serialize}; -use crate::{ChannelId, Channel}; + +use crate::{Channel, ChannelId}; pub type ChannelsById = HashMap>; @@ -21,9 +22,7 @@ impl Deref for ChannelPath { impl ChannelPath { pub fn parent_id(&self) -> Option { - self.0.len().checked_sub(2).map(|i| { - self.0[i] - }) + self.0.len().checked_sub(2).map(|i| self.0[i]) } } @@ -39,7 +38,6 @@ pub struct ChannelIndex { channels_by_id: ChannelsById, } - impl ChannelIndex { pub fn by_id(&self) -> &ChannelsById { &self.channels_by_id @@ -62,24 +60,53 @@ impl ChannelIndex { self.paths.iter() } - /// Remove the given edge from this index. This will not remove the channel - /// and may result in dangling channels. - pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { - self.paths.retain(|path| { - !path - .windows(2) - .any(|window| window == [parent_id, channel_id]) - }); + /// Remove the given edge from this index. This will not remove the channel. + /// If this operation would result in a dangling edge, re-insert it. + pub fn delete_edge(&mut self, parent_id: Option, channel_id: ChannelId) { + if let Some(parent_id) = parent_id { + self.paths.retain(|path| { + !path + .windows(2) + .any(|window| window == [parent_id, channel_id]) + }); + } else { + self.paths.retain(|path| path.first() != Some(&channel_id)); + } + + // Ensure that there is at least one channel path in the index + if !self + .paths + .iter() + .any(|path| path.iter().any(|id| id == &channel_id)) + { + let path = ChannelPath(Arc::from([channel_id])); + let current_item: Vec<_> = + channel_path_sorting_key(&path, &self.channels_by_id).collect(); + match self.paths.binary_search_by(|channel_path| { + current_item + .iter() + .copied() + .cmp(channel_path_sorting_key(channel_path, &self.channels_by_id)) + }) { + Ok(ix) => self.paths.insert(ix, path), + Err(ix) => self.paths.insert(ix, path), + } + } } /// Delete the given channels from this index. pub fn delete_channels(&mut self, channels: &[ChannelId]) { - self.channels_by_id.retain(|channel_id, _| !channels.contains(channel_id)); - self.paths.retain(|channel_path| !channel_path.iter().any(|channel_id| {channels.contains(channel_id)})) + self.channels_by_id + .retain(|channel_id, _| !channels.contains(channel_id)); + self.paths.retain(|channel_path| { + !channel_path + .iter() + .any(|channel_id| channels.contains(channel_id)) + }) } /// Upsert one or more channels into this index. - pub fn start_upsert(& mut self) -> ChannelPathsUpsertGuard { + pub fn start_upsert(&mut self) -> ChannelPathsUpsertGuard { ChannelPathsUpsertGuard { paths: &mut self.paths, channels_by_id: &mut self.channels_by_id, @@ -90,7 +117,7 @@ impl ChannelIndex { /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions pub struct ChannelPathsUpsertGuard<'a> { - paths: &'a mut Vec, + paths: &'a mut Vec, channels_by_id: &'a mut ChannelsById, } @@ -151,7 +178,6 @@ impl<'a> Drop for ChannelPathsUpsertGuard<'a> { } } - fn channel_path_sorting_key<'a>( path: &'a [ChannelId], channels_by_id: &'a ChannelsById, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 2bfbb7abdb9ba0ef20e983c63f35a6fb9c81650a..449f48992fe9140756d911cd46c2957e520254bd 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -870,36 +870,22 @@ impl Database { &self, user: UserId, channel: ChannelId, - from: Option, + from: ChannelId, tx: &DatabaseTransaction, ) -> Result<()> { - if let Some(from) = from { - self.check_user_is_channel_admin(from, user, &*tx).await?; + self.check_user_is_channel_admin(from, user, &*tx).await?; - let sql = r#" + let sql = r#" DELETE FROM channel_paths WHERE id_path LIKE '%' || $1 || '/' || $2 || '%' "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [from.to_proto().into(), channel.to_proto().into()], - ); - tx.execute(channel_paths_stmt).await?; - } else { - let sql = r#" - DELETE FROM channel_paths - WHERE - id_path = '/' || $1 || '/' - "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [channel.to_proto().into()], - ); - tx.execute(channel_paths_stmt).await?; - } + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [from.to_proto().into(), channel.to_proto().into()], + ); + tx.execute(channel_paths_stmt).await?; // Make sure that there is always at least one path to the channel let sql = r#" @@ -929,7 +915,7 @@ impl Database { &self, user: UserId, channel: ChannelId, - from: Option, + from: ChannelId, to: ChannelId, ) -> Result> { self.transaction(|tx| async move { @@ -941,6 +927,10 @@ impl Database { self.unlink_channel_internal(user, channel, from, &*tx) .await?; + dbg!(channel_path::Entity::find().all(&*tx).await); + + dbg!(&moved_channels); + Ok(moved_channels) }) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 325d2e390be50253db5dc9fa12d88aa01052d7d1..8a6488459b651dda892c1256daea813ab5b30a42 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2435,22 +2435,23 @@ async fn unlink_channel( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let from = request.from.map(ChannelId::from_proto); + + // Get the members before we remove it, so we know who to notify + let members = db.get_channel_members(channel_id).await?; + db.unlink_channel(session.user_id, channel_id, from).await?; - if let Some(from_parent) = from { - 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())?; - } + let update = proto::UpdateChannels { + delete_channel_edge: vec![proto::ChannelEdge { + channel_id: channel_id.to_proto(), + parent_id: from.map(ChannelId::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())?; } } @@ -2468,16 +2469,24 @@ async fn move_channel( let channel_id = ChannelId::from_proto(request.channel_id); let from_parent = request.from.map(ChannelId::from_proto); let to = ChannelId::from_proto(request.to); + + let mut members = db.get_channel_members(channel_id).await?; + let channels_to_send: Vec = db .move_channel(session.user_id, channel_id, from_parent, to) .await?; + let members_after = db.get_channel_members(channel_id).await?; + + members.extend(members_after); + members.sort(); + members.dedup(); + 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(), + parent_id: Some(from_parent.to_proto()), }], ..Default::default() }; @@ -2489,7 +2498,6 @@ async fn move_channel( } } - let members = db.get_channel_members(to).await?; let connection_pool = session.connection_pool().await; let update = proto::UpdateChannels { channels: channels_to_send diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index fe286895b4ada34c697a6587b0243c130d0f328e..5cfff053cb7176887ccdc704f970835416d28500 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -25,7 +25,7 @@ async fn test_core_channel_buffers( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Client A joins the channel buffer @@ -135,6 +135,7 @@ async fn test_channel_buffer_replica_ids( let channel_id = server .make_channel( "the-channel", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -279,7 +280,7 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu let client_a = server.create_client(cx_a, "user_a").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut []) + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) .await; let channel_buffer_1 = client_a @@ -341,7 +342,7 @@ async fn test_channel_buffer_disconnect( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("the-channel", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let channel_buffer_a = client_a @@ -411,7 +412,7 @@ async fn test_rejoin_channel_buffer( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("the-channel", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let channel_buffer_a = client_a @@ -491,6 +492,7 @@ async fn test_channel_buffers_and_server_restarts( let channel_id = server .make_channel( "the-channel", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index a0d480e57f80305aa9c711928706432a9eca7d96..008dc0abc5f95223f6bcfc548fc2f516dcae34e1 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -326,7 +326,7 @@ async fn test_joining_channel_ancestor_member( let client_b = server.create_client(cx_b, "user_b").await; let parent_id = server - .make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let sub_id = client_a @@ -361,6 +361,7 @@ async fn test_channel_room( let zed_id = server .make_channel( "zed", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -544,9 +545,11 @@ async fn test_channel_jumping(deterministic: Arc, cx_a: &mut Test let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; - let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + let zed_id = server + .make_channel("zed", None, (&client_a, cx_a), &mut []) + .await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut []) + .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; let active_call_a = cx_a.read(ActiveCall::global); @@ -597,7 +600,7 @@ async fn test_permissions_update_while_invited( let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut []) + .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; client_a @@ -658,7 +661,7 @@ async fn test_channel_rename( let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Rename the channel @@ -716,6 +719,7 @@ async fn test_call_from_channel( let channel_id = server .make_channel( "x", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -776,6 +780,7 @@ async fn test_lost_channel_creation( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; @@ -786,7 +791,9 @@ async fn test_lost_channel_creation( .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await; + let channel_id = server + .make_channel("x", None, (&client_a, cx_a), &mut []) + .await; // Invite a member client_a @@ -875,140 +882,216 @@ async fn test_lost_channel_creation( } #[gpui::test] -async fn test_channel_moving(deterministic: Arc, cx_a: &mut TestAppContext) { +async fn test_channel_moving( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").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 + let channels = server + .make_channel_tree( + &[ + ("channel-a", None), + ("channel-b", Some("channel-a")), + ("channel-c", Some("channel-b")), + ("channel-d", Some("channel-c")), + ], + (&client_a, cx_a), + ) + .await; + let channel_a_a_id = channels[0]; + let channel_a_b_id = channels[1]; + let channel_a_c_id = channels[2]; + let channel_a_d_id = channels[3]; + + // Current shape: + // a - b - c - d + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 3), + ], + ); + + client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-b", Some(channel_a_id), cx) + channel_store.move_channel(channel_a_d_id, Some(channel_a_c_id), channel_a_b_id, cx) }) .await .unwrap(); - let channel_c_id = client_a + + // Current shape: + // /- d + // a - b -- c + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 2), + ], + ); + + client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-c", Some(channel_b_id), cx) + channel_store.link_channel(channel_a_d_id, channel_a_c_id, cx) }) .await .unwrap(); // Current shape: - // a - b - c - deterministic.run_until_parked(); - assert_channels( + // /------\ + // a - b -- c -- d + assert_channels_list_shape( 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, - }, + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 3), + (channel_a_d_id, 2), ], ); - client_a + let b_channels = server + .make_channel_tree( + &[ + ("channel-mu", None), + ("channel-gamma", Some("channel-mu")), + ("channel-epsilon", Some("channel-mu")), + ], + (&client_b, cx_b), + ) + .await; + let channel_b_mu_id = b_channels[0]; + let channel_b_gamma_id = b_channels[1]; + let channel_b_epsilon_id = b_channels[2]; + + // Current shape for B: + // /- ep + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1) + ], + ); + + client_a.add_admin_to_channel((&client_b, cx_b), channel_a_b_id, cx_a).await; + // Current shape for B: + // /- ep + // mu -- ga + // /---------\ + // b -- c -- d + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // B's old channels + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1), + + // New channels from a + (channel_a_b_id, 0), + (channel_a_c_id, 1), + (channel_a_d_id, 1), + (channel_a_d_id, 2), + ], + ); + + client_b .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_c_id, Some(channel_b_id), channel_a_id, cx) + channel_store.move_channel(channel_a_b_id, None, channel_b_epsilon_id, cx) }) .await .unwrap(); - // Current shape: - // /- c - // a -- b - deterministic.run_until_parked(); - assert_channels( - client_a.channel_store(), - cx_a, + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, &[ - 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, - }, + // B's old channels + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1), + + // New channels from a, now under epsilon + (channel_a_b_id, 2), + (channel_a_c_id, 3), + (channel_a_d_id, 3), + (channel_a_d_id, 4), ], ); - client_a + client_b .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.link_channel(channel_c_id, channel_b_id, cx) + channel_store.link_channel(channel_b_gamma_id, channel_a_b_id, cx) }) .await .unwrap(); - // Current shape: - // /------\ - // a -- b -- c - deterministic.run_until_parked(); - assert_channels( + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // / \ + // mu ---------- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // B's old channels + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1), + + // New channels from a, now under epsilon, with gamma + (channel_a_b_id, 2), + (channel_b_gamma_id, 3), + (channel_a_c_id, 3), + (channel_a_d_id, 3), + (channel_a_d_id, 4), + ], + ); + + // Current shape for A: + assert_channels_list_shape( 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, - }, + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_b_gamma_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 3), + (channel_a_d_id, 2), ], ); + // TODO: Make sure to test that non-local root removing problem I was thinking about } #[derive(Debug, PartialEq)] @@ -1059,3 +1142,20 @@ fn assert_channels( }); pretty_assertions::assert_eq!(actual, expected_channels); } + +#[track_caller] +fn assert_channels_list_shape( + channel_store: &ModelHandle, + cx: &TestAppContext, + expected_channels: &[(u64, usize)], +) { + cx.foreground().run_until_parked(); + + let actual = channel_store.read_with(cx, |store, _| { + store + .channels() + .map(|(depth, channel)| (channel.id, depth)) + .collect::>() + }); + pretty_assertions::assert_eq!(actual, expected_channels); +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index a7dbd97239079b41ff62a5e3e1a3cae4b9051986..7f1a91f426f7847ec371cf75c6e83bb639d068b0 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -288,6 +288,7 @@ impl TestServer { pub async fn make_channel( &self, channel: &str, + parent: Option, admin: (&TestClient, &mut TestAppContext), members: &mut [(&TestClient, &mut TestAppContext)], ) -> u64 { @@ -296,7 +297,7 @@ impl TestServer { .app_state .channel_store .update(admin_cx, |channel_store, cx| { - channel_store.create_channel(channel, None, cx) + channel_store.create_channel(channel, parent, cx) }) .await .unwrap(); @@ -331,6 +332,39 @@ impl TestServer { channel_id } + pub async fn make_channel_tree( + &self, + channels: &[(&str, Option<&str>)], + creator: (&TestClient, &mut TestAppContext), + ) -> Vec { + let mut observed_channels = HashMap::default(); + let mut result = Vec::new(); + for (channel, parent) in channels { + let id; + if let Some(parent) = parent { + if let Some(parent_id) = observed_channels.get(parent) { + id = self + .make_channel(channel, Some(*parent_id), (creator.0, creator.1), &mut []) + .await; + } else { + panic!( + "Edge {}->{} referenced before {} was created", + parent, channel, parent + ) + } + } else { + id = self + .make_channel(channel, None, (creator.0, creator.1), &mut []) + .await; + } + + observed_channels.insert(channel, id); + result.push(id); + } + + result + } + pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { self.make_contacts(clients).await; @@ -549,6 +583,41 @@ impl TestClient { ) -> WindowHandle { cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } + + pub async fn add_admin_to_channel( + &self, + user: (&TestClient, &mut TestAppContext), + channel: u64, + cx_self: &mut TestAppContext, + ) { + let (other_client, other_cx) = user; + + self + .app_state + .channel_store + .update(cx_self, |channel_store, cx| { + channel_store.invite_member( + channel, + other_client.user_id().unwrap(), + true, + cx, + ) + }) + .await + .unwrap(); + + cx_self.foreground().run_until_parked(); + + other_client + .app_state + .channel_store + .update(other_cx, |channels, _| { + channels.respond_to_channel_invite(channel, true) + }) + .await + .unwrap(); + + } } impl Drop for TestClient { From 9afb67f2cf1c2bb4ad7f978d2b42f046b7b2e3bb Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 14 Sep 2023 20:29:29 -0700 Subject: [PATCH 13/26] Implement final move, link, unlink db APIs --- crates/channel/src/channel_store.rs | 4 +- .../src/channel_store/channel_index.rs | 17 +- crates/collab/src/db/queries/channels.rs | 26 ++- crates/collab/src/db/tests/channel_tests.rs | 38 +---- crates/collab/src/rpc.rs | 46 +++-- crates/collab/src/tests/channel_tests.rs | 157 +++++++++--------- crates/collab_ui/src/collab_panel.rs | 40 ++--- crates/rpc/proto/zed.proto | 4 +- 8 files changed, 159 insertions(+), 173 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index f95ae6bd9bacf927c1778083a9f1403a1d1db463..08ff11f85dae83104f57859abad72d2559e5d5d0 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -346,7 +346,7 @@ impl ChannelStore { pub fn unlink_channel( &mut self, channel_id: ChannelId, - from: Option, + from: ChannelId, cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); @@ -362,7 +362,7 @@ impl ChannelStore { pub fn move_channel( &mut self, channel_id: ChannelId, - from: Option, + from: ChannelId, to: ChannelId, cx: &mut ModelContext, ) -> Task> { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 90cde46e903d983d6029946fb41ab859102a8280..08c50601966bce44678944b3ed44af6e2a193212 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -62,16 +62,13 @@ impl ChannelIndex { /// Remove the given edge from this index. This will not remove the channel. /// If this operation would result in a dangling edge, re-insert it. - pub fn delete_edge(&mut self, parent_id: Option, channel_id: ChannelId) { - if let Some(parent_id) = parent_id { - self.paths.retain(|path| { - !path - .windows(2) - .any(|window| window == [parent_id, channel_id]) - }); - } else { - self.paths.retain(|path| path.first() != Some(&channel_id)); - } + pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + self.paths.retain(|path| { + !path + .windows(2) + .any(|window| window == [parent_id, channel_id]) + }); + // Ensure that there is at least one channel path in the index if !self diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 449f48992fe9140756d911cd46c2957e520254bd..f8ad45363271a3e2326f8fe699ca601bc9204691 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -48,7 +48,6 @@ impl Database { .insert(&*tx) .await?; - let channel_paths_stmt; if let Some(parent) = parent { let sql = r#" INSERT INTO channel_paths @@ -60,7 +59,7 @@ impl Database { WHERE channel_id = $3 "#; - channel_paths_stmt = Statement::from_sql_and_values( + let channel_paths_stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, [ @@ -796,6 +795,8 @@ impl Database { return Err(anyhow!("Cannot create a channel cycle").into()); } } + + // Now insert all of the new paths let sql = r#" INSERT INTO channel_paths (id_path, channel_id) @@ -832,6 +833,21 @@ impl Database { } } + // If we're linking a channel, remove any root edges for the channel + { + let sql = r#" + DELETE FROM channel_paths + WHERE + id_path = '/' || $1 || '/' + "#; + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [channel.to_proto().into()], + ); + tx.execute(channel_paths_stmt).await?; + } + if let Some(channel) = from_descendants.get_mut(&channel) { // Remove the other parents channel.clear(); @@ -849,7 +865,7 @@ impl Database { &self, user: UserId, channel: ChannelId, - from: Option, + from: ChannelId, ) -> Result<()> { self.transaction(|tx| async move { // Note that even with these maxed permissions, this linking operation @@ -927,10 +943,6 @@ impl Database { self.unlink_channel_internal(user, channel, from, &*tx) .await?; - dbg!(channel_path::Entity::find().all(&*tx).await); - - dbg!(&moved_channels); - Ok(moved_channels) }) .await diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index edf4bbef5a452526e9ebb3fc645c62e3dd9a1956..50faa2a91081708fe727be51f1023f94c4f345b2 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -664,7 +664,7 @@ async fn test_channels_moving(db: &Arc) { .unlink_channel( a_id, livestreaming_dag_sub_id, - Some(livestreaming_id), + livestreaming_id, ) .await .unwrap(); @@ -688,7 +688,7 @@ async fn test_channels_moving(db: &Arc) { // ======================================================================== // Test unlinking in a complex DAG by removing the inner link - db.unlink_channel(a_id, livestreaming_id, Some(gpui2_id)) + db.unlink_channel(a_id, livestreaming_id, gpui2_id) .await .unwrap(); @@ -709,7 +709,7 @@ async fn test_channels_moving(db: &Arc) { // ======================================================================== // Test moving DAG nodes by moving livestreaming to be below gpui2 - db.move_channel(a_id, livestreaming_id, Some(crdb_id), gpui2_id) + db.move_channel(a_id, livestreaming_id, crdb_id, gpui2_id) .await .unwrap(); @@ -746,7 +746,7 @@ async fn test_channels_moving(db: &Arc) { // ======================================================================== // Unlinking a channel from it's parent should automatically promote it to a root channel - db.unlink_channel(a_id, crdb_id, Some(zed_id)) + db.unlink_channel(a_id, crdb_id, zed_id) .await .unwrap(); @@ -764,29 +764,9 @@ async fn test_channels_moving(db: &Arc) { (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), ]); - // ======================================================================== - // Unlinking a root channel should not have any effect - db.unlink_channel(a_id, crdb_id, None) - .await - .unwrap(); - - // DAG is now: - // crdb - // zed - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - // - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, None), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); - // ======================================================================== // You should be able to move a root channel into a non-root channel - db.move_channel(a_id, crdb_id, None, zed_id) + db.link_channel(a_id, crdb_id, zed_id) .await .unwrap(); @@ -805,8 +785,8 @@ async fn test_channels_moving(db: &Arc) { // ======================================================================== - // Moving a non-root channel without a parent id should be the equivalent of a link operation - db.move_channel(a_id, livestreaming_id, None, crdb_id) + // Prep for DAG deletion test + db.link_channel(a_id, livestreaming_id, crdb_id) .await .unwrap(); @@ -824,10 +804,10 @@ async fn test_channels_moving(db: &Arc) { (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), ]); - // ======================================================================== - // Deleting a parent of a DAG should delete the whole DAG: + // Deleting the parent of a DAG should delete the whole DAG: 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() ) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8a6488459b651dda892c1256daea813ab5b30a42..6a5ba3e5d4a3632ec6f512e17ecb98fbcc0fa758 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2434,17 +2434,16 @@ async fn unlink_channel( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let from = request.from.map(ChannelId::from_proto); - - // Get the members before we remove it, so we know who to notify - let members = db.get_channel_members(channel_id).await?; + let from = ChannelId::from_proto(request.from); db.unlink_channel(session.user_id, channel_id, from).await?; + let members = db.get_channel_members(from).await?; + let update = proto::UpdateChannels { delete_channel_edge: vec![proto::ChannelEdge { channel_id: channel_id.to_proto(), - parent_id: from.map(ChannelId::to_proto), + parent_id: from.to_proto(), }], ..Default::default() }; @@ -2467,38 +2466,31 @@ async fn move_channel( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let from_parent = request.from.map(ChannelId::from_proto); + let from_parent = ChannelId::from_proto(request.from); let to = ChannelId::from_proto(request.to); - let mut members = db.get_channel_members(channel_id).await?; + let members_from = db.get_channel_members(channel_id).await?; let channels_to_send: Vec = db .move_channel(session.user_id, channel_id, from_parent, to) .await?; - let members_after = db.get_channel_members(channel_id).await?; - - members.extend(members_after); - members.sort(); - members.dedup(); + let members_to = db.get_channel_members(channel_id).await?; - if let Some(from_parent) = from_parent { - let update = proto::UpdateChannels { - delete_channel_edge: vec![proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: Some(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())?; - } + 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_from { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; } } - let connection_pool = session.connection_pool().await; let update = proto::UpdateChannels { channels: channels_to_send .into_iter() @@ -2510,7 +2502,7 @@ async fn move_channel( .collect(), ..Default::default() }; - for member_id in members { + for member_id in members_to { for connection_id in connection_pool.user_connection_ids(member_id) { session.peer.send(connection_id, update.clone())?; } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 008dc0abc5f95223f6bcfc548fc2f516dcae34e1..09583fda29194a52b2f53b66fa8a7e0847dd6de9 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -905,10 +905,15 @@ async fn test_channel_moving( (&client_a, cx_a), ) .await; - let channel_a_a_id = channels[0]; - let channel_a_b_id = channels[1]; - let channel_a_c_id = channels[2]; - let channel_a_d_id = channels[3]; + let channel_a_id = channels[0]; + let channel_b_id = channels[1]; + let channel_c_id = channels[2]; + let channel_d_id = channels[3]; + + dbg!(channel_a_id); + dbg!(channel_b_id); + dbg!(channel_c_id); + dbg!(channel_d_id); // Current shape: // a - b - c - d @@ -916,17 +921,17 @@ async fn test_channel_moving( client_a.channel_store(), cx_a, &[ - (channel_a_a_id, 0), - (channel_a_b_id, 1), - (channel_a_c_id, 2), - (channel_a_d_id, 3), + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), ], ); client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_a_d_id, Some(channel_a_c_id), channel_a_b_id, cx) + channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx) }) .await .unwrap(); @@ -938,17 +943,17 @@ async fn test_channel_moving( client_a.channel_store(), cx_a, &[ - (channel_a_a_id, 0), - (channel_a_b_id, 1), - (channel_a_c_id, 2), - (channel_a_d_id, 2), + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 2), ], ); client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.link_channel(channel_a_d_id, channel_a_c_id, cx) + channel_store.link_channel(channel_d_id, channel_c_id, cx) }) .await .unwrap(); @@ -960,11 +965,11 @@ async fn test_channel_moving( client_a.channel_store(), cx_a, &[ - (channel_a_a_id, 0), - (channel_a_b_id, 1), - (channel_a_c_id, 2), - (channel_a_d_id, 3), - (channel_a_d_id, 2), + (channel_a_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), ], ); @@ -978,9 +983,9 @@ async fn test_channel_moving( (&client_b, cx_b), ) .await; - let channel_b_mu_id = b_channels[0]; - let channel_b_gamma_id = b_channels[1]; - let channel_b_epsilon_id = b_channels[2]; + let channel_mu_id = b_channels[0]; + let channel_ga_id = b_channels[1]; + let channel_ep_id = b_channels[2]; // Current shape for B: // /- ep @@ -989,13 +994,13 @@ async fn test_channel_moving( client_b.channel_store(), cx_b, &[ - (channel_b_mu_id, 0), - (channel_b_gamma_id, 1), - (channel_b_epsilon_id, 1) + (channel_mu_id, 0), + (channel_ga_id, 1), + (channel_ep_id, 1) ], ); - client_a.add_admin_to_channel((&client_b, cx_b), channel_a_b_id, cx_a).await; + client_a.add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a).await; // Current shape for B: // /- ep // mu -- ga @@ -1006,51 +1011,51 @@ async fn test_channel_moving( cx_b, &[ // B's old channels - (channel_b_mu_id, 0), - (channel_b_gamma_id, 1), - (channel_b_epsilon_id, 1), + (channel_mu_id, 0), + (channel_ga_id, 1), + (channel_ep_id, 1), // New channels from a - (channel_a_b_id, 0), - (channel_a_c_id, 1), - (channel_a_d_id, 1), - (channel_a_d_id, 2), + (channel_b_id, 0), + (channel_c_id, 1), + (channel_d_id, 1), + (channel_d_id, 2), ], ); - client_b - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_a_b_id, None, channel_b_epsilon_id, cx) - }) - .await - .unwrap(); - - // Current shape for B: - // /---------\ - // /- ep -- b -- c -- d - // mu -- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - // B's old channels - (channel_b_mu_id, 0), - (channel_b_gamma_id, 1), - (channel_b_epsilon_id, 1), - - // New channels from a, now under epsilon - (channel_a_b_id, 2), - (channel_a_c_id, 3), - (channel_a_d_id, 3), - (channel_a_d_id, 4), - ], - ); + // client_b + // .channel_store() + // .update(cx_a, |channel_store, cx| { + // channel_store.move_channel(channel_a_b_id, None, channel_b_epsilon_id, cx) + // }) + // .await + // .unwrap(); + + // // Current shape for B: + // // /---------\ + // // /- ep -- b -- c -- d + // // mu -- ga + // assert_channels_list_shape( + // client_b.channel_store(), + // cx_b, + // &[ + // // B's old channels + // (channel_b_mu_id, 0), + // (channel_b_gamma_id, 1), + // (channel_b_epsilon_id, 1), + + // // New channels from a, now under epsilon + // (channel_a_b_id, 2), + // (channel_a_c_id, 3), + // (channel_a_d_id, 3), + // (channel_a_d_id, 4), + // ], + // ); client_b .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.link_channel(channel_b_gamma_id, channel_a_b_id, cx) + channel_store.link_channel(channel_ga_id, channel_b_id, cx) }) .await .unwrap(); @@ -1065,16 +1070,16 @@ async fn test_channel_moving( cx_b, &[ // B's old channels - (channel_b_mu_id, 0), - (channel_b_gamma_id, 1), - (channel_b_epsilon_id, 1), + (channel_mu_id, 0), + (channel_ga_id, 1), + (channel_ep_id, 1), // New channels from a, now under epsilon, with gamma - (channel_a_b_id, 2), - (channel_b_gamma_id, 3), - (channel_a_c_id, 3), - (channel_a_d_id, 3), - (channel_a_d_id, 4), + (channel_b_id, 2), + (channel_ga_id, 3), + (channel_c_id, 3), + (channel_d_id, 3), + (channel_d_id, 4), ], ); @@ -1083,12 +1088,12 @@ async fn test_channel_moving( client_a.channel_store(), cx_a, &[ - (channel_a_a_id, 0), - (channel_a_b_id, 1), - (channel_b_gamma_id, 1), - (channel_a_c_id, 2), - (channel_a_d_id, 3), - (channel_a_d_id, 2), + (channel_a_id, 0), + (channel_b_id, 1), + (channel_ga_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), ], ); // TODO: Make sure to test that non-local root removing problem I was thinking about diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8914b7d9011a0ca51e706d1761cc88ada9b5df30..4e889d35d49288615f082947a1138083aa9a898f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -114,7 +114,7 @@ struct PutChannel { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct UnlinkChannel { channel_id: ChannelId, - parent_id: Option, + parent_id: ChannelId, } actions!( @@ -218,19 +218,21 @@ pub fn init(cx: &mut AppContext) { match copy { ChannelCopy::Move { channel_id, - parent_id, + parent_id: Some(parent_id), } => panel.channel_store.update(cx, |channel_store, cx| { channel_store .move_channel(channel_id, parent_id, action.to, cx) .detach_and_log_err(cx) }), - ChannelCopy::Link(channel) => { - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(channel, action.to, cx) - .detach_and_log_err(cx) - }) - } + ChannelCopy::Link(channel) + | ChannelCopy::Move { + channel_id: channel, + parent_id: None, + } => panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .link_channel(channel, action.to, cx) + .detach_and_log_err(cx) + }), } } }, @@ -2142,17 +2144,15 @@ impl CollabPanel { ContextMenuItem::Separator, ]); - items.push(ContextMenuItem::action( - if parent_id.is_some() { - "Unlink from parent" - } else { - "Unlink from root" - }, - UnlinkChannel { - channel_id: location.channel, - parent_id, - }, - )); + if let Some(parent_id) = parent_id { + items.push(ContextMenuItem::action( + "Unlink from parent", + UnlinkChannel { + channel_id: location.channel, + parent_id, + }, + )); + } items.extend([ ContextMenuItem::action( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c12a55935504bf823285070dac26de985cf48f4e..54414f38f4b7d50a094a2421576704331ffe1474 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1091,12 +1091,12 @@ message LinkChannel { message UnlinkChannel { uint64 channel_id = 1; - optional uint64 from = 2; + uint64 from = 2; } message MoveChannel { uint64 channel_id = 1; - optional uint64 from = 2; + uint64 from = 2; uint64 to = 3; } From 67ad75a376bf77b173cf3cdf58a0c89eb79b4230 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 00:38:02 -0700 Subject: [PATCH 14/26] Clean up implementation of channel index, get simple channel moving test cases working --- crates/channel/src/channel_store.rs | 17 ++-- .../src/channel_store/channel_index.rs | 88 +++++++------------ crates/collab/src/db/queries/channels.rs | 35 ++++++-- crates/collab/src/rpc.rs | 4 +- crates/collab/src/tests/channel_tests.rs | 79 ++++++++--------- crates/rpc/proto/zed.proto | 2 +- 6 files changed, 114 insertions(+), 111 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 08ff11f85dae83104f57859abad72d2559e5d5d0..b37f918052a820c98e6eb206ac78645ab62fbd8c 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -747,7 +747,7 @@ impl ChannelStore { } } - let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty(); + let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() || !payload.delete_edge.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { self.channel_index.delete_channels(&payload.delete_channels); @@ -768,17 +768,20 @@ impl ChannelStore { } } - let mut channel_index = self.channel_index.start_upsert(); + let mut index_edit = self.channel_index.bulk_edit(); + for channel in payload.channels { - channel_index.upsert(channel) + index_edit.upsert(channel) } - } - for edge in payload.delete_channel_edge { - self.channel_index - .delete_edge(edge.parent_id, edge.channel_id); + for edge in payload.delete_edge { + index_edit + .delete_edge(edge.parent_id, edge.channel_id); + } } + + for permission in payload.channel_permissions { if permission.is_admin { self.channels_with_admin_privileges diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 08c50601966bce44678944b3ed44af6e2a193212..36e02f413483b07001660a64aa907aaa7a04b334 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -4,7 +4,6 @@ use collections::HashMap; use rpc::proto; use serde_derive::{Deserialize, Serialize}; - use crate::{Channel, ChannelId}; pub type ChannelsById = HashMap>; @@ -48,18 +47,40 @@ impl ChannelIndex { self.channels_by_id.clear(); } - pub fn len(&self) -> usize { - self.paths.len() + /// Delete the given channels from this index. + pub fn delete_channels(&mut self, channels: &[ChannelId]) { + self.channels_by_id + .retain(|channel_id, _| !channels.contains(channel_id)); + self.paths.retain(|path| { + path.iter() + .all(|channel_id| self.channels_by_id.contains_key(channel_id)) + }); } - pub fn get(&self, idx: usize) -> Option<&ChannelPath> { - self.paths.get(idx) + pub fn bulk_edit(&mut self) -> ChannelPathsEditGuard { + ChannelPathsEditGuard { + paths: &mut self.paths, + channels_by_id: &mut self.channels_by_id, + } } +} + +impl Deref for ChannelIndex { + type Target = [ChannelPath]; - pub fn iter(&self) -> impl Iterator { - self.paths.iter() + fn deref(&self) -> &Self::Target { + &self.paths } +} + +/// A guard for ensuring that the paths index maintains its sort and uniqueness +/// invariants after a series of insertions +pub struct ChannelPathsEditGuard<'a> { + paths: &'a mut Vec, + channels_by_id: &'a mut ChannelsById, +} +impl<'a> ChannelPathsEditGuard<'a> { /// Remove the given edge from this index. This will not remove the channel. /// If this operation would result in a dangling edge, re-insert it. pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { @@ -69,56 +90,16 @@ impl ChannelIndex { .any(|window| window == [parent_id, channel_id]) }); - // Ensure that there is at least one channel path in the index if !self .paths .iter() .any(|path| path.iter().any(|id| id == &channel_id)) { - let path = ChannelPath(Arc::from([channel_id])); - let current_item: Vec<_> = - channel_path_sorting_key(&path, &self.channels_by_id).collect(); - match self.paths.binary_search_by(|channel_path| { - current_item - .iter() - .copied() - .cmp(channel_path_sorting_key(channel_path, &self.channels_by_id)) - }) { - Ok(ix) => self.paths.insert(ix, path), - Err(ix) => self.paths.insert(ix, path), - } + self.insert_root(channel_id); } } - /// Delete the given channels from this index. - pub fn delete_channels(&mut self, channels: &[ChannelId]) { - self.channels_by_id - .retain(|channel_id, _| !channels.contains(channel_id)); - self.paths.retain(|channel_path| { - !channel_path - .iter() - .any(|channel_id| channels.contains(channel_id)) - }) - } - - /// Upsert one or more channels into this index. - pub fn start_upsert(&mut self) -> ChannelPathsUpsertGuard { - ChannelPathsUpsertGuard { - paths: &mut self.paths, - channels_by_id: &mut self.channels_by_id, - } - } -} - -/// A guard for ensuring that the paths index maintains its sort and uniqueness -/// invariants after a series of insertions -pub struct ChannelPathsUpsertGuard<'a> { - paths: &'a mut Vec, - channels_by_id: &'a mut ChannelsById, -} - -impl<'a> ChannelPathsUpsertGuard<'a> { pub fn upsert(&mut self, channel_proto: proto::Channel) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { Arc::make_mut(existing_channel).name = channel_proto.name; @@ -149,9 +130,12 @@ impl<'a> ChannelPathsUpsertGuard<'a> { let mut new_path = path.to_vec(); new_path.push(channel_id); self.paths.insert(ix + 1, ChannelPath(new_path.into())); + ix += 2; + } else if path.len() == 1 && path[0] == channel_id { + self.paths.swap_remove(ix); + } else { ix += 1; } - ix += 1; } } @@ -160,7 +144,7 @@ impl<'a> ChannelPathsUpsertGuard<'a> { } } -impl<'a> Drop for ChannelPathsUpsertGuard<'a> { +impl<'a> Drop for ChannelPathsEditGuard<'a> { fn drop(&mut self) { self.paths.sort_by(|a, b| { let a = channel_path_sorting_key(a, &self.channels_by_id); @@ -168,10 +152,6 @@ impl<'a> Drop for ChannelPathsUpsertGuard<'a> { a.cmp(b) }); self.paths.dedup(); - self.paths.retain(|path| { - path.iter() - .all(|channel_id| self.channels_by_id.contains_key(channel_id)) - }); } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index f8ad45363271a3e2326f8fe699ca601bc9204691..90374e76b20be7de62ddb438960a3832a11c093c 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -333,9 +333,10 @@ impl Database { .await } - async fn get_all_channels( + async fn get_channels_internal( &self, parents_by_child_id: ChannelDescendants, + trim_dangling_parents: bool, tx: &DatabaseTransaction, ) -> Result> { let mut channels = Vec::with_capacity(parents_by_child_id.len()); @@ -346,15 +347,36 @@ impl Database { .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 { + let mut added_channel = false; for parent in parents { + // Trim out any dangling parent pointers. + // That the user doesn't have access to + if trim_dangling_parents { + if parents_by_child_id.contains_key(parent) { + added_channel = true; + channels.push(Channel { + id: row.id, + name: row.name.clone(), + parent_id: Some(*parent), + }); + } + } else { + added_channel = true; + channels.push(Channel { + id: row.id, + name: row.name.clone(), + parent_id: Some(*parent), + }); + } + } + if !added_channel { channels.push(Channel { id: row.id, - name: row.name.clone(), - parent_id: Some(*parent), + name: row.name, + parent_id: None, }); } } else { @@ -392,7 +414,8 @@ impl Database { .filter_map(|membership| membership.admin.then_some(membership.channel_id)) .collect(); - let channels = self.get_all_channels(parents_by_child_id, &tx).await?; + let channels = self.get_channels_internal(parents_by_child_id, true, &tx).await?; + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { @@ -854,7 +877,7 @@ impl Database { channel.insert(to); } - let channels = self.get_all_channels(from_descendants, &*tx).await?; + let channels = self.get_channels_internal(from_descendants, false, &*tx).await?; Ok(channels) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6a5ba3e5d4a3632ec6f512e17ecb98fbcc0fa758..1d89c6fe42ae50161bfa2cfd02b112da5472e9f9 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2441,7 +2441,7 @@ async fn unlink_channel( let members = db.get_channel_members(from).await?; let update = proto::UpdateChannels { - delete_channel_edge: vec![proto::ChannelEdge { + delete_edge: vec![proto::ChannelEdge { channel_id: channel_id.to_proto(), parent_id: from.to_proto(), }], @@ -2478,7 +2478,7 @@ async fn move_channel( let members_to = db.get_channel_members(channel_id).await?; let update = proto::UpdateChannels { - delete_channel_edge: vec![proto::ChannelEdge { + delete_edge: vec![proto::ChannelEdge { channel_id: channel_id.to_proto(), parent_id: from_parent.to_proto(), }], diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 09583fda29194a52b2f53b66fa8a7e0847dd6de9..aeb2f1adcc637c2f435178034e9b12397fa26656 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -910,11 +910,6 @@ async fn test_channel_moving( let channel_c_id = channels[2]; let channel_d_id = channels[3]; - dbg!(channel_a_id); - dbg!(channel_b_id); - dbg!(channel_c_id); - dbg!(channel_d_id); - // Current shape: // a - b - c - d assert_channels_list_shape( @@ -987,6 +982,7 @@ async fn test_channel_moving( let channel_ga_id = b_channels[1]; let channel_ep_id = b_channels[2]; + // Current shape for B: // /- ep // mu -- ga @@ -995,8 +991,8 @@ async fn test_channel_moving( cx_b, &[ (channel_mu_id, 0), + (channel_ep_id, 1), (channel_ga_id, 1), - (channel_ep_id, 1) ], ); @@ -1010,51 +1006,52 @@ async fn test_channel_moving( client_b.channel_store(), cx_b, &[ - // B's old channels - (channel_mu_id, 0), - (channel_ga_id, 1), - (channel_ep_id, 1), - // New channels from a (channel_b_id, 0), (channel_c_id, 1), - (channel_d_id, 1), (channel_d_id, 2), + (channel_d_id, 1), + + // B's old channels + (channel_mu_id, 0), + (channel_ep_id, 1), + (channel_ga_id, 1), + ], ); - // client_b - // .channel_store() - // .update(cx_a, |channel_store, cx| { - // channel_store.move_channel(channel_a_b_id, None, channel_b_epsilon_id, cx) - // }) - // .await - // .unwrap(); - - // // Current shape for B: - // // /---------\ - // // /- ep -- b -- c -- d - // // mu -- ga - // assert_channels_list_shape( - // client_b.channel_store(), - // cx_b, - // &[ - // // B's old channels - // (channel_b_mu_id, 0), - // (channel_b_gamma_id, 1), - // (channel_b_epsilon_id, 1), - - // // New channels from a, now under epsilon - // (channel_a_b_id, 2), - // (channel_a_c_id, 3), - // (channel_a_d_id, 3), - // (channel_a_d_id, 4), - // ], - // ); + client_b + .channel_store() + .update(cx_b, |channel_store, cx| { + channel_store.link_channel(channel_b_id, channel_ep_id, cx) + }) + .await + .unwrap(); + + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // B's old channels + (channel_mu_id, 0), + (channel_ga_id, 1), + (channel_ep_id, 1), + + // New channels from a, now under epsilon + (channel_b_id, 2), + (channel_c_id, 3), + (channel_d_id, 3), + (channel_d_id, 4), + ], + ); client_b .channel_store() - .update(cx_a, |channel_store, cx| { + .update(cx_b, |channel_store, cx| { channel_store.link_channel(channel_ga_id, channel_b_id, cx) }) .await diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 54414f38f4b7d50a094a2421576704331ffe1474..294f9a9706a54f458d5fb96d3a0b56c2f8e607ca 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -959,7 +959,7 @@ message LspDiskBasedDiagnosticsUpdated {} message UpdateChannels { repeated Channel channels = 1; - repeated ChannelEdge delete_channel_edge = 2; + repeated ChannelEdge delete_edge = 2; repeated uint64 delete_channels = 3; repeated Channel channel_invitations = 4; repeated uint64 remove_channel_invitations = 5; From d424e27164960af8f6d99884f889d8da8a32c3b3 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 10:08:07 -0700 Subject: [PATCH 15/26] Finish testing new channel store client behavior --- .../src/channel_store/channel_index.rs | 6 +- crates/collab/src/tests/channel_tests.rs | 83 ++++++++++++++----- 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 36e02f413483b07001660a64aa907aaa7a04b334..1d01d33050740dc4ea5db930f9fd3e763195ac8a 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -123,6 +123,7 @@ impl<'a> ChannelPathsEditGuard<'a> { } fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + debug_assert!(self.channels_by_id.contains_key(&parent_id)); let mut ix = 0; while ix < self.paths.len() { let path = &self.paths[ix]; @@ -131,8 +132,9 @@ impl<'a> ChannelPathsEditGuard<'a> { new_path.push(channel_id); self.paths.insert(ix + 1, ChannelPath(new_path.into())); ix += 2; - } else if path.len() == 1 && path[0] == channel_id { - self.paths.swap_remove(ix); + } else if path.get(0) == Some(&channel_id) { + // Clear out any paths that have this chahnnel as their root + self.paths.remove(ix); } else { ix += 1; } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index aeb2f1adcc637c2f435178034e9b12397fa26656..fbd73de87b84060052b215cbde26f9373edda3a1 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -780,7 +780,6 @@ async fn test_lost_channel_creation( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; @@ -953,7 +952,7 @@ async fn test_channel_moving( .await .unwrap(); - // Current shape: + // Current shape for A: // /------\ // a - b -- c -- d assert_channels_list_shape( @@ -982,21 +981,23 @@ async fn test_channel_moving( let channel_ga_id = b_channels[1]; let channel_ep_id = b_channels[2]; - // Current shape for B: // /- ep // mu -- ga assert_channels_list_shape( client_b.channel_store(), cx_b, - &[ - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_ga_id, 1), - ], + &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)], ); - client_a.add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a).await; + client_a + .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) + .await; + + client_b + .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) + .await; + // Current shape for B: // /- ep // mu -- ga @@ -1011,12 +1012,20 @@ async fn test_channel_moving( (channel_c_id, 1), (channel_d_id, 2), (channel_d_id, 1), - // B's old channels (channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1), + ], + ); + // Current shape for C: + // - ep + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[ + (channel_ep_id, 0), ], ); @@ -1036,16 +1045,28 @@ async fn test_channel_moving( client_b.channel_store(), cx_b, &[ - // B's old channels (channel_mu_id, 0), - (channel_ga_id, 1), (channel_ep_id, 1), - - // New channels from a, now under epsilon (channel_b_id, 2), (channel_c_id, 3), - (channel_d_id, 3), (channel_d_id, 4), + (channel_d_id, 3), + (channel_ga_id, 1), + ], + ); + + // Current shape for C: + // /---------\ + // ep -- b -- c -- d + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[ + (channel_ep_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), ], ); @@ -1066,34 +1087,50 @@ async fn test_channel_moving( client_b.channel_store(), cx_b, &[ - // B's old channels (channel_mu_id, 0), - (channel_ga_id, 1), (channel_ep_id, 1), - - // New channels from a, now under epsilon, with gamma (channel_b_id, 2), - (channel_ga_id, 3), (channel_c_id, 3), - (channel_d_id, 3), (channel_d_id, 4), + (channel_d_id, 3), + (channel_ga_id, 3), + (channel_ga_id, 1), ], ); // Current shape for A: + // /------\ + // a - b -- c -- d + // \-- ga assert_channels_list_shape( client_a.channel_store(), cx_a, &[ (channel_a_id, 0), (channel_b_id, 1), - (channel_ga_id, 1), (channel_c_id, 2), (channel_d_id, 3), (channel_d_id, 2), + (channel_ga_id, 2), + ], + ); + + // Current shape for C: + // /-------\ + // ep -- b -- c -- d + // \-- ga + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[ + (channel_ep_id, 0), + (channel_b_id, 1), + (channel_c_id, 2), + (channel_d_id, 3), + (channel_d_id, 2), + (channel_ga_id, 2), ], ); - // TODO: Make sure to test that non-local root removing problem I was thinking about } #[derive(Debug, PartialEq)] From 16707d16f6b37330a704b0b7458d2e92a9fab92a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 11:06:15 -0700 Subject: [PATCH 16/26] Improve context-menu behavior --- .../src/channel_store/channel_index.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 119 +++++++----------- 2 files changed, 45 insertions(+), 76 deletions(-) diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 1d01d33050740dc4ea5db930f9fd3e763195ac8a..31115854670842e23d276b2f1f4b8229dc68b927 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -134,7 +134,7 @@ impl<'a> ChannelPathsEditGuard<'a> { ix += 2; } else if path.get(0) == Some(&channel_id) { // Clear out any paths that have this chahnnel as their root - self.paths.remove(ix); + self.paths.swap_remove(ix); } else { ix += 1; } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4e889d35d49288615f082947a1138083aa9a898f..60372a16375e5436d11522a8adcd762d72ebb357 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -95,19 +95,19 @@ struct OpenChannelBuffer { channel_id: ChannelId, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct LinkChannel { +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct StartMoveChannel { channel_id: ChannelId, + parent_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct MoveChannel { - channel_id: ChannelId, - parent_id: Option, +struct LinkChannel { + to: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct PutChannel { +struct MoveChannel { to: ChannelId, } @@ -141,8 +141,8 @@ impl_actions!( JoinChannelCall, OpenChannelBuffer, LinkChannel, + StartMoveChannel, MoveChannel, - PutChannel, UnlinkChannel ] ); @@ -198,42 +198,40 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::open_channel_buffer); cx.add_action( - |panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext| { - panel.link_or_move = Some(ChannelCopy::Link(action.channel_id)); + |panel: &mut CollabPanel, action: &StartMoveChannel, _: &mut ViewContext| { + dbg!(action); + panel.channel_move = Some(*action); }, ); cx.add_action( - |panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext| { - panel.link_or_move = Some(ChannelCopy::Move { - channel_id: action.channel_id, - parent_id: action.parent_id, - }); + |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext| { + if let Some(move_start) = panel.channel_move.take() { + dbg!(action.to, &move_start); + panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .link_channel(move_start.channel_id, action.to, cx) + .detach_and_log_err(cx) + }) + } }, ); cx.add_action( - |panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext| { - if let Some(copy) = panel.link_or_move.take() { - match copy { - ChannelCopy::Move { - channel_id, - parent_id: Some(parent_id), - } => panel.channel_store.update(cx, |channel_store, cx| { + |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { + if let Some(move_start) = panel.channel_move.take() { + dbg!(&move_start, action.to); + panel.channel_store.update(cx, |channel_store, cx| { + if let Some(parent) = move_start.parent_id { channel_store - .move_channel(channel_id, parent_id, action.to, cx) + .move_channel(move_start.channel_id, parent, action.to, cx) .detach_and_log_err(cx) - }), - ChannelCopy::Link(channel) - | ChannelCopy::Move { - channel_id: channel, - parent_id: None, - } => panel.channel_store.update(cx, |channel_store, cx| { + } else { channel_store - .link_channel(channel, action.to, cx) + .link_channel(move_start.channel_id, action.to, cx) .detach_and_log_err(cx) - }), - } + } + }) } }, ); @@ -270,36 +268,11 @@ impl ChannelEditingState { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ChannelCopy { - Move { - channel_id: u64, - parent_id: Option, - }, - Link(u64), -} - -impl ChannelCopy { - fn channel_id(&self) -> u64 { - match self { - ChannelCopy::Move { channel_id, .. } => *channel_id, - ChannelCopy::Link(channel_id) => *channel_id, - } - } - - fn is_move(&self) -> bool { - match self { - ChannelCopy::Move { .. } => true, - ChannelCopy::Link(_) => false, - } - } -} - pub struct CollabPanel { width: Option, fs: Arc, has_focus: bool, - link_or_move: Option, + channel_move: Option, pending_serialization: Task>, context_menu: ViewHandle, filter_editor: ViewHandle, @@ -562,7 +535,7 @@ impl CollabPanel { let mut this = Self { width: None, has_focus: false, - link_or_move: None, + channel_move: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), @@ -2071,13 +2044,13 @@ impl CollabPanel { ) { self.context_menu_on_selected = position.is_none(); - let operation_details = self.link_or_move.as_ref().and_then(|link_or_move| { + let channel_name = self.channel_move.as_ref().and_then(|channel| { let channel_name = self .channel_store .read(cx) - .channel_for_id(link_or_move.channel_id()) + .channel_for_id(channel.channel_id) .map(|channel| channel.name.clone())?; - Some((channel_name, link_or_move.is_move())) + Some(channel_name) }); self.context_menu.update(cx, |context_menu, cx| { @@ -2089,14 +2062,16 @@ impl CollabPanel { let mut items = Vec::new(); - if let Some((channel_name, is_move)) = operation_details { + if let Some(channel_name) = channel_name { items.push(ContextMenuItem::action( - format!( - "{} '#{}' here", - if is_move { "Move" } else { "Link" }, - channel_name - ), - PutChannel { + format!("Move '#{}' here", channel_name), + MoveChannel { + to: location.channel, + }, + )); + items.push(ContextMenuItem::action( + format!("Link '#{}' here", channel_name), + LinkChannel { to: location.channel, }, )); @@ -2155,15 +2130,9 @@ impl CollabPanel { } items.extend([ - ContextMenuItem::action( - "Link this channel", - LinkChannel { - channel_id: location.channel, - }, - ), ContextMenuItem::action( "Move this channel", - MoveChannel { + StartMoveChannel { channel_id: location.channel, parent_id, }, From f9fff3a7b2a84312c9979993ede44cb82582ebf4 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 11:19:40 -0700 Subject: [PATCH 17/26] fmt --- crates/channel/src/channel_store.rs | 9 +- crates/collab/src/db/queries/channels.rs | 9 +- crates/collab/src/db/tests/channel_tests.rs | 243 ++++++++++-------- crates/collab/src/db/tests/db_tests.rs | 4 +- .../collab/src/tests/channel_buffer_tests.rs | 14 +- crates/collab/src/tests/channel_tests.rs | 8 +- crates/collab/src/tests/test_server.rs | 11 +- crates/collab_ui/src/collab_panel.rs | 16 +- 8 files changed, 166 insertions(+), 148 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index b37f918052a820c98e6eb206ac78645ab62fbd8c..eec727c6dcc3cfb41104606f361a60196b5e9a7a 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -747,7 +747,9 @@ impl ChannelStore { } } - let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() || !payload.delete_edge.is_empty(); + let channels_changed = !payload.channels.is_empty() + || !payload.delete_channels.is_empty() + || !payload.delete_edge.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { self.channel_index.delete_channels(&payload.delete_channels); @@ -775,13 +777,10 @@ impl ChannelStore { } for edge in payload.delete_edge { - index_edit - .delete_edge(edge.parent_id, edge.channel_id); + index_edit.delete_edge(edge.parent_id, edge.channel_id); } } - - for permission in payload.channel_permissions { if permission.is_admin { self.channels_with_admin_privileges diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 90374e76b20be7de62ddb438960a3832a11c093c..de464c17562e7f8ab5a586a4ed404f20fe0d571a 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -414,8 +414,9 @@ impl Database { .filter_map(|membership| membership.admin.then_some(membership.channel_id)) .collect(); - let channels = self.get_channels_internal(parents_by_child_id, true, &tx).await?; - + let channels = self + .get_channels_internal(parents_by_child_id, true, &tx) + .await?; #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { @@ -877,7 +878,9 @@ impl Database { channel.insert(to); } - let channels = self.get_channels_internal(from_descendants, false, &*tx).await?; + let channels = self + .get_channels_internal(from_descendants, false, &*tx) + .await?; Ok(channels) } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 50faa2a91081708fe727be51f1023f94c4f345b2..f82ae7d7737bf805fe884e4dea177a3added5008 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -535,14 +535,17 @@ async fn test_channels_moving(db: &Arc) { // zed -- crdb - livestreaming - livestreaming_dag // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + ], + ); // ======================================================================== // Create a new channel below a channel with multiple parents @@ -561,15 +564,18 @@ async fn test_channels_moving(db: &Arc) { // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Test a complex DAG by making another link @@ -595,16 +601,19 @@ async fn test_channels_moving(db: &Arc) { ); let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Test a complex DAG by making another link @@ -646,26 +655,24 @@ async fn test_channels_moving(db: &Arc) { ); let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Test unlinking in a complex DAG by removing the inner link - db - .unlink_channel( - a_id, - livestreaming_dag_sub_id, - livestreaming_id, - ) + db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) .await .unwrap(); @@ -675,16 +682,19 @@ async fn test_channels_moving(db: &Arc) { // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Test unlinking in a complex DAG by removing the inner link @@ -697,15 +707,18 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Test moving DAG nodes by moving livestreaming to be below gpui2 @@ -718,15 +731,18 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb / // \---------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (gpui2_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(gpui2_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Deleting a channel should not delete children that still have other parents @@ -736,19 +752,20 @@ async fn test_channels_moving(db: &Arc) { // zed - crdb // \- livestreaming - livestreaming_dag - livestreaming_dag_sub let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Unlinking a channel from it's parent should automatically promote it to a root channel - db.unlink_channel(a_id, crdb_id, zed_id) - .await - .unwrap(); + db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap(); // DAG is now: // crdb @@ -756,33 +773,36 @@ async fn test_channels_moving(db: &Arc) { // \- livestreaming - livestreaming_dag - livestreaming_dag_sub let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, None), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, None), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // You should be able to move a root channel into a non-root channel - db.link_channel(a_id, crdb_id, zed_id) - .await - .unwrap(); + db.link_channel(a_id, crdb_id, zed_id).await.unwrap(); // DAG is now: // zed - crdb // \- livestreaming - livestreaming_dag - livestreaming_dag_sub let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); - + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // ======================================================================== // Prep for DAG deletion test @@ -795,22 +815,23 @@ async fn test_channels_moving(db: &Arc) { // \--------/ let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag(result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ]); + assert_dag( + result.channels, + &[ + (zed_id, None), + (crdb_id, Some(zed_id)), + (livestreaming_id, Some(zed_id)), + (livestreaming_id, Some(crdb_id)), + (livestreaming_dag_id, Some(livestreaming_id)), + (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + ], + ); // Deleting the parent of a DAG should delete the whole DAG: 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() - ) + assert!(result.channels.is_empty()) } #[track_caller] diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index b710d6460bb3c12223e22c8d7818c8952e251321..0e6a0529c4f72636069d5e1dab43db05d0f4de9c 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -794,11 +794,11 @@ async fn test_joining_channels(db: &Arc) { github_login: "user2".into(), github_user_id: 6, invite_count: 0, - }, ) .await - .unwrap() .user_id; + .unwrap() + .user_id; let channel_1 = db .create_root_channel("channel_1", "1", user_1) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 5cfff053cb7176887ccdc704f970835416d28500..ba5a70895a693a3cff33f2cced0a5068acfcba93 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -342,7 +342,12 @@ async fn test_channel_buffer_disconnect( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) .await; let channel_buffer_a = client_a @@ -412,7 +417,12 @@ async fn test_rejoin_channel_buffer( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) .await; let channel_buffer_a = client_a diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index fbd73de87b84060052b215cbde26f9373edda3a1..551d6ee3bc8be806f8fde0b36f30d7c7a81a5979 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1021,13 +1021,7 @@ async fn test_channel_moving( // Current shape for C: // - ep - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[ - (channel_ep_id, 0), - ], - ); + assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); client_b .channel_store() diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 7f1a91f426f7847ec371cf75c6e83bb639d068b0..6572722df347ba72e50e04c1a70d58b63b8484da 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -592,16 +592,10 @@ impl TestClient { ) { let (other_client, other_cx) = user; - self - .app_state + self.app_state .channel_store .update(cx_self, |channel_store, cx| { - channel_store.invite_member( - channel, - other_client.user_id().unwrap(), - true, - cx, - ) + channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx) }) .await .unwrap(); @@ -616,7 +610,6 @@ impl TestClient { }) .await .unwrap(); - } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 60372a16375e5436d11522a8adcd762d72ebb357..d9c28b9049af55af0d8c3819004736cdb8651fd8 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2129,15 +2129,13 @@ impl CollabPanel { )); } - items.extend([ - ContextMenuItem::action( - "Move this channel", - StartMoveChannel { - channel_id: location.channel, - parent_id, - }, - ), - ]); + items.extend([ContextMenuItem::action( + "Move this channel", + StartMoveChannel { + channel_id: location.channel, + parent_id, + }, + )]); items.extend([ ContextMenuItem::Separator, From 54006054833b6b8b55beadd2cf03550c49d0adb4 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 12:30:26 -0700 Subject: [PATCH 18/26] Fix merge conflicts --- crates/channel/src/channel.rs | 4 ++- crates/channel/src/channel_store.rs | 27 +++++++++++++++++-- .../src/channel_store/channel_index.rs | 26 ++---------------- crates/collab/src/db/tests/db_tests.rs | 4 +-- crates/collab/src/rpc.rs | 4 +-- .../collab/src/tests/channel_message_tests.rs | 15 +++++++++-- crates/collab_ui/src/chat_panel.rs | 4 +-- crates/collab_ui/src/collab_panel.rs | 6 ++--- 8 files changed, 51 insertions(+), 39 deletions(-) diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 37f1c0ce44ba8a8f3a86247ea411ba0d2b669f7d..d3e2f8956435727502f83f00517fdb29ac61eb45 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -4,7 +4,9 @@ mod channel_store; pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent}; pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; -pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; +pub use channel_store::{ + Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, +}; use client::Client; use std::sync::Arc; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index eec727c6dcc3cfb41104606f361a60196b5e9a7a..208247aca557f1ffa3a766488780192c8ca83121 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -7,11 +7,11 @@ use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; -use std::{mem, sync::Arc, time::Duration}; +use serde_derive::{Deserialize, Serialize}; +use std::{mem, ops::Deref, sync::Arc, time::Duration}; use util::ResultExt; use self::channel_index::ChannelIndex; -pub use self::channel_index::ChannelPath; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -40,6 +40,29 @@ pub struct Channel { pub name: String, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct ChannelPath(Arc<[ChannelId]>); + +impl Deref for ChannelPath { + type Target = [ChannelId]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ChannelPath { + pub fn parent_id(&self) -> Option { + self.0.len().checked_sub(2).map(|i| self.0[i]) + } +} + +impl Default for ChannelPath { + fn default() -> Self { + ChannelPath(Arc::from([])) + } +} + pub struct ChannelMembership { pub user: Arc, pub kind: proto::channel_member::Kind, diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 31115854670842e23d276b2f1f4b8229dc68b927..dd911ab363e3cccca75014b0edea8e2ffa10b0e6 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -2,34 +2,12 @@ use std::{ops::Deref, sync::Arc}; use collections::HashMap; use rpc::proto; -use serde_derive::{Deserialize, Serialize}; use crate::{Channel, ChannelId}; -pub type ChannelsById = HashMap>; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] -pub struct ChannelPath(Arc<[ChannelId]>); - -impl Deref for ChannelPath { - type Target = [ChannelId]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} +use super::ChannelPath; -impl ChannelPath { - pub fn parent_id(&self) -> Option { - self.0.len().checked_sub(2).map(|i| self.0[i]) - } -} - -impl Default for ChannelPath { - fn default() -> Self { - ChannelPath(Arc::from([])) - } -} +pub type ChannelsById = HashMap>; #[derive(Default, Debug)] pub struct ChannelIndex { diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 0e6a0529c4f72636069d5e1dab43db05d0f4de9c..d5e3349d4755c49f4767a072a54efaabdef8398c 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -750,11 +750,11 @@ async fn test_channels(db: &Arc) { ); // 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]); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1d89c6fe42ae50161bfa2cfd02b112da5472e9f9..0447813356402a2f667c08cdc8206b17ec377c80 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,8 @@ mod connection_pool; use crate::{ auth, db::{ - self, Channel, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, - UserId, + self, Channel, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, + ServerId, User, UserId, }, executor::Executor, AppState, Result, diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 1a9460c6cfb00f931ef4d0fb4ea67dc690135dd7..58494c538b4a57c4ee8117136b77df190a8744f9 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -15,7 +15,12 @@ async fn test_basic_channel_messages( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) .await; let channel_chat_a = client_a @@ -68,7 +73,12 @@ async fn test_rejoin_channel_chat( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) .await; let channel_chat_a = client_a @@ -139,6 +149,7 @@ async fn test_remove_channel_message( let channel_id = server .make_channel( "the-channel", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 087f2e1b8e5e9e2c95aebe9871ee1d86f9a95b8e..4200ada36bfa939f4fc845ab039b8edbae5e5472 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -167,7 +167,7 @@ impl ChatPanel { .channel_store .read(cx) .channel_at_index(selected_ix) - .map(|e| e.1.id); + .map(|e| e.0.id); if let Some(selected_channel_id) = selected_channel_id { this.select_channel(selected_channel_id, cx) .detach_and_log_err(cx); @@ -391,7 +391,7 @@ impl ChatPanel { (ItemType::Unselected, true) => &theme.channel_select.hovered_item, }; - let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().1; + let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().0; let channel_id = channel.id; let mut row = Flex::row() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d9c28b9049af55af0d8c3819004736cdb8651fd8..2ed2a1a230cd9d1e3bcbc2a453feacc9bab36e9f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -5,13 +5,12 @@ use crate::{ channel_view::{self, ChannelView}, chat_panel::ChatPanel, face_pile::FacePile, - CollaborationPanelSettings, + panel_settings, CollaborationPanelSettings, }; use anyhow::Result; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath}; -use channel_modal::ChannelModal; use channel::{Channel, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; +use channel_modal::ChannelModal; use client::{proto::PeerId, Client, Contact, User, UserStore}; use contact_finder::ContactFinder; use context_menu::{ContextMenu, ContextMenuItem}; @@ -195,7 +194,6 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::collapse_selected_channel); cx.add_action(CollabPanel::expand_selected_channel); cx.add_action(CollabPanel::open_channel_notes); - cx.add_action(CollabPanel::open_channel_buffer); cx.add_action( |panel: &mut CollabPanel, action: &StartMoveChannel, _: &mut ViewContext| { From 52057c5619f7f3bfa1d334315fbb3d0f24b33166 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 13:18:50 -0700 Subject: [PATCH 19/26] Simplify path representation in collab panel Optimize set representation in collab --- Cargo.lock | 1 + crates/channel/src/channel_store.rs | 71 ++++-- .../src/channel_store/channel_index.rs | 4 +- crates/collab/Cargo.toml | 1 + crates/collab/src/db/queries/channels.rs | 45 +++- crates/collab_ui/src/collab_panel.rs | 212 ++++++------------ 6 files changed, 161 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 829ed18bbbf8334364902ce382354ab615922594..327ca26937d78970a4a09c74f0a9854da51214ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1505,6 +1505,7 @@ dependencies = [ "serde_json", "settings", "sha-1 0.9.8", + "smallvec", "sqlx", "text", "theme", diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 208247aca557f1ffa3a766488780192c8ca83121..702679fdda3aa13055ad7ccef19fea96372b14f5 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,12 +3,12 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; -use collections::{hash_map, HashMap, HashSet}; +use collections::{hash_map::{self, DefaultHasher}, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use serde_derive::{Deserialize, Serialize}; -use std::{mem, ops::Deref, sync::Arc, time::Duration}; +use std::{mem, ops::Deref, sync::Arc, time::Duration, borrow::Cow, hash::{Hash, Hasher}}; use util::ResultExt; use self::channel_index::ChannelIndex; @@ -43,26 +43,6 @@ pub struct Channel { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] pub struct ChannelPath(Arc<[ChannelId]>); -impl Deref for ChannelPath { - type Target = [ChannelId]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ChannelPath { - pub fn parent_id(&self) -> Option { - self.0.len().checked_sub(2).map(|i| self.0[i]) - } -} - -impl Default for ChannelPath { - fn default() -> Self { - ChannelPath(Arc::from([])) - } -} - pub struct ChannelMembership { pub user: Arc, pub kind: proto::channel_member::Kind, @@ -860,3 +840,50 @@ impl ChannelStore { })) } } + +impl Deref for ChannelPath { + type Target = [ChannelId]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ChannelPath { + pub fn new(path: Arc<[ChannelId]>) -> Self { + debug_assert!(path.len() >= 1); + Self(path) + } + + pub fn parent_id(&self) -> Option { + self.0.len().checked_sub(2).map(|i| self.0[i]) + } + + pub fn channel_id(&self) -> ChannelId { + self.0[self.0.len() - 1] + } + + pub fn unique_id(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.0.deref().hash(&mut hasher); + hasher.finish() + } +} + +impl From for Cow<'static, ChannelPath> { + fn from(value: ChannelPath) -> Self { + Cow::Owned(value) + } +} + +impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> { + fn from(value: &'a ChannelPath) -> Self { + Cow::Borrowed(value) + } +} + +impl Default for ChannelPath { + fn default() -> Self { + ChannelPath(Arc::from([])) + } +} diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index dd911ab363e3cccca75014b0edea8e2ffa10b0e6..f7d9e873ae5eb99e533d85e05951689ae588a740 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -108,7 +108,7 @@ impl<'a> ChannelPathsEditGuard<'a> { if path.ends_with(&[parent_id]) { let mut new_path = path.to_vec(); new_path.push(channel_id); - self.paths.insert(ix + 1, ChannelPath(new_path.into())); + self.paths.insert(ix + 1, ChannelPath::new(new_path.into())); ix += 2; } else if path.get(0) == Some(&channel_id) { // Clear out any paths that have this chahnnel as their root @@ -120,7 +120,7 @@ impl<'a> ChannelPathsEditGuard<'a> { } fn insert_root(&mut self, channel_id: ChannelId) { - self.paths.push(ChannelPath(Arc::from([channel_id]))); + self.paths.push(ChannelPath::new(Arc::from([channel_id]))); } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index c580e911bcf67fa55952380d00fc992762f14ce8..57db1cd761e71257b65e13aaaf95f01df0bc85d2 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -41,6 +41,7 @@ prost.workspace = true rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" +smallvec.workspace = true # Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released. sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] } sea-query = "0.27" diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index de464c17562e7f8ab5a586a4ed404f20fe0d571a..dab392c583d82f82b95ac709d07ba73e43c77e1a 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,6 +1,8 @@ +use smallvec::SmallVec; + use super::*; -type ChannelDescendants = HashMap>; +type ChannelDescendants = HashMap>; impl Database { #[cfg(test)] @@ -150,7 +152,7 @@ impl Database { .exec(&*tx) .await?; - // Delete any other paths that incldue this channel + // Delete any other paths that include this channel let sql = r#" DELETE FROM channel_paths WHERE @@ -351,7 +353,7 @@ impl Database { let parents = parents_by_child_id.get(&row.id).unwrap(); if parents.len() > 0 { let mut added_channel = false; - for parent in parents { + for parent in parents.iter() { // Trim out any dangling parent pointers. // That the user doesn't have access to if trim_dangling_parents { @@ -843,7 +845,7 @@ impl Database { ); tx.execute(channel_paths_stmt).await?; for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&channel) { - for to_id in to_ids { + for to_id in to_ids.iter() { let channel_paths_stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, @@ -979,3 +981,38 @@ impl Database { enum QueryUserIds { UserId, } + +struct SmallSet(SmallVec<[T; 1]>); + +impl Deref for SmallSet { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl Default for SmallSet { + fn default() -> Self { + Self(SmallVec::new()) + } +} + +impl SmallSet { + fn insert(&mut self, value: T) -> bool + where + T: Ord, + { + match self.binary_search(&value) { + Ok(_) => false, + Err(ix) => { + self.0.insert(ix, value); + true + }, + } + } + + fn clear(&mut self) { + self.0.clear(); + } +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 2ed2a1a230cd9d1e3bcbc2a453feacc9bab36e9f..2fe6148fe503489939b6fd49db8614ba29a14a8c 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -50,33 +50,33 @@ use workspace::{ }; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct RemoveChannel { - channel_id: ChannelId, +struct ToggleCollapse { + location: ChannelPath, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct ToggleCollapse { - location: ChannelLocation<'static>, +struct NewChannel { + location: ChannelPath, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct NewChannel { - location: ChannelLocation<'static>, +struct RenameChannel { + location: ChannelPath, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct InviteMembers { +struct RemoveChannel { channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct ManageMembers { +struct InviteMembers { channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct RenameChannel { - location: ChannelLocation<'static>, +struct ManageMembers { + channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -148,30 +148,6 @@ impl_actions!( const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct ChannelLocation<'a> { - channel: ChannelId, - path: Cow<'a, ChannelPath>, -} - -impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> { - fn from(value: (ChannelId, ChannelPath)) -> Self { - ChannelLocation { - channel: value.0, - path: Cow::Owned(value.1), - } - } -} - -impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> { - fn from(value: (ChannelId, &'a ChannelPath)) -> Self { - ChannelLocation { - channel: value.0, - path: Cow::Borrowed(value.1), - } - } -} - pub fn init(cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); @@ -190,7 +166,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::manage_members); cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); - cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::toggle_channel_collapsed_action); cx.add_action(CollabPanel::collapse_selected_channel); cx.add_action(CollabPanel::expand_selected_channel); cx.add_action(CollabPanel::open_channel_notes); @@ -248,11 +224,11 @@ pub fn init(cx: &mut AppContext) { #[derive(Debug)] pub enum ChannelEditingState { Create { - location: Option>, + location: Option, pending_name: Option, }, Rename { - location: ChannelLocation<'static>, + location: ChannelPath, pending_name: Option, }, } @@ -286,7 +262,7 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, - collapsed_channels: Vec>, + collapsed_channels: Vec, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -294,7 +270,7 @@ pub struct CollabPanel { #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, - collapsed_channels: Option>>, + collapsed_channels: Option>, } #[derive(Debug)] @@ -826,15 +802,13 @@ impl CollabPanel { let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap(); let depth = path.len() - 1; - let location: ChannelLocation<'_> = (channel.id, path).into(); - - if collapse_depth.is_none() && self.is_channel_collapsed(&location) { + if collapse_depth.is_none() && self.is_channel_collapsed(path) { collapse_depth = Some(depth); } else if let Some(collapsed_depth) = collapse_depth { if depth > collapsed_depth { continue; } - if self.is_channel_collapsed(&location) { + if self.is_channel_collapsed(path) { collapse_depth = Some(depth); } else { collapse_depth = None; @@ -843,9 +817,9 @@ impl CollabPanel { match &self.channel_editing_state { Some(ChannelEditingState::Create { - location: parent_id, + location: parent_path, .. - }) if *parent_id == Some(location) => { + }) if parent_path.as_ref() == Some(path) => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, @@ -854,10 +828,10 @@ impl CollabPanel { self.entries .push(ListEntry::ChannelEditor { depth: depth + 1 }); } - Some(ChannelEditingState::Rename { location, .. }) - if location.channel == channel.id - && location.path == Cow::Borrowed(path) => - { + Some(ChannelEditingState::Rename { + location: parent_path, + .. + }) if parent_path == path => { self.entries.push(ListEntry::ChannelEditor { depth }); } _ => { @@ -1674,13 +1648,7 @@ impl CollabPanel { let channel_id = channel.id; let has_children = self.channel_store.read(cx).has_children(channel_id); - let disclosed = { - let location = ChannelLocation { - channel: channel_id, - path: Cow::Borrowed(&path), - }; - has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok()) - }; + let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); let is_active = iife!({ let call_channel = ActiveCall::global(cx) @@ -1696,7 +1664,7 @@ impl CollabPanel { enum ChannelCall {} - MouseEventHandler::new::(id(&path) as usize, cx, |state, cx| { + MouseEventHandler::new::(path.unique_id() as usize, cx, |state, cx| { let row_hovered = state.hovered(); Flex::::row() @@ -1767,10 +1735,10 @@ impl CollabPanel { .disclosable( disclosed, Box::new(ToggleCollapse { - location: (channel_id, path.clone()).into(), + location: path.clone(), }), ) - .with_id(id(&path) as usize) + .with_id(path.unique_id() as usize) .with_style(theme.disclosure.clone()) .element() .constrained() @@ -1786,11 +1754,7 @@ impl CollabPanel { this.join_channel_chat(channel_id, cx); }) .on_click(MouseButton::Right, move |e, this, cx| { - this.deploy_channel_context_menu( - Some(e.position), - &(channel_id, path.clone()).into(), - cx, - ); + this.deploy_channel_context_menu(Some(e.position), &path, cx); }) .with_cursor_style(CursorStyle::PointingHand) .into_any() @@ -2037,7 +2001,7 @@ impl CollabPanel { fn deploy_channel_context_menu( &mut self, position: Option, - location: &ChannelLocation<'static>, + path: &ChannelPath, cx: &mut ViewContext, ) { self.context_menu_on_selected = position.is_none(); @@ -2063,20 +2027,16 @@ impl CollabPanel { if let Some(channel_name) = channel_name { items.push(ContextMenuItem::action( format!("Move '#{}' here", channel_name), - MoveChannel { - to: location.channel, - }, + MoveChannel { to: path.channel_id() }, )); items.push(ContextMenuItem::action( format!("Link '#{}' here", channel_name), - LinkChannel { - to: location.channel, - }, + LinkChannel { to: path.channel_id() }, )); items.push(ContextMenuItem::Separator) } - let expand_action_name = if self.is_channel_collapsed(&location) { + let expand_action_name = if self.is_channel_collapsed(&path) { "Expand Subchannels" } else { "Collapse Subchannels" @@ -2086,32 +2046,27 @@ impl CollabPanel { ContextMenuItem::action( expand_action_name, ToggleCollapse { - location: location.clone(), - }, - ), - ContextMenuItem::action( - "Open Notes", - OpenChannelBuffer { - channel_id: location.channel, + location: path.clone(), }, ), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: path.channel_id() }), ]); - if self.channel_store.read(cx).is_user_admin(location.channel) { - let parent_id = location.path.parent_id(); + if self.channel_store.read(cx).is_user_admin(path.channel_id()) { + let parent_id = path.parent_id(); items.extend([ ContextMenuItem::Separator, ContextMenuItem::action( "New Subchannel", NewChannel { - location: location.clone(), + location: path.clone(), }, ), ContextMenuItem::action( "Rename", RenameChannel { - location: location.clone(), + location: path.clone(), }, ), ContextMenuItem::Separator, @@ -2121,7 +2076,7 @@ impl CollabPanel { items.push(ContextMenuItem::action( "Unlink from parent", UnlinkChannel { - channel_id: location.channel, + channel_id: path.channel_id(), parent_id, }, )); @@ -2130,7 +2085,7 @@ impl CollabPanel { items.extend([ContextMenuItem::action( "Move this channel", StartMoveChannel { - channel_id: location.channel, + channel_id: path.channel_id(), parent_id, }, )]); @@ -2140,20 +2095,20 @@ impl CollabPanel { ContextMenuItem::action( "Invite Members", InviteMembers { - channel_id: location.channel, + channel_id: path.channel_id(), }, ), ContextMenuItem::action( "Manage Members", ManageMembers { - channel_id: location.channel, + channel_id: path.channel_id(), }, ), ContextMenuItem::Separator, ContextMenuItem::action( "Delete", RemoveChannel { - channel_id: location.channel, + channel_id: path.channel_id(), }, ), ]); @@ -2296,7 +2251,7 @@ impl CollabPanel { .update(cx, |channel_store, cx| { channel_store.create_channel( &channel_name, - location.as_ref().map(|location| location.channel), + location.as_ref().map(|location| location.channel_id()), cx, ) }) @@ -2315,7 +2270,7 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.rename(location.channel, &channel_name, cx) + channel_store.rename(location.channel_id(), &channel_name, cx) }) .detach(); cx.notify(); @@ -2342,58 +2297,48 @@ impl CollabPanel { _: &CollapseSelectedChannel, cx: &mut ViewContext, ) { - let Some((channel_id, path)) = self + let Some((_, path)) = self .selected_channel() .map(|(channel, parent)| (channel.id, parent)) else { return; }; - let path = path.to_owned(); - - if self.is_channel_collapsed(&(channel_id, path.clone()).into()) { + if self.is_channel_collapsed(&path) { return; } - self.toggle_channel_collapsed( - &ToggleCollapse { - location: (channel_id, path).into(), - }, - cx, - ) + self.toggle_channel_collapsed(&path.clone(), cx); + } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - let Some((channel_id, path)) = self + let Some((_, path)) = self .selected_channel() .map(|(channel, parent)| (channel.id, parent)) else { return; }; - let path = path.to_owned(); - - if !self.is_channel_collapsed(&(channel_id, path.clone()).into()) { + if !self.is_channel_collapsed(&path) { return; } - self.toggle_channel_collapsed( - &ToggleCollapse { - location: (channel_id, path).into(), - }, - cx, - ) + self.toggle_channel_collapsed(path.to_owned(), cx) } - fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { - let location = action.location.clone(); + fn toggle_channel_collapsed_action(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { + self.toggle_channel_collapsed(&action.location, cx); + } - match self.collapsed_channels.binary_search(&location) { + fn toggle_channel_collapsed<'a>(&mut self, path: impl Into>, cx: &mut ViewContext) { + let path = path.into(); + match self.collapsed_channels.binary_search(&path) { Ok(ix) => { self.collapsed_channels.remove(ix); } Err(ix) => { - self.collapsed_channels.insert(ix, location); + self.collapsed_channels.insert(ix, path.into_owned()); } }; self.serialize(cx); @@ -2402,8 +2347,8 @@ impl CollabPanel { cx.focus_self(); } - fn is_channel_collapsed(&self, location: &ChannelLocation) -> bool { - self.collapsed_channels.binary_search(location).is_ok() + fn is_channel_collapsed(&self, path: &ChannelPath) -> bool { + self.collapsed_channels.binary_search(path).is_ok() } fn leave_call(cx: &mut ViewContext) { @@ -2472,10 +2417,10 @@ impl CollabPanel { } fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - if let Some((channel, parent)) = self.selected_channel() { + if let Some((_, parent)) = self.selected_channel() { self.rename_channel( &RenameChannel { - location: (channel.id, parent.to_owned()).into(), + location: parent.to_owned(), }, cx, ); @@ -2484,11 +2429,11 @@ impl CollabPanel { fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); - if !channel_store.is_user_admin(action.location.channel) { + if !channel_store.is_user_admin(action.location.channel_id()) { return; } if let Some(channel) = channel_store - .channel_for_id(action.location.channel) + .channel_for_id(action.location.channel_id()) .cloned() { self.channel_editing_state = Some(ChannelEditingState::Rename { @@ -2512,11 +2457,11 @@ impl CollabPanel { } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { - let Some((channel, path)) = self.selected_channel() else { + let Some((_, path)) = self.selected_channel() else { return; }; - self.deploy_channel_context_menu(None, &(channel.id, path.to_owned()).into(), cx); + self.deploy_channel_context_menu(None, &path.to_owned(), cx); } fn selected_channel(&self) -> Option<(&Arc, &ChannelPath)> { @@ -2983,26 +2928,3 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } - -/// Hash a channel path to a u64, for use as a mouse id -/// Based on the Fowler–Noll–Vo hash: -/// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function -fn id(path: &[ChannelId]) -> u64 { - // I probably should have done this, but I didn't - // let hasher = DefaultHasher::new(); - // let path = path.hash(&mut hasher); - // let x = hasher.finish(); - - const OFFSET: u64 = 14695981039346656037; - const PRIME: u64 = 1099511628211; - - let mut hash = OFFSET; - for id in path.iter() { - for id in id.to_ne_bytes() { - hash = hash ^ (id as u64); - hash = (hash as u128 * PRIME as u128) as u64; - } - } - - hash -} From 363867c65b5f1b6b75cf16b6714d776c98e27357 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 13:44:01 -0700 Subject: [PATCH 20/26] Make DAG tests order independent --- crates/collab/src/db/tests/channel_tests.rs | 28 ++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index f82ae7d7737bf805fe884e4dea177a3added5008..75db9d619cdb735408b174ff7c36858556f17a1a 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1,3 +1,4 @@ +use collections::{HashMap, HashSet}; use rpc::{proto, ConnectionId}; use crate::{ @@ -459,12 +460,12 @@ async fn test_channel_renames(db: &Arc) { } test_both_dbs!( - test_channels_moving, + test_db_channel_moving, test_channels_moving_postgres, test_channels_moving_sqlite ); -async fn test_channels_moving(db: &Arc) { +async fn test_db_channel_moving(db: &Arc) { let a_id = db .create_user( "user1@example.com", @@ -661,9 +662,9 @@ async fn test_channels_moving(db: &Arc) { (zed_id, None), (crdb_id, Some(zed_id)), (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), (livestreaming_id, Some(zed_id)), (livestreaming_id, Some(crdb_id)), + (livestreaming_id, Some(gpui2_id)), (livestreaming_dag_id, Some(livestreaming_id)), (livestreaming_dag_sub_id, Some(livestreaming_id)), (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), @@ -836,10 +837,25 @@ async fn test_channels_moving(db: &Arc) { #[track_caller] fn assert_dag(actual: Vec, expected: &[(ChannelId, Option)]) { + /// This is used to allow tests to be ordering independent + fn make_parents_map(association_table: impl IntoIterator)>) -> HashMap> { + let mut map: HashMap> = HashMap::default(); + + for (child, parent) in association_table { + let entry = map.entry(child).or_default(); + if let Some(parent) = parent { + entry.insert(parent); + } + } + + map + } let actual = actual .iter() - .map(|channel| (channel.id, channel.parent_id)) - .collect::>(); + .map(|channel| (channel.id, channel.parent_id)); + + let actual_map = make_parents_map(actual); + let expected_map = make_parents_map(expected.iter().copied()); - pretty_assertions::assert_eq!(actual, expected) + pretty_assertions::assert_eq!(actual_map, expected_map) } From 5f9c56c8b08b61dd8326452b68d221561fa29f0e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 15 Sep 2023 17:57:23 -0700 Subject: [PATCH 21/26] WIP: Send the channel name and the channel edges seperately, so we're not repeating them constantly This commit is currently broken and includes debug data for a failed attempt at rewriting the insert_edge logic --- crates/channel/src/channel_store.rs | 54 ++- .../src/channel_store/channel_index.rs | 84 +++- crates/channel/src/channel_store_tests.rs | 29 +- crates/collab/src/db.rs | 9 +- crates/collab/src/db/queries/channels.rs | 200 +++++--- crates/collab/src/db/tests.rs | 25 + crates/collab/src/db/tests/channel_tests.rs | 212 ++++---- crates/collab/src/db/tests/db_tests.rs | 452 ------------------ crates/collab/src/rpc.rs | 66 +-- crates/collab/src/tests/channel_tests.rs | 16 +- crates/collab_ui/src/collab_panel.rs | 3 - crates/rpc/proto/zed.proto | 26 +- crates/rpc/src/proto.rs | 7 +- 13 files changed, 430 insertions(+), 753 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 702679fdda3aa13055ad7ccef19fea96372b14f5..8a3875b2b966abd7346110489a29ed38814891e8 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,12 +3,25 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; -use collections::{hash_map::{self, DefaultHasher}, HashMap, HashSet}; +use collections::{ + hash_map::{self, DefaultHasher}, + HashMap, HashSet, +}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; -use rpc::{proto, TypedEnvelope}; +use rpc::{ + proto::{self, ChannelEdge, ChannelPermission}, + TypedEnvelope, +}; use serde_derive::{Deserialize, Serialize}; -use std::{mem, ops::Deref, sync::Arc, time::Duration, borrow::Cow, hash::{Hash, Hasher}}; +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, + mem, + ops::Deref, + sync::Arc, + time::Duration, +}; use util::ResultExt; use self::channel_index::ChannelIndex; @@ -301,18 +314,33 @@ impl ChannelStore { let client = self.client.clone(); let name = name.trim_start_matches("#").to_owned(); cx.spawn(|this, mut cx| async move { - let channel = client + let response = client .request(proto::CreateChannel { name, parent_id }) - .await? + .await?; + + let channel = response .channel .ok_or_else(|| anyhow!("missing channel in response"))?; - let channel_id = channel.id; + let parent_edge = if let Some(parent_id) = parent_id { + vec![ChannelEdge { + channel_id: channel.id, + parent_id, + }] + } else { + vec![] + }; + this.update(&mut cx, |this, cx| { let task = this.update_channels( proto::UpdateChannels { channels: vec![channel], + insert_edge: parent_edge, + channel_permissions: vec![ChannelPermission { + channel_id, + is_admin: true, + }], ..Default::default() }, cx, @@ -730,6 +758,8 @@ impl ChannelStore { payload: proto::UpdateChannels, cx: &mut ModelContext, ) -> Option>> { + dbg!(self.client.user_id(), &payload); + if !payload.remove_channel_invitations.is_empty() { self.channel_invitations .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); @@ -752,7 +782,9 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() + || !payload.insert_edge.is_empty() || !payload.delete_edge.is_empty(); + if channels_changed { if !payload.delete_channels.is_empty() { self.channel_index.delete_channels(&payload.delete_channels); @@ -774,14 +806,20 @@ impl ChannelStore { } let mut index_edit = self.channel_index.bulk_edit(); - + dbg!(&index_edit); for channel in payload.channels { - index_edit.upsert(channel) + index_edit.insert(channel) + } + + for edge in payload.insert_edge { + index_edit.insert_edge(edge.parent_id, edge.channel_id); } for edge in payload.delete_edge { index_edit.delete_edge(edge.parent_id, edge.channel_id); } + drop(index_edit); + dbg!(&self.channel_index); } for permission in payload.channel_permissions { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index f7d9e873ae5eb99e533d85e05951689ae588a740..b1b205a94169a18a06053302ff6631eab61be54a 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -53,6 +53,7 @@ impl Deref for ChannelIndex { /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions +#[derive(Debug)] pub struct ChannelPathsEditGuard<'a> { paths: &'a mut Vec, channels_by_id: &'a mut ChannelsById, @@ -78,42 +79,81 @@ impl<'a> ChannelPathsEditGuard<'a> { } } - pub fn upsert(&mut self, channel_proto: proto::Channel) { + pub fn insert(&mut self, channel_proto: proto::Channel) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { Arc::make_mut(existing_channel).name = channel_proto.name; - - if let Some(parent_id) = channel_proto.parent_id { - self.insert_edge(parent_id, channel_proto.id) - } } else { - let channel = Arc::new(Channel { - id: channel_proto.id, - name: channel_proto.name, - }); - self.channels_by_id.insert(channel.id, channel.clone()); - - if let Some(parent_id) = channel_proto.parent_id { - self.insert_edge(parent_id, channel.id); - } else { - self.insert_root(channel.id); - } + self.channels_by_id.insert( + channel_proto.id, + Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }), + ); + self.insert_root(channel_proto.id); } } - fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { + pub fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { debug_assert!(self.channels_by_id.contains_key(&parent_id)); let mut ix = 0; + println!("*********** INSERTING EDGE {}, {} ***********", channel_id, parent_id); + dbg!(&self.paths); while ix < self.paths.len() { let path = &self.paths[ix]; + println!("*********"); + dbg!(path); + if path.ends_with(&[parent_id]) { - let mut new_path = path.to_vec(); + dbg!("Appending to parent path"); + let mut new_path = Vec::with_capacity(path.len() + 1); + new_path.extend_from_slice(path); new_path.push(channel_id); - self.paths.insert(ix + 1, ChannelPath::new(new_path.into())); + self.paths.insert(ix + 1, dbg!(ChannelPath::new(new_path.into()))); ix += 2; - } else if path.get(0) == Some(&channel_id) { - // Clear out any paths that have this chahnnel as their root - self.paths.swap_remove(ix); + } else if let Some(path_ix) = path.iter().position(|c| c == &channel_id) { + if path.contains(&parent_id) { + dbg!("Doing nothing"); + ix += 1; + continue; + } + if path_ix == 0 && path.len() == 1 { + dbg!("Removing path that is just this"); + self.paths.swap_remove(ix); + continue; + } + // This is the busted section rn + // We're trying to do this weird, unsorted context + // free insertion thing, but we can't insert 'parent_id', + // we have to _prepend_ with _parent path to_, + // or something like that. + // It's a bit busted rn, I think I need to keep this whole thing + // sorted now, as this is a huge mess. + // Basically, we want to do the exact thing we do in the + // server, except explicitly. + // Also, rethink the bulk edit abstraction, it's use may no longer + // be as needed with the channel names and edges seperated. + dbg!("Expanding path which contains"); + let (left, right) = path.split_at(path_ix); + let mut new_path = Vec::with_capacity(left.len() + right.len() + 1); + + /// WRONG WRONG WRONG + new_path.extend_from_slice(left); + new_path.push(parent_id); + /// WRONG WRONG WRONG + + new_path.extend_from_slice(right); + if path_ix == 0 { + dbg!("Replacing path that starts with this"); + self.paths[ix] = dbg!(ChannelPath::new(new_path.into())); + } else { + dbg!("inserting new path"); + self.paths.insert(ix + 1, dbg!(ChannelPath::new(new_path.into()))); + ix += 1; + } + ix += 1; } else { + dbg!("Doing nothing"); ix += 1; } } diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 1d3694866fcc0094112ae360ebbf163ef7cdefa8..59bfe341aa948b7d053bec9cede85aba1843a8c9 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -18,12 +18,11 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 1, name: "b".to_string(), - parent_id: None, }, proto::Channel { id: 2, name: "a".to_string(), - parent_id: None, + }, ], channel_permissions: vec![proto::ChannelPermission { @@ -51,12 +50,20 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 3, name: "x".to_string(), - parent_id: Some(1), }, proto::Channel { id: 4, name: "y".to_string(), - parent_id: Some(2), + }, + ], + insert_edge: vec![ + proto::ChannelEdge { + parent_id: 1, + channel_id: 3, + }, + proto::ChannelEdge { + parent_id: 2, + channel_id: 4, }, ], ..Default::default() @@ -86,17 +93,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { proto::Channel { id: 0, name: "a".to_string(), - parent_id: None, }, proto::Channel { id: 1, name: "b".to_string(), - parent_id: Some(0), }, proto::Channel { id: 2, name: "c".to_string(), - parent_id: Some(1), + }, + ], + insert_edge: vec![ + proto::ChannelEdge { + parent_id: 0, + channel_id: 1, + }, + proto::ChannelEdge { + parent_id: 1, + channel_id: 2, }, ], channel_permissions: vec![proto::ChannelPermission { @@ -145,7 +159,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { channels: vec![proto::Channel { id: channel_id, name: "the-channel".to_string(), - parent_id: None, }], ..Default::default() }); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b5d968ddf3b5e0b3302d03e8ad37c73df687d724..11c9c986146c9414666567cce99a46bd95ad8722 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -14,7 +14,7 @@ use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; use rand::{prelude::StdRng, Rng, SeedableRng}; -use rpc::{proto, ConnectionId}; +use rpc::{proto::{self}, ConnectionId}; use sea_orm::{ entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, @@ -43,6 +43,8 @@ pub use ids::*; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; +use self::queries::channels::ChannelGraph; + pub struct Database { options: ConnectOptions, pool: DatabaseConnection, @@ -421,16 +423,15 @@ pub struct NewUserResult { pub signup_device_id: Option, } -#[derive(FromQueryResult, Debug, PartialEq)] +#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)] pub struct Channel { pub id: ChannelId, pub name: String, - pub parent_id: Option, } #[derive(Debug, PartialEq)] pub struct ChannelsForUser { - pub channels: Vec, + pub channels: ChannelGraph, pub channel_participants: HashMap>, pub channels_with_admin_privileges: HashSet, } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index dab392c583d82f82b95ac709d07ba73e43c77e1a..ca5e6f0df3a0ea2d113ccf805ff872c30f537183 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,3 +1,4 @@ +use rpc::proto::ChannelEdge; use smallvec::SmallVec; use super::*; @@ -326,7 +327,6 @@ impl Database { .map(|channel| Channel { id: channel.id, name: channel.name, - parent_id: None, }) .collect(); @@ -335,12 +335,12 @@ impl Database { .await } - async fn get_channels_internal( + async fn get_channel_graph( &self, parents_by_child_id: ChannelDescendants, trim_dangling_parents: bool, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result { let mut channels = Vec::with_capacity(parents_by_child_id.len()); { let mut rows = channel::Entity::find() @@ -349,49 +349,36 @@ impl Database { .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 { - let mut added_channel = false; - for parent in parents.iter() { - // Trim out any dangling parent pointers. - // That the user doesn't have access to - if trim_dangling_parents { - if parents_by_child_id.contains_key(parent) { - added_channel = true; - channels.push(Channel { - id: row.id, - name: row.name.clone(), - parent_id: Some(*parent), - }); - } - } else { - added_channel = true; - channels.push(Channel { - id: row.id, - name: row.name.clone(), - parent_id: Some(*parent), - }); - } - } - if !added_channel { - channels.push(Channel { - id: row.id, - name: row.name, - parent_id: None, + channels.push(Channel { + id: row.id, + name: row.name, + }) + } + } + + let mut edges = Vec::with_capacity(parents_by_child_id.len()); + for (channel, parents) in parents_by_child_id.iter() { + for parent in parents.into_iter() { + if trim_dangling_parents { + if parents_by_child_id.contains_key(parent) { + edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: parent.to_proto(), }); } } else { - channels.push(Channel { - id: row.id, - name: row.name, - parent_id: None, + edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: parent.to_proto(), }); } } } - Ok(channels) + Ok(ChannelGraph { + channels, + edges, + }) } pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { @@ -407,49 +394,72 @@ impl Database { .all(&*tx) .await?; - let parents_by_child_id = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) - .await?; + self.get_user_channels(channel_memberships, user_id, &tx).await + }) + .await + } - let channels_with_admin_privileges = channel_memberships - .iter() - .filter_map(|membership| membership.admin.then_some(membership.channel_id)) - .collect(); + pub async fn get_channel_for_user(&self, channel_id: ChannelId, user_id: UserId) -> Result { + self.transaction(|tx| async move { + let tx = tx; - let channels = self - .get_channels_internal(parents_by_child_id, true, &tx) + let channel_membership = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::ChannelId.eq(channel_id)) + .and(channel_member::Column::Accepted.eq(true)), + ) + .all(&*tx) .await?; - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIdsAndChannelIds { - ChannelId, - UserId, - } + self.get_user_channels(channel_membership, user_id, &tx).await + }) + .await + } - let mut channel_participants: HashMap> = HashMap::default(); - { - let mut rows = room_participant::Entity::find() - .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) - .select_only() - .column(room::Column::ChannelId) - .column(room_participant::Column::UserId) - .into_values::<_, QueryUserIdsAndChannelIds>() - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row: (ChannelId, UserId) = row?; - channel_participants.entry(row.0).or_default().push(row.1) - } + pub async fn get_user_channels(&self, channel_memberships: Vec, user_id: UserId, tx: &DatabaseTransaction) -> Result { + let parents_by_child_id = self + .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 graph = self + .get_channel_graph(parents_by_child_id, true, &tx) + .await?; + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIdsAndChannelIds { + ChannelId, + UserId, + } + + let mut channel_participants: HashMap> = HashMap::default(); + { + let mut rows = room_participant::Entity::find() + .inner_join(room::Entity) + .filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id))) + .select_only() + .column(room::Column::ChannelId) + .column(room_participant::Column::UserId) + .into_values::<_, QueryUserIdsAndChannelIds>() + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row: (ChannelId, UserId) = row?; + channel_participants.entry(row.0).or_default().push(row.1) } + } - Ok(ChannelsForUser { - channels, - channel_participants, - channels_with_admin_privileges, - }) + Ok(ChannelsForUser { + channels: graph, + channel_participants, + channels_with_admin_privileges, }) - .await } pub async fn get_channel_members(&self, id: ChannelId) -> Result> { @@ -759,7 +769,6 @@ impl Database { Channel { id: channel.id, name: channel.name, - parent_id: None, }, is_accepted, ))) @@ -792,7 +801,7 @@ impl Database { user: UserId, channel: ChannelId, to: ChannelId, - ) -> Result> { + ) -> Result { 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 @@ -811,7 +820,7 @@ impl Database { channel: ChannelId, to: ChannelId, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result { self.check_user_is_channel_admin(to, user, &*tx).await?; let to_ancestors = self.get_channel_ancestors(to, &*tx).await?; @@ -881,7 +890,7 @@ impl Database { } let channels = self - .get_channels_internal(from_descendants, false, &*tx) + .get_channel_graph(from_descendants, false, &*tx) .await?; Ok(channels) @@ -961,7 +970,7 @@ impl Database { channel: ChannelId, from: ChannelId, to: ChannelId, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel, user, &*tx) .await?; @@ -982,6 +991,39 @@ enum QueryUserIds { UserId, } +#[derive(Debug)] +pub struct ChannelGraph { + pub channels: Vec, + pub edges: Vec, +} + +impl ChannelGraph { + pub fn is_empty(&self) -> bool { + self.channels.is_empty() && self.edges.is_empty() + } +} + +#[cfg(test)] +impl PartialEq for ChannelGraph { + fn eq(&self, other: &Self) -> bool { + // Order independent comparison for tests + let channels_set = self.channels.iter().collect::>(); + let other_channels_set = other.channels.iter().collect::>(); + let edges_set = self.edges.iter().map(|edge| (edge.channel_id, edge.parent_id)).collect::>(); + let other_edges_set = other.edges.iter().map(|edge| (edge.channel_id, edge.parent_id)).collect::>(); + + channels_set == other_channels_set && edges_set == other_edges_set + } +} + +#[cfg(not(test))] +impl PartialEq for ChannelGraph { + fn eq(&self, other: &Self) -> bool { + self.channels == other.channels && self.edges == other.edges + } +} + + struct SmallSet(SmallVec<[T; 1]>); impl Deref for SmallSet { @@ -1008,7 +1050,7 @@ impl SmallSet { Err(ix) => { self.0.insert(ix, value); true - }, + } } } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 3ba5af8518e4568fb0c9abe20fdfa2c468fd242c..cf12be9b8d3beafb7332f20ee10f81ad65ad6a98 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -7,6 +7,7 @@ mod message_tests; use super::*; use gpui::executor::Background; use parking_lot::Mutex; +use rpc::proto::ChannelEdge; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; use std::sync::Arc; @@ -144,3 +145,27 @@ impl Drop for TestDb { } } } + +/// The second tuples are (channel_id, parent) +fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph { + let mut graph = ChannelGraph { + channels: vec![], + edges: vec![], + }; + + for (id, name) in channels { + graph.channels.push(Channel { + id: *id, + name: name.to_string(), + }) + } + + for (channel, parent) in edges { + graph.edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: parent.to_proto(), + }) + } + + graph +} diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 75db9d619cdb735408b174ff7c36858556f17a1a..d8870cbd93431bc78da13779cf069d9652c4ac3f 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1,8 +1,11 @@ use collections::{HashMap, HashSet}; -use rpc::{proto, ConnectionId}; +use rpc::{ + proto::{self}, + ConnectionId, +}; use crate::{ - db::{Channel, ChannelId, Database, NewUserParams}, + db::{queries::channels::ChannelGraph, ChannelId, Database, NewUserParams, tests::graph}, test_both_dbs, }; use std::sync::Arc; @@ -82,70 +85,42 @@ async fn test_channels(db: &Arc) { let result = db.get_channels_for_user(a_id).await.unwrap(); assert_eq!( result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - }, - Channel { - id: cargo_id, - name: "cargo".to_string(), - parent_id: Some(rust_id), - }, - Channel { - id: cargo_ra_id, - name: "cargo-ra".to_string(), - parent_id: Some(cargo_id), - } - ] + graph( + &[ + (zed_id, "zed"), + (crdb_id, "crdb"), + (livestreaming_id, "livestreaming"), + (replace_id, "replace"), + (rust_id, "rust"), + (cargo_id, "cargo"), + (cargo_ra_id, "cargo-ra") + ], + &[ + (crdb_id, zed_id), + (livestreaming_id, zed_id), + (replace_id, zed_id), + (cargo_id, rust_id), + (cargo_ra_id, cargo_id), + ] + ) ); let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - ] + graph( + &[ + (zed_id, "zed"), + (crdb_id, "crdb"), + (livestreaming_id, "livestreaming"), + (replace_id, "replace") + ], + &[ + (crdb_id, zed_id), + (livestreaming_id, zed_id), + (replace_id, zed_id) + ] + ) ); // Update member permissions @@ -157,28 +132,19 @@ async fn test_channels(db: &Arc) { let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - ] + graph( + &[ + (zed_id, "zed"), + (crdb_id, "crdb"), + (livestreaming_id, "livestreaming"), + (replace_id, "replace") + ], + &[ + (crdb_id, zed_id), + (livestreaming_id, zed_id), + (replace_id, zed_id) + ] + ) ); // Remove a single channel @@ -594,11 +560,10 @@ async fn test_db_channel_moving(db: &Arc) { // Not using the assert_dag helper because we want to make sure we're returning the full data pretty_assertions::assert_eq!( returned_channels, - vec![Channel { - id: livestreaming_dag_sub_id, - name: "livestreaming_dag_sub".to_string(), - parent_id: Some(livestreaming_id), - }] + graph( + &[(livestreaming_dag_sub_id, "livestreaming_dag_sub")], + &[(livestreaming_dag_sub_id, livestreaming_id)] + ) ); let result = db.get_channels_for_user(a_id).await.unwrap(); @@ -631,28 +596,19 @@ async fn test_db_channel_moving(db: &Arc) { // Make sure that we're correctly getting the full sub-dag pretty_assertions::assert_eq!( returned_channels, - vec![ - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(gpui2_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), - } - ] + graph( + &[ + (livestreaming_id, "livestreaming"), + (livestreaming_dag_id, "livestreaming_dag"), + (livestreaming_dag_sub_id, "livestreaming_dag_sub"), + ], + &[ + (livestreaming_id, gpui2_id), + (livestreaming_dag_id, livestreaming_id), + (livestreaming_dag_sub_id, livestreaming_id), + (livestreaming_dag_sub_id, livestreaming_dag_id), + ] + ) ); let result = db.get_channels_for_user(a_id).await.unwrap(); @@ -836,26 +792,26 @@ async fn test_db_channel_moving(db: &Arc) { } #[track_caller] -fn assert_dag(actual: Vec, expected: &[(ChannelId, Option)]) { - /// This is used to allow tests to be ordering independent - fn make_parents_map(association_table: impl IntoIterator)>) -> HashMap> { - let mut map: HashMap> = HashMap::default(); - - for (child, parent) in association_table { - let entry = map.entry(child).or_default(); - if let Some(parent) = parent { - entry.insert(parent); - } - } - - map +fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { + let mut actual_map: HashMap> = HashMap::default(); + for channel in actual.channels { + actual_map.insert(channel.id, HashSet::default()); } - let actual = actual - .iter() - .map(|channel| (channel.id, channel.parent_id)); + for edge in actual.edges { + actual_map + .get_mut(&ChannelId::from_proto(edge.channel_id)) + .unwrap() + .insert(ChannelId::from_proto(edge.parent_id)); + } + + let mut expected_map: HashMap> = HashMap::default(); - let actual_map = make_parents_map(actual); - let expected_map = make_parents_map(expected.iter().copied()); + for (child, parent) in expected { + let entry = expected_map.entry(*child).or_default(); + if let Some(parent) = parent { + entry.insert(*parent); + } + } pretty_assertions::assert_eq!(actual_map, expected_map) } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index d5e3349d4755c49f4767a072a54efaabdef8398c..9a617166fead82ca5b538b84ec268329f1f8de22 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -575,458 +575,6 @@ async fn test_fuzzy_search_users() { } } -test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); - -async fn test_channels(db: &Arc) { - let a_id = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let b_id = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); - - // Make sure that people cannot read channels they haven't been invited to - assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); - - db.invite_channel_member(zed_id, b_id, a_id, false) - .await - .unwrap(); - - db.respond_to_channel_invite(zed_id, b_id, true) - .await - .unwrap(); - - let crdb_id = db - .create_channel("crdb", Some(zed_id), "2", a_id) - .await - .unwrap(); - let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), "3", a_id) - .await - .unwrap(); - let replace_id = db - .create_channel("replace", Some(zed_id), "4", a_id) - .await - .unwrap(); - - let mut members = db.get_channel_members(replace_id).await.unwrap(); - members.sort(); - assert_eq!(members, &[a_id, b_id]); - - let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); - let cargo_id = db - .create_channel("cargo", Some(rust_id), "6", a_id) - .await - .unwrap(); - - let cargo_ra_id = db - .create_channel("cargo-ra", Some(cargo_id), "7", a_id) - .await - .unwrap(); - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - }, - Channel { - id: cargo_id, - name: "cargo".to_string(), - parent_id: Some(rust_id), - }, - Channel { - id: cargo_ra_id, - name: "cargo-ra".to_string(), - parent_id: Some(cargo_id), - } - ] - ); - - let result = db.get_channels_for_user(b_id).await.unwrap(); - assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - ] - ); - - // Update member permissions - let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; - assert!(set_subchannel_admin.is_err()); - let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; - assert!(set_channel_admin.is_ok()); - - let result = db.get_channels_for_user(b_id).await.unwrap(); - assert_eq!( - result.channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - ] - ); - - // Remove a single channel - 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.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]); - - assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); -} - -test_both_dbs!( - test_joining_channels, - test_joining_channels_postgres, - test_joining_channels_sqlite -); - -async fn test_joining_channels(db: &Arc) { - let owner_id = db.create_server("test").await.unwrap().0 as u32; - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); - - // can join a room with membership to its channel - let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(joined_room.room.participants.len(), 1); - - drop(joined_room); - // cannot join a room without membership to its channel - assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) - .await - .is_err()); -} - -test_both_dbs!( - test_channel_invites, - test_channel_invites_postgres, - test_channel_invites_sqlite -); - -async fn test_channel_invites(db: &Arc) { - db.create_server("test").await.unwrap(); - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) - .await - .unwrap(); - - db.invite_channel_member(channel_1_1, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) - .await - .unwrap(); - - let user_2_invites = db - .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); - - let user_3_invites = db - .get_channel_invites_for_user(user_3) // -> [channel_1_1] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - assert_eq!(user_3_invites, &[channel_1_1]); - - let members = db - .get_channel_member_details(channel_1_1, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: false, - }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: true, - }, - ] - ); - - db.respond_to_channel_invite(channel_1_1, user_2, true) - .await - .unwrap(); - - let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) - .await - .unwrap(); - - let members = db - .get_channel_member_details(channel_1_3, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, - }, - ] - ); -} - -test_both_dbs!( - test_channel_renames, - test_channel_renames_postgres, - test_channel_renames_sqlite -); - -async fn test_channel_renames(db: &Arc) { - db.create_server("test").await.unwrap(); - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); - - db.rename_channel(zed_id, user_1, "#zed-archive") - .await - .unwrap(); - - let zed_archive_id = zed_id; - - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); - assert_eq!(channel.name, "zed-archive"); - - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; - assert!(non_permissioned_rename.is_err()); - - let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; - assert!(bad_name_rename.is_err()) -} - fn build_background_executor() -> Arc { Deterministic::new(0).build_background() } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0447813356402a2f667c08cdc8206b17ec377c80..60f4216f64d2c9b459d089aea48f9948272aec2e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,7 +3,7 @@ mod connection_pool; use crate::{ auth, db::{ - self, Channel, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, + self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, UserId, }, executor::Executor, @@ -39,7 +39,7 @@ use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, - LiveKitConnectionInfo, RequestMessage, + LiveKitConnectionInfo, RequestMessage, ChannelEdge, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -2200,33 +2200,38 @@ async fn create_channel( let channel = proto::Channel { id: id.to_proto(), name: request.name, - parent_id: request.parent_id, }; - response.send(proto::ChannelResponse { + response.send(proto::CreateChannelResponse { channel: Some(channel.clone()), + parent_id: request.parent_id, })?; - let mut update = proto::UpdateChannels::default(); - update.channels.push(channel); + let Some(parent_id) = parent_id else { + return Ok(()); + }; - let user_ids_to_notify = if let Some(parent_id) = parent_id { - db.get_channel_members(parent_id).await? - } else { - vec![session.user_id] + let update = proto::UpdateChannels { + channels: vec![channel], + insert_edge: vec![ + ChannelEdge { + parent_id: parent_id.to_proto(), + channel_id: id.to_proto(), + } + ], + ..Default::default() }; + let user_ids_to_notify = + db.get_channel_members(parent_id).await?; + let connection_pool = session.connection_pool().await; for user_id in user_ids_to_notify { for connection_id in connection_pool.user_connection_ids(user_id) { - let mut update = update.clone(); if user_id == session.user_id { - update.channel_permissions.push(proto::ChannelPermission { - channel_id: id.to_proto(), - is_admin: true, - }); + continue; } - session.peer.send(connection_id, update)?; + session.peer.send(connection_id, update.clone())?; } } @@ -2282,7 +2287,6 @@ async fn invite_channel_member( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: None, }); for connection_id in session .connection_pool() @@ -2373,9 +2377,8 @@ async fn rename_channel( let channel = proto::Channel { id: request.channel_id, name: new_name, - parent_id: None, }; - response.send(proto::ChannelResponse { + response.send(proto::RenameChannelResponse { channel: Some(channel.clone()), })?; let mut update = proto::UpdateChannels::default(); @@ -2407,13 +2410,14 @@ async fn link_channel( let connection_pool = session.connection_pool().await; let update = proto::UpdateChannels { channels: channels_to_send + .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(), + insert_edge: channels_to_send.edges, ..Default::default() }; for member_id in members { @@ -2469,13 +2473,12 @@ async fn move_channel( let from_parent = ChannelId::from_proto(request.from); let to = ChannelId::from_proto(request.to); - let members_from = db.get_channel_members(channel_id).await?; - - let channels_to_send: Vec = db + let channels_to_send = db .move_channel(session.user_id, channel_id, from_parent, to) .await?; - let members_to = db.get_channel_members(channel_id).await?; + let members_from = db.get_channel_members(from_parent).await?; + let members_to = db.get_channel_members(to).await?; let update = proto::UpdateChannels { delete_edge: vec![proto::ChannelEdge { @@ -2493,13 +2496,14 @@ async fn move_channel( let update = proto::UpdateChannels { channels: channels_to_send + .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(), + insert_edge: channels_to_send.edges, ..Default::default() }; for member_id in members_to { @@ -2542,14 +2546,14 @@ async fn respond_to_channel_invite( .remove_channel_invitations .push(channel_id.to_proto()); if request.accept { - let result = db.get_channels_for_user(session.user_id).await?; + let result = db.get_channel_for_user(channel_id, session.user_id).await?; update .channels - .extend(result.channels.into_iter().map(|channel| proto::Channel { + .extend(result.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), })); + update.insert_edge = result.channels.edges; update .channel_participants .extend( @@ -2967,14 +2971,15 @@ fn build_initial_channels_update( ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); - for channel in channels.channels { + for channel in channels.channels.channels{ update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: channel.parent_id.map(|id| id.to_proto()), }); } + update.insert_edge = channels.channels.edges; + for (channel_id, participants) in channels.channel_participants { update .channel_participants @@ -3000,7 +3005,6 @@ fn build_initial_channels_update( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: None, }); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 551d6ee3bc8be806f8fde0b36f30d7c7a81a5979..906461a10e29117e5b42ce164cb8939ccaf114e6 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -142,6 +142,8 @@ async fn test_core_channels( ], ); + println!("STARTING CREATE CHANNEL C"); + let channel_c_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { @@ -994,10 +996,6 @@ async fn test_channel_moving( .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) .await; - client_b - .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) - .await; - // Current shape for B: // /- ep // mu -- ga @@ -1019,10 +1017,18 @@ async fn test_channel_moving( ], ); + client_b + .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) + .await; + // Current shape for C: // - ep assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); + println!("*******************************************"); + println!("********** STARTING LINK CHANNEL **********"); + println!("*******************************************"); + dbg!(client_b.user_id()); client_b .channel_store() .update(cx_b, |channel_store, cx| { @@ -1190,5 +1196,5 @@ fn assert_channels_list_shape( .map(|(depth, channel)| (channel.id, depth)) .collect::>() }); - pretty_assertions::assert_eq!(actual, expected_channels); + pretty_assertions::assert_eq!(dbg!(actual), expected_channels); } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 2fe6148fe503489939b6fd49db8614ba29a14a8c..db69ce546074e743d28a8f2711513d119de2cfd4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -173,7 +173,6 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, action: &StartMoveChannel, _: &mut ViewContext| { - dbg!(action); panel.channel_move = Some(*action); }, ); @@ -181,7 +180,6 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext| { if let Some(move_start) = panel.channel_move.take() { - dbg!(action.to, &move_start); panel.channel_store.update(cx, |channel_store, cx| { channel_store .link_channel(move_start.channel_id, action.to, cx) @@ -194,7 +192,6 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { if let Some(move_start) = panel.channel_move.take() { - dbg!(&move_start, action.to); panel.channel_store.update(cx, |channel_store, cx| { if let Some(parent) = move_start.parent_id { channel_store diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 294f9a9706a54f458d5fb96d3a0b56c2f8e607ca..c2bb9e9cef1b505d7fbfd7babdc0d3ebb50e910a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -135,7 +135,7 @@ message Envelope { RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; - ChannelResponse channel_response = 120; + CreateChannelResponse create_channel_response = 120; InviteChannelMember invite_channel_member = 121; RemoveChannelMember remove_channel_member = 122; RespondToChannelInvite respond_to_channel_invite = 123; @@ -146,6 +146,7 @@ message Envelope { GetChannelMembersResponse get_channel_members_response = 128; SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; + RenameChannelResponse rename_channel_response = 154; JoinChannelBuffer join_channel_buffer = 131; JoinChannelBufferResponse join_channel_buffer_response = 132; @@ -169,7 +170,7 @@ message Envelope { LinkChannel link_channel = 151; UnlinkChannel unlink_channel = 152; - MoveChannel move_channel = 153; // Current max + MoveChannel move_channel = 153; // Current max: 154 } } @@ -959,12 +960,13 @@ message LspDiskBasedDiagnosticsUpdated {} message UpdateChannels { repeated Channel channels = 1; - repeated ChannelEdge delete_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; + repeated ChannelEdge insert_edge = 2; + repeated ChannelEdge delete_edge = 3; + repeated uint64 delete_channels = 4; + repeated Channel channel_invitations = 5; + repeated uint64 remove_channel_invitations = 6; + repeated ChannelParticipants channel_participants = 7; + repeated ChannelPermission channel_permissions = 8; } message ChannelEdge { @@ -1015,8 +1017,9 @@ message CreateChannel { optional uint64 parent_id = 2; } -message ChannelResponse { +message CreateChannelResponse { Channel channel = 1; + optional uint64 parent_id = 2; } message InviteChannelMember { @@ -1041,6 +1044,10 @@ message RenameChannel { string name = 2; } +message RenameChannelResponse { + Channel channel = 1; +} + message JoinChannelChat { uint64 channel_id = 1; } @@ -1512,7 +1519,6 @@ message Nonce { message Channel { uint64 id = 1; string name = 2; - optional uint64 parent_id = 3; } message Contact { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 0a2c4a9d7de0f977ff3b5b6dddb4fcd7ff7d7057..44a7df3b749903fb94b9bb830b1b4b31401263b2 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -146,7 +146,7 @@ messages!( (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), (CreateChannel, Foreground), - (ChannelResponse, Foreground), + (CreateChannelResponse, Foreground), (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), @@ -229,6 +229,7 @@ messages!( (RoomUpdated, Foreground), (SaveBuffer, Foreground), (RenameChannel, Foreground), + (RenameChannelResponse, Foreground), (SetChannelMemberAdmin, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -285,7 +286,7 @@ request_messages!( (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), - (CreateChannel, ChannelResponse), + (CreateChannel, CreateChannelResponse), (DeclineCall, Ack), (DeleteProjectEntry, ProjectEntryResponse), (ExpandProjectEntry, ExpandProjectEntryResponse), @@ -333,7 +334,7 @@ request_messages!( (RemoveChannelMessage, Ack), (DeleteChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), - (RenameChannel, ChannelResponse), + (RenameChannel, RenameChannelResponse), (LinkChannel, Ack), (UnlinkChannel, Ack), (MoveChannel, Ack), From dadad397ef0014f859fd7cae8cb3c69d14846d33 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 18 Sep 2023 20:24:33 -0700 Subject: [PATCH 22/26] Finish optimizing channel representations and operations --- Cargo.lock | 1 + crates/channel/Cargo.toml | 1 + crates/channel/src/channel_store.rs | 11 +- .../src/channel_store/channel_index.rs | 108 +++++++----------- crates/channel/src/channel_store_tests.rs | 1 - crates/collab/src/db.rs | 5 +- crates/collab/src/db/queries/channels.rs | 54 +++++---- crates/collab/src/db/tests/channel_tests.rs | 2 +- crates/collab/src/rpc.rs | 37 +++--- crates/collab_ui/src/collab_panel.rs | 28 ++++- 10 files changed, 131 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 327ca26937d78970a4a09c74f0a9854da51214ff..e927ae5bf97162c52ed3c41060411c5242a4f1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,6 +1222,7 @@ dependencies = [ "serde", "serde_derive", "settings", + "smallvec", "smol", "sum_tree", "tempfile", diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 00e9135bc1791f7a59e9270f48e9c9282f7b5b5d..16a1d418d5c17750db582252118c3ddcc1cad2d3 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -28,6 +28,7 @@ anyhow.workspace = true futures.workspace = true image = "0.23" lazy_static.workspace = true +smallvec.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 8a3875b2b966abd7346110489a29ed38814891e8..02eddb9900f9e56efa49475a212303d1ef06c7a7 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -805,21 +805,18 @@ impl ChannelStore { } } - let mut index_edit = self.channel_index.bulk_edit(); - dbg!(&index_edit); + let mut index = self.channel_index.bulk_insert(); for channel in payload.channels { - index_edit.insert(channel) + index.insert(channel) } for edge in payload.insert_edge { - index_edit.insert_edge(edge.parent_id, edge.channel_id); + index.insert_edge(edge.channel_id, edge.parent_id); } for edge in payload.delete_edge { - index_edit.delete_edge(edge.parent_id, edge.channel_id); + index.delete_edge(edge.parent_id, edge.channel_id); } - drop(index_edit); - dbg!(&self.channel_index); } for permission in payload.channel_permissions { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index b1b205a94169a18a06053302ff6631eab61be54a..8fe2607f9e96542e4071914151998f0f2fc04450 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,10 +1,9 @@ use std::{ops::Deref, sync::Arc}; +use crate::{Channel, ChannelId}; use collections::HashMap; use rpc::proto; -use crate::{Channel, ChannelId}; - use super::ChannelPath; pub type ChannelsById = HashMap>; @@ -35,8 +34,8 @@ impl ChannelIndex { }); } - pub fn bulk_edit(&mut self) -> ChannelPathsEditGuard { - ChannelPathsEditGuard { + pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { + ChannelPathsInsertGuard { paths: &mut self.paths, channels_by_id: &mut self.channels_by_id, } @@ -54,12 +53,12 @@ impl Deref for ChannelIndex { /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions #[derive(Debug)] -pub struct ChannelPathsEditGuard<'a> { +pub struct ChannelPathsInsertGuard<'a> { paths: &'a mut Vec, channels_by_id: &'a mut ChannelsById, } -impl<'a> ChannelPathsEditGuard<'a> { +impl<'a> ChannelPathsInsertGuard<'a> { /// Remove the given edge from this index. This will not remove the channel. /// If this operation would result in a dangling edge, re-insert it. pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { @@ -94,69 +93,50 @@ impl<'a> ChannelPathsEditGuard<'a> { } } - pub fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { - debug_assert!(self.channels_by_id.contains_key(&parent_id)); - let mut ix = 0; - println!("*********** INSERTING EDGE {}, {} ***********", channel_id, parent_id); - dbg!(&self.paths); - while ix < self.paths.len() { - let path = &self.paths[ix]; - println!("*********"); - dbg!(path); + pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) { + let mut parents = Vec::new(); + let mut descendants = Vec::new(); + let mut ixs_to_remove = Vec::new(); + for (ix, path) in self.paths.iter().enumerate() { + if path + .windows(2) + .any(|window| window[0] == parent_id && window[1] == channel_id) + { + // We already have this edge in the index + return; + } if path.ends_with(&[parent_id]) { - dbg!("Appending to parent path"); - let mut new_path = Vec::with_capacity(path.len() + 1); - new_path.extend_from_slice(path); - new_path.push(channel_id); - self.paths.insert(ix + 1, dbg!(ChannelPath::new(new_path.into()))); - ix += 2; - } else if let Some(path_ix) = path.iter().position(|c| c == &channel_id) { - if path.contains(&parent_id) { - dbg!("Doing nothing"); - ix += 1; - continue; - } - if path_ix == 0 && path.len() == 1 { - dbg!("Removing path that is just this"); - self.paths.swap_remove(ix); - continue; - } - // This is the busted section rn - // We're trying to do this weird, unsorted context - // free insertion thing, but we can't insert 'parent_id', - // we have to _prepend_ with _parent path to_, - // or something like that. - // It's a bit busted rn, I think I need to keep this whole thing - // sorted now, as this is a huge mess. - // Basically, we want to do the exact thing we do in the - // server, except explicitly. - // Also, rethink the bulk edit abstraction, it's use may no longer - // be as needed with the channel names and edges seperated. - dbg!("Expanding path which contains"); - let (left, right) = path.split_at(path_ix); - let mut new_path = Vec::with_capacity(left.len() + right.len() + 1); - - /// WRONG WRONG WRONG - new_path.extend_from_slice(left); - new_path.push(parent_id); - /// WRONG WRONG WRONG - - new_path.extend_from_slice(right); - if path_ix == 0 { - dbg!("Replacing path that starts with this"); - self.paths[ix] = dbg!(ChannelPath::new(new_path.into())); - } else { - dbg!("inserting new path"); - self.paths.insert(ix + 1, dbg!(ChannelPath::new(new_path.into()))); - ix += 1; + parents.push(path); + } else if let Some(position) = path.iter().position(|id| id == &channel_id) { + if position == 0 { + ixs_to_remove.push(ix); } - ix += 1; + descendants.push(path.split_at(position).1); + } + } + + let mut new_paths = Vec::new(); + for parent in parents.iter() { + if descendants.is_empty() { + let mut new_path = Vec::with_capacity(parent.len() + 1); + new_path.extend_from_slice(parent); + new_path.push(channel_id); + new_paths.push(ChannelPath(new_path.into())); } else { - dbg!("Doing nothing"); - ix += 1; + for descendant in descendants.iter() { + let mut new_path = Vec::with_capacity(parent.len() + descendant.len()); + new_path.extend_from_slice(parent); + new_path.extend_from_slice(descendant); + new_paths.push(ChannelPath(new_path.into())); + } } } + + for ix in ixs_to_remove.into_iter().rev() { + self.paths.swap_remove(ix); + } + self.paths.extend(new_paths) } fn insert_root(&mut self, channel_id: ChannelId) { @@ -164,7 +144,7 @@ impl<'a> ChannelPathsEditGuard<'a> { } } -impl<'a> Drop for ChannelPathsEditGuard<'a> { +impl<'a> Drop for ChannelPathsInsertGuard<'a> { fn drop(&mut self) { self.paths.sort_by(|a, b| { let a = channel_path_sorting_key(a, &self.channels_by_id); diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 59bfe341aa948b7d053bec9cede85aba1843a8c9..775bf2942541a19acf0730d3ccb79fba2e977fc9 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -22,7 +22,6 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 2, name: "a".to_string(), - }, ], channel_permissions: vec![proto::ChannelPermission { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 11c9c986146c9414666567cce99a46bd95ad8722..527c4faaa5ccd5f8e5776e61bf7c6f2c032ad9ea 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -14,7 +14,10 @@ use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; use rand::{prelude::StdRng, Rng, SeedableRng}; -use rpc::{proto::{self}, ConnectionId}; +use rpc::{ + proto::{self}, + ConnectionId, +}; use sea_orm::{ entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ca5e6f0df3a0ea2d113ccf805ff872c30f537183..2d65139a9889210bd7356833e4aa1d27e6e08f77 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -375,10 +375,7 @@ impl Database { } } - Ok(ChannelGraph { - channels, - edges, - }) + Ok(ChannelGraph { channels, edges }) } pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { @@ -394,12 +391,16 @@ impl Database { .all(&*tx) .await?; - self.get_user_channels(channel_memberships, user_id, &tx).await + self.get_user_channels(channel_memberships, &tx).await }) .await } - pub async fn get_channel_for_user(&self, channel_id: ChannelId, user_id: UserId) -> Result { + pub async fn get_channel_for_user( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -413,12 +414,16 @@ impl Database { .all(&*tx) .await?; - self.get_user_channels(channel_membership, user_id, &tx).await + self.get_user_channels(channel_membership, &tx).await }) .await } - pub async fn get_user_channels(&self, channel_memberships: Vec, user_id: UserId, tx: &DatabaseTransaction) -> Result { + pub async fn get_user_channels( + &self, + channel_memberships: Vec, + tx: &DatabaseTransaction, + ) -> Result { let parents_by_child_id = self .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; @@ -824,9 +829,9 @@ impl Database { self.check_user_is_channel_admin(to, user, &*tx).await?; let to_ancestors = self.get_channel_ancestors(to, &*tx).await?; - let mut from_descendants = self.get_channel_descendants([channel], &*tx).await?; + let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?; for ancestor in to_ancestors { - if from_descendants.contains_key(&ancestor) { + if channel_descendants.contains_key(&ancestor) { return Err(anyhow!("Cannot create a channel cycle").into()); } } @@ -853,15 +858,17 @@ impl Database { ], ); tx.execute(channel_paths_stmt).await?; - for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&channel) { - for to_id in to_ids.iter() { + for (descdenant_id, descendant_parent_ids) in + channel_descendants.iter().filter(|(id, _)| id != &&channel) + { + for descendant_parent_id in descendant_parent_ids.iter() { let channel_paths_stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, [ - from_id.to_proto().into(), - from_id.to_proto().into(), - to_id.to_proto().into(), + descdenant_id.to_proto().into(), + descdenant_id.to_proto().into(), + descendant_parent_id.to_proto().into(), ], ); tx.execute(channel_paths_stmt).await?; @@ -883,14 +890,14 @@ impl Database { tx.execute(channel_paths_stmt).await?; } - if let Some(channel) = from_descendants.get_mut(&channel) { + if let Some(channel) = channel_descendants.get_mut(&channel) { // Remove the other parents channel.clear(); channel.insert(to); } let channels = self - .get_channel_graph(from_descendants, false, &*tx) + .get_channel_graph(channel_descendants, false, &*tx) .await?; Ok(channels) @@ -1009,8 +1016,16 @@ impl PartialEq for ChannelGraph { // Order independent comparison for tests let channels_set = self.channels.iter().collect::>(); let other_channels_set = other.channels.iter().collect::>(); - let edges_set = self.edges.iter().map(|edge| (edge.channel_id, edge.parent_id)).collect::>(); - let other_edges_set = other.edges.iter().map(|edge| (edge.channel_id, edge.parent_id)).collect::>(); + let edges_set = self + .edges + .iter() + .map(|edge| (edge.channel_id, edge.parent_id)) + .collect::>(); + let other_edges_set = other + .edges + .iter() + .map(|edge| (edge.channel_id, edge.parent_id)) + .collect::>(); channels_set == other_channels_set && edges_set == other_edges_set } @@ -1023,7 +1038,6 @@ impl PartialEq for ChannelGraph { } } - struct SmallSet(SmallVec<[T; 1]>); impl Deref for SmallSet { diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index d8870cbd93431bc78da13779cf069d9652c4ac3f..8a03014cf9ef844fb743319720dd5846784577d6 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -5,7 +5,7 @@ use rpc::{ }; use crate::{ - db::{queries::channels::ChannelGraph, ChannelId, Database, NewUserParams, tests::graph}, + db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams}, test_both_dbs, }; use std::sync::Arc; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 60f4216f64d2c9b459d089aea48f9948272aec2e..87b77202356accb6594ab0193c12cc0ce648cd71 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,8 @@ mod connection_pool; use crate::{ auth, db::{ - self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, - ServerId, User, UserId, + self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, + UserId, }, executor::Executor, AppState, Result, @@ -38,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, - LiveKitConnectionInfo, RequestMessage, ChannelEdge, + self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage, + EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -2213,17 +2213,14 @@ async fn create_channel( let update = proto::UpdateChannels { channels: vec![channel], - insert_edge: vec![ - ChannelEdge { - parent_id: parent_id.to_proto(), - channel_id: id.to_proto(), - } - ], + insert_edge: vec![ChannelEdge { + parent_id: parent_id.to_proto(), + channel_id: id.to_proto(), + }], ..Default::default() }; - let user_ids_to_notify = - db.get_channel_members(parent_id).await?; + let user_ids_to_notify = db.get_channel_members(parent_id).await?; let connection_pool = session.connection_pool().await; for user_id in user_ids_to_notify { @@ -2549,10 +2546,16 @@ async fn respond_to_channel_invite( let result = db.get_channel_for_user(channel_id, session.user_id).await?; update .channels - .extend(result.channels.channels.into_iter().map(|channel| proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - })); + .extend( + result + .channels + .channels + .into_iter() + .map(|channel| proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + }), + ); update.insert_edge = result.channels.edges; update .channel_participants @@ -2971,7 +2974,7 @@ fn build_initial_channels_update( ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); - for channel in channels.channels.channels{ + for channel in channels.channels.channels { update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index db69ce546074e743d28a8f2711513d119de2cfd4..70137407a10945d86672ebbae9db808392d6df10 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2024,11 +2024,15 @@ impl CollabPanel { if let Some(channel_name) = channel_name { items.push(ContextMenuItem::action( format!("Move '#{}' here", channel_name), - MoveChannel { to: path.channel_id() }, + MoveChannel { + to: path.channel_id(), + }, )); items.push(ContextMenuItem::action( format!("Link '#{}' here", channel_name), - LinkChannel { to: path.channel_id() }, + LinkChannel { + to: path.channel_id(), + }, )); items.push(ContextMenuItem::Separator) } @@ -2046,7 +2050,12 @@ impl CollabPanel { location: path.clone(), }, ), - ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: path.channel_id() }), + ContextMenuItem::action( + "Open Notes", + OpenChannelBuffer { + channel_id: path.channel_id(), + }, + ), ]); if self.channel_store.read(cx).is_user_admin(path.channel_id()) { @@ -2306,7 +2315,6 @@ impl CollabPanel { } self.toggle_channel_collapsed(&path.clone(), cx); - } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { @@ -2324,11 +2332,19 @@ impl CollabPanel { self.toggle_channel_collapsed(path.to_owned(), cx) } - fn toggle_channel_collapsed_action(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { + fn toggle_channel_collapsed_action( + &mut self, + action: &ToggleCollapse, + cx: &mut ViewContext, + ) { self.toggle_channel_collapsed(&action.location, cx); } - fn toggle_channel_collapsed<'a>(&mut self, path: impl Into>, cx: &mut ViewContext) { + fn toggle_channel_collapsed<'a>( + &mut self, + path: impl Into>, + cx: &mut ViewContext, + ) { let path = path.into(); match self.collapsed_channels.binary_search(&path) { Ok(ix) => { From 9bff3b6916c274672c783508abf30f2163be0866 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 19 Sep 2023 11:20:01 -0700 Subject: [PATCH 23/26] Add basic drag and drop support --- Cargo.lock | 1 + crates/channel/src/channel.rs | 2 +- crates/channel/src/channel_store.rs | 4 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_panel.rs | 107 ++++++++++++++++++++++++++- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e927ae5bf97162c52ed3c41060411c5242a4f1fc..de0a6c1e92e865ca67973e1f8c34620f7af7d906 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1537,6 +1537,7 @@ dependencies = [ "collections", "context_menu", "db", + "drag_and_drop", "editor", "feature_flags", "feedback", diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index d3e2f8956435727502f83f00517fdb29ac61eb45..724ff75d60f01d75371b1c1286a4e46cca1b298a 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -5,7 +5,7 @@ mod channel_store; pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent}; pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; pub use channel_store::{ - Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, + Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, }; use client::Client; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 02eddb9900f9e56efa49475a212303d1ef06c7a7..5e2e10516538003c4e072e653848aac778d6e5f9 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -47,6 +47,8 @@ pub struct ChannelStore { _update_channels: Task<()>, } +pub type ChannelData = (Channel, ChannelPath); + #[derive(Clone, Debug, PartialEq)] pub struct Channel { pub id: ChannelId, @@ -758,8 +760,6 @@ impl ChannelStore { payload: proto::UpdateChannels, cx: &mut ModelContext, ) -> Option>> { - dbg!(self.client.user_id(), &payload); - if !payload.remove_channel_invitations.is_empty() { self.channel_invitations .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 0a52b9a19fe9df7c070e33de1d8c719b0dd8dae2..b6e45471f14a0691f2188d3f0c5f20d003128b86 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -30,6 +30,7 @@ channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } +drag_and_drop = { path = "../drag_and_drop" } editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 70137407a10945d86672ebbae9db808392d6df10..b4920175ca4cf356c256ed34744c449d3be06ca0 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,12 +9,13 @@ use crate::{ }; use anyhow::Result; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; +use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use channel_modal::ChannelModal; use client::{proto::PeerId, Client, Contact, User, UserStore}; use contact_finder::ContactFinder; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; +use drag_and_drop::{DragAndDrop, Draggable}; use editor::{Cancel, Editor}; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use futures::StreamExt; @@ -116,6 +117,8 @@ struct UnlinkChannel { parent_id: ChannelId, } +type DraggedChannel = (Channel, Option); + actions!( collab_panel, [ @@ -260,6 +263,7 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, + dragged_channel_target: Option, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -525,6 +529,7 @@ impl CollabPanel { workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, + dragged_channel_target: None, list_state, }; @@ -1661,6 +1666,20 @@ impl CollabPanel { enum ChannelCall {} + let mut is_dragged_over = false; + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + && self + .dragged_channel_target + .as_ref() + .filter(|(_, dragged_path)| path.starts_with(dragged_path)) + .is_some() + { + is_dragged_over = true; + } + MouseEventHandler::new::(path.unique_id() as usize, cx, |state, cx| { let row_hovered = state.hovered(); @@ -1741,7 +1760,11 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style(*theme.channel_row.style_for(is_selected || is_active, state)) + .with_style( + *theme + .channel_row + .style_for(is_selected || is_active || is_dragged_over, state), + ) .with_padding_left( theme.channel_row.default_style().padding.left + theme.channel_indent * depth as f32, @@ -1750,9 +1773,85 @@ impl CollabPanel { .on_click(MouseButton::Left, move |_, this, cx| { this.join_channel_chat(channel_id, cx); }) - .on_click(MouseButton::Right, move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), &path, cx); + .on_click(MouseButton::Right, { + let path = path.clone(); + move |e, this, cx| { + this.deploy_channel_context_menu(Some(e.position), &path, cx); + } + }) + .on_up(MouseButton::Left, move |e, this, cx| { + if let Some((_, dragged_channel)) = cx + .global::>() + .currently_dragged::(cx.window()) + { + if e.modifiers.alt { + this.channel_store.update(cx, |channel_store, cx| { + channel_store + .link_channel(dragged_channel.0.id, channel_id, cx) + .detach_and_log_err(cx) + }) + } else { + this.channel_store.update(cx, |channel_store, cx| { + match dragged_channel.1 { + Some(parent_id) => channel_store.move_channel( + dragged_channel.0.id, + parent_id, + channel_id, + cx, + ), + None => { + channel_store.link_channel(dragged_channel.0.id, channel_id, cx) + } + } + .detach_and_log_err(cx) + }) + } + } + }) + .on_move({ + let channel = channel.clone(); + let path = path.clone(); + move |_, this, cx| { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + { + this.dragged_channel_target = Some((channel.clone(), path.clone())); + } + } }) + .as_draggable( + (channel.clone(), path.parent_id()), + move |(channel, _), cx: &mut ViewContext| { + let theme = &theme::current(cx).collab_panel; + + Flex::::row() + .with_child( + Svg::new("icons/hash.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new(channel.name.clone(), theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .align_children_center() + .contained() + .with_padding_left( + theme.channel_row.default_style().padding.left + + theme.channel_indent * depth as f32, + ) + .into_any() + }, + ) .with_cursor_style(CursorStyle::PointingHand) .into_any() } From f3b91082a6e22c040c3d0698a776cc5fde7b512d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 19 Sep 2023 14:48:23 -0700 Subject: [PATCH 24/26] Improve drag and drop to look and feel better WIP: Change rendering of drag and drop based on alt-modifier --- crates/collab_ui/src/collab_panel.rs | 51 ++++++++++++++++--- crates/drag_and_drop/src/drag_and_drop.rs | 60 +++++++++++++++++++---- crates/project_panel/src/project_panel.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 8 ++- 5 files changed, 102 insertions(+), 21 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b4920175ca4cf356c256ed34744c449d3be06ca0..4e5c793ef3fbcec947d38de9c68976cb532c3a8d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -33,7 +33,7 @@ use gpui::{ vector::{vec2f, Vector2F}, }, impl_actions, - platform::{CursorStyle, MouseButton, PromptLevel}, + platform::{CursorStyle, ModifiersChangedEvent, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -1669,7 +1669,7 @@ impl CollabPanel { let mut is_dragged_over = false; if cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::(cx.window()) .is_some() && self .dragged_channel_target @@ -1771,7 +1771,9 @@ impl CollabPanel { ) }) .on_click(MouseButton::Left, move |_, this, cx| { - this.join_channel_chat(channel_id, cx); + if this.dragged_channel_target.take().is_none() { + this.join_channel_chat(channel_id, cx); + } }) .on_click(MouseButton::Right, { let path = path.clone(); @@ -1817,16 +1819,32 @@ impl CollabPanel { .currently_dragged::(cx.window()) .is_some() { - this.dragged_channel_target = Some((channel.clone(), path.clone())); + if let Some(dragged_channel_target) = &this.dragged_channel_target { + if dragged_channel_target.0 != channel || dragged_channel_target.1 != path { + this.dragged_channel_target = Some((channel.clone(), path.clone())); + cx.notify(); + } + } else { + this.dragged_channel_target = Some((channel.clone(), path.clone())); + cx.notify(); + } } } }) .as_draggable( (channel.clone(), path.parent_id()), - move |(channel, _), cx: &mut ViewContext| { + move |e, (channel, _), cx: &mut ViewContext| { let theme = &theme::current(cx).collab_panel; Flex::::row() + .with_children(e.alt.then(|| { + Svg::new("icons/plus.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left() + })) .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -1840,11 +1858,17 @@ impl CollabPanel { .contained() .with_style(theme.channel_name.container) .aligned() - .left() - .flex(1., true), + .left(), ) .align_children_center() .contained() + .with_background_color( + theme + .container + .background_color + .unwrap_or(gpui::color::Color::transparent_black()), + ) + .contained() .with_padding_left( theme.channel_row.default_style().padding.left + theme.channel_indent * depth as f32, @@ -2816,6 +2840,19 @@ impl View for CollabPanel { self.has_focus = false; } + fn modifiers_changed(&mut self, _: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + { + cx.notify(); + true + } else { + false + } + } + fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let theme = &theme::current(cx).collab_panel; diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index 197c9918f54176e038efbd6e93887cc129c3f764..f58717a2cfa8fcf89da472f7f270ba7b9f140200 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -4,7 +4,7 @@ use collections::HashSet; use gpui::{ elements::{Empty, MouseEventHandler, Overlay}, geometry::{rect::RectF, vector::Vector2F}, - platform::{CursorStyle, MouseButton}, + platform::{CursorStyle, Modifiers, MouseButton}, scene::{MouseDown, MouseDrag}, AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext, }; @@ -21,12 +21,13 @@ enum State { region: RectF, }, Dragging { + modifiers: Modifiers, window: AnyWindowHandle, position: Vector2F, region_offset: Vector2F, region: RectF, payload: Rc, - render: Rc, &mut ViewContext) -> AnyElement>, + render: Rc, &mut ViewContext) -> AnyElement>, }, Canceled, } @@ -49,6 +50,7 @@ impl Clone for State { region, }, State::Dragging { + modifiers, window, position, region_offset, @@ -62,6 +64,7 @@ impl Clone for State { region: region.clone(), payload: payload.clone(), render: render.clone(), + modifiers: modifiers.clone(), }, State::Canceled => State::Canceled, } @@ -111,6 +114,27 @@ impl DragAndDrop { }) } + pub fn any_currently_dragged(&self, window: AnyWindowHandle) -> bool { + self.currently_dragged + .as_ref() + .map(|state| { + if let State::Dragging { + window: window_dragged_from, + .. + } = state + { + if &window != window_dragged_from { + return false; + } + + true + } else { + false + } + }) + .unwrap_or(false) + } + pub fn drag_started(event: MouseDown, cx: &mut WindowContext) { cx.update_global(|this: &mut Self, _| { this.currently_dragged = Some(State::Down { @@ -124,7 +148,7 @@ impl DragAndDrop { event: MouseDrag, payload: Rc, cx: &mut WindowContext, - render: Rc) -> AnyElement>, + render: Rc) -> AnyElement>, ) { let window = cx.window(); cx.update_global(|this: &mut Self, cx| { @@ -141,13 +165,14 @@ impl DragAndDrop { }) => { if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE { this.currently_dragged = Some(State::Dragging { + modifiers: event.modifiers, window, region_offset, region, position: event.position, payload, - render: Rc::new(move |payload, cx| { - render(payload.downcast_ref::().unwrap(), cx) + render: Rc::new(move |modifiers, payload, cx| { + render(modifiers, payload.downcast_ref::().unwrap(), cx) }), }); } else { @@ -163,13 +188,14 @@ impl DragAndDrop { .. }) => { this.currently_dragged = Some(State::Dragging { + modifiers: event.modifiers, window, region_offset, region, position: event.position, payload, - render: Rc::new(move |payload, cx| { - render(payload.downcast_ref::().unwrap(), cx) + render: Rc::new(move |modifiers, payload, cx| { + render(modifiers, payload.downcast_ref::().unwrap(), cx) }), }); } @@ -178,6 +204,19 @@ impl DragAndDrop { }); } + pub fn update_modifiers(new_modifiers: Modifiers, cx: &mut ViewContext) -> bool { + cx.update_global(|this: &mut Self, _| match &mut this.currently_dragged { + Some(state) => match state { + State::Dragging { modifiers, .. } => { + *modifiers = new_modifiers; + true + } + _ => false, + }, + None => false, + }) + } + pub fn render(cx: &mut ViewContext) -> Option> { enum DraggedElementHandler {} cx.global::() @@ -188,6 +227,7 @@ impl DragAndDrop { State::Down { .. } => None, State::DeadZone { .. } => None, State::Dragging { + modifiers, window, region_offset, position, @@ -205,7 +245,7 @@ impl DragAndDrop { MouseEventHandler::new::( 0, cx, - |_, cx| render(payload, cx), + |_, cx| render(&modifiers, payload, cx), ) .with_cursor_style(CursorStyle::Arrow) .on_up(MouseButton::Left, |_, _, cx| { @@ -295,7 +335,7 @@ pub trait Draggable { fn as_draggable( self, payload: P, - render: impl 'static + Fn(&P, &mut ViewContext) -> AnyElement, + render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext) -> AnyElement, ) -> Self where Self: Sized; @@ -305,7 +345,7 @@ impl Draggable for MouseEventHandler { fn as_draggable( self, payload: P, - render: impl 'static + Fn(&P, &mut ViewContext) -> AnyElement, + render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext) -> AnyElement, ) -> Self where Self: Sized, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 617870fcb4674b981c35924aa3e7bbe656f429ee..726a07acee7fd05ae5502243cad626b765c4727c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1485,7 +1485,7 @@ impl ProjectPanel { .as_draggable(entry_id, { let row_container_style = theme.dragged_entry.container; - move |_, cx: &mut ViewContext| { + move |_, _, cx: &mut ViewContext| { let theme = theme::current(cx).clone(); Self::render_entry_visual_element( &details, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1b48275ee98d53d4668f8c91ebc1fd0a2664e4a7..a3e6a547ddfd73c1fd67c50c4b6c4df2d6ff51d1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1383,7 +1383,7 @@ impl Pane { let theme = theme::current(cx).clone(); let detail = detail.clone(); - move |dragged_item: &DraggedItem, cx: &mut ViewContext| { + move |_, dragged_item: &DraggedItem, cx: &mut ViewContext| { let tab_style = &theme.workspace.tab_bar.dragged_tab; Self::render_dragged_tab( &dragged_item.handle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7d0f6db917f4d7024732f1fe37d9feffdc52e4fd..23c12722fbf8877abe9e517bbdbfa3e3fbf2cf30 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -33,8 +33,8 @@ use gpui::{ }, impl_actions, platform::{ - CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds, - WindowOptions, + CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, + WindowBounds, WindowOptions, }, AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, @@ -3807,6 +3807,10 @@ impl View for Workspace { cx.focus(&self.active_pane); } } + + fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { + DragAndDrop::::update_modifiers(e.modifiers, cx) + } } impl ViewId { From d5f0ce0e203c747447d9ae33ab35d030d8936e7e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 19 Sep 2023 15:49:19 -0700 Subject: [PATCH 25/26] Finish implementing drag and drop --- .../src/channel_store/channel_index.rs | 14 +++--- crates/collab_ui/src/collab_panel.rs | 44 +++++++------------ crates/drag_and_drop/src/drag_and_drop.rs | 13 ++++-- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 8fe2607f9e96542e4071914151998f0f2fc04450..95e750aad0dfe57b0635900e419452b2709ea2c7 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -6,16 +6,14 @@ use rpc::proto; use super::ChannelPath; -pub type ChannelsById = HashMap>; - #[derive(Default, Debug)] pub struct ChannelIndex { paths: Vec, - channels_by_id: ChannelsById, + channels_by_id: HashMap>, } impl ChannelIndex { - pub fn by_id(&self) -> &ChannelsById { + pub fn by_id(&self) -> &HashMap> { &self.channels_by_id } @@ -55,7 +53,7 @@ impl Deref for ChannelIndex { #[derive(Debug)] pub struct ChannelPathsInsertGuard<'a> { paths: &'a mut Vec, - channels_by_id: &'a mut ChannelsById, + channels_by_id: &'a mut HashMap>, } impl<'a> ChannelPathsInsertGuard<'a> { @@ -122,13 +120,13 @@ impl<'a> ChannelPathsInsertGuard<'a> { let mut new_path = Vec::with_capacity(parent.len() + 1); new_path.extend_from_slice(parent); new_path.push(channel_id); - new_paths.push(ChannelPath(new_path.into())); + new_paths.push(ChannelPath::new(new_path.into())); } else { for descendant in descendants.iter() { let mut new_path = Vec::with_capacity(parent.len() + descendant.len()); new_path.extend_from_slice(parent); new_path.extend_from_slice(descendant); - new_paths.push(ChannelPath(new_path.into())); + new_paths.push(ChannelPath::new(new_path.into())); } } } @@ -157,7 +155,7 @@ impl<'a> Drop for ChannelPathsInsertGuard<'a> { fn channel_path_sorting_key<'a>( path: &'a [ChannelId], - channels_by_id: &'a ChannelsById, + channels_by_id: &'a HashMap>, ) -> impl 'a + Iterator> { path.iter() .map(|id| Some(channels_by_id.get(id)?.name.as_str())) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4e5c793ef3fbcec947d38de9c68976cb532c3a8d..093164e3c252e83741586052f9e7bee1f46d01be 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -33,7 +33,7 @@ use gpui::{ vector::{vec2f, Vector2F}, }, impl_actions, - platform::{CursorStyle, ModifiersChangedEvent, MouseButton, PromptLevel}, + platform::{CursorStyle, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -263,7 +263,7 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, - dragged_channel_target: Option, + drag_target_channel: Option, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -529,7 +529,7 @@ impl CollabPanel { workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, - dragged_channel_target: None, + drag_target_channel: None, list_state, }; @@ -1672,7 +1672,7 @@ impl CollabPanel { .currently_dragged::(cx.window()) .is_some() && self - .dragged_channel_target + .drag_target_channel .as_ref() .filter(|(_, dragged_path)| path.starts_with(dragged_path)) .is_some() @@ -1771,7 +1771,7 @@ impl CollabPanel { ) }) .on_click(MouseButton::Left, move |_, this, cx| { - if this.dragged_channel_target.take().is_none() { + if this.drag_target_channel.take().is_none() { this.join_channel_chat(channel_id, cx); } }) @@ -1814,19 +1814,20 @@ impl CollabPanel { let channel = channel.clone(); let path = path.clone(); move |_, this, cx| { - if cx - .global::>() - .currently_dragged::(cx.window()) - .is_some() + if let Some((_, _dragged_channel)) = + cx.global::>() + .currently_dragged::(cx.window()) { - if let Some(dragged_channel_target) = &this.dragged_channel_target { - if dragged_channel_target.0 != channel || dragged_channel_target.1 != path { - this.dragged_channel_target = Some((channel.clone(), path.clone())); + match &this.drag_target_channel { + Some(current_target) + if current_target.0 == channel && current_target.1 == path => + { + return + } + _ => { + this.drag_target_channel = Some((channel.clone(), path.clone())); cx.notify(); } - } else { - this.dragged_channel_target = Some((channel.clone(), path.clone())); - cx.notify(); } } } @@ -2840,19 +2841,6 @@ impl View for CollabPanel { self.has_focus = false; } - fn modifiers_changed(&mut self, _: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { - if cx - .global::>() - .currently_dragged::(cx.window()) - .is_some() - { - cx.notify(); - true - } else { - false - } - } - fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let theme = &theme::current(cx).collab_panel; diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index f58717a2cfa8fcf89da472f7f270ba7b9f140200..dc778759facb66bcaef2a1ec96811077b38d25a9 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -185,10 +185,11 @@ impl DragAndDrop { Some(&State::Dragging { region_offset, region, + modifiers, .. }) => { this.currently_dragged = Some(State::Dragging { - modifiers: event.modifiers, + modifiers, window, region_offset, region, @@ -205,7 +206,7 @@ impl DragAndDrop { } pub fn update_modifiers(new_modifiers: Modifiers, cx: &mut ViewContext) -> bool { - cx.update_global(|this: &mut Self, _| match &mut this.currently_dragged { + let result = cx.update_global(|this: &mut Self, _| match &mut this.currently_dragged { Some(state) => match state { State::Dragging { modifiers, .. } => { *modifiers = new_modifiers; @@ -214,7 +215,13 @@ impl DragAndDrop { _ => false, }, None => false, - }) + }); + + if result { + cx.notify(); + } + + result } pub fn render(cx: &mut ViewContext) -> Option> { From ac65e7590c7b284757f79d41bd7e9c5d87a9f08b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 19 Sep 2023 17:48:43 -0700 Subject: [PATCH 26/26] Add hover styles to channels matching the current selection Fix chat desync from moving / linking channels --- assets/keymaps/default.json | 8 + crates/channel/src/channel_store.rs | 23 +- .../src/channel_store/channel_index.rs | 10 +- crates/channel/src/channel_store_tests.rs | 4 +- crates/collab/src/tests/channel_tests.rs | 9 +- .../src/tests/random_channel_buffer_tests.rs | 4 +- crates/collab_ui/src/chat_panel.rs | 6 +- crates/collab_ui/src/collab_panel.rs | 314 +++++++++++++----- 8 files changed, 284 insertions(+), 94 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2211f9563d517ffca3e621c00c87501bc6a5d88c..14b9d01b21513a2699c7533964c8c8375c4a7bd3 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -585,6 +585,14 @@ "space": "menu::Confirm" } }, + { + "context": "CollabPanel > Editor", + "bindings": { + "cmd-c": "collab_panel::StartLinkChannel", + "cmd-x": "collab_panel::StartMoveChannel", + "cmd-v": "collab_panel::MoveOrLinkToSelected" + } + }, { "context": "ChannelModal", "bindings": { diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 5e2e10516538003c4e072e653848aac778d6e5f9..ee1929f720c9a7a6d0023fc32681e862dfbe0419 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -146,17 +146,26 @@ impl ChannelStore { }) } + /// Returns the number of unique channels in the store pub fn channel_count(&self) -> usize { - self.channel_index.len() + self.channel_index.by_id().len() } + /// Returns the index of a channel ID in the list of unique channels pub fn index_of_channel(&self, channel_id: ChannelId) -> Option { self.channel_index - .iter() - .position(|path| path.ends_with(&[channel_id])) + .by_id() + .keys() + .position(|id| *id == channel_id) } - pub fn channels(&self) -> impl '_ + Iterator)> { + /// Returns an iterator over all unique channels + pub fn channels(&self) -> impl '_ + Iterator> { + self.channel_index.by_id().values() + } + + /// Iterate over all entries in the channel DAG + pub fn channel_dag_entries(&self) -> impl '_ + Iterator)> { self.channel_index.iter().map(move |path| { let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); @@ -164,7 +173,7 @@ impl ChannelStore { }) } - pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc, &ChannelPath)> { + pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc, &ChannelPath)> { let path = self.channel_index.get(ix)?; let id = path.last().unwrap(); let channel = self.channel_for_id(*id).unwrap(); @@ -172,6 +181,10 @@ impl ChannelStore { Some((channel, path)) } + pub fn channel_at(&self, ix: usize) -> Option<&Arc> { + self.channel_index.by_id().values().nth(ix) + } + pub fn channel_invitations(&self) -> &[Arc] { &self.channel_invitations } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 95e750aad0dfe57b0635900e419452b2709ea2c7..d0c49dc298be882acc22cc33bdeccf21872e4c91 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,7 +1,7 @@ use std::{ops::Deref, sync::Arc}; use crate::{Channel, ChannelId}; -use collections::HashMap; +use collections::BTreeMap; use rpc::proto; use super::ChannelPath; @@ -9,11 +9,11 @@ use super::ChannelPath; #[derive(Default, Debug)] pub struct ChannelIndex { paths: Vec, - channels_by_id: HashMap>, + channels_by_id: BTreeMap>, } impl ChannelIndex { - pub fn by_id(&self) -> &HashMap> { + pub fn by_id(&self) -> &BTreeMap> { &self.channels_by_id } @@ -53,7 +53,7 @@ impl Deref for ChannelIndex { #[derive(Debug)] pub struct ChannelPathsInsertGuard<'a> { paths: &'a mut Vec, - channels_by_id: &'a mut HashMap>, + channels_by_id: &'a mut BTreeMap>, } impl<'a> ChannelPathsInsertGuard<'a> { @@ -155,7 +155,7 @@ impl<'a> Drop for ChannelPathsInsertGuard<'a> { fn channel_path_sorting_key<'a>( path: &'a [ChannelId], - channels_by_id: &'a HashMap>, + channels_by_id: &'a BTreeMap>, ) -> impl 'a + Iterator> { path.iter() .map(|id| Some(channels_by_id.get(id)?.name.as_str())) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 775bf2942541a19acf0730d3ccb79fba2e977fc9..41acafa3a30525b4c1fd54ecf479a674b2f67df0 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -181,7 +181,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { // Join a channel and populate its existing messages. let channel = channel_store.update(cx, |store, cx| { - let channel_id = store.channels().next().unwrap().1.id; + let channel_id = store.channel_dag_entries().next().unwrap().1.id; store.open_channel_chat(channel_id, cx) }); let join_channel = server.receive::().await.unwrap(); @@ -363,7 +363,7 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channels() + .channel_dag_entries() .map(|(depth, channel)| { ( depth, diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 906461a10e29117e5b42ce164cb8939ccaf114e6..6b300282a1740c2d349cf1bdd2a0b82a304a16d5 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -56,7 +56,10 @@ async fn test_core_channels( ); client_b.channel_store().read_with(cx_b, |channels, _| { - assert!(channels.channels().collect::>().is_empty()) + assert!(channels + .channel_dag_entries() + .collect::>() + .is_empty()) }); // Invite client B to channel A as client A. @@ -1170,7 +1173,7 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channels() + .channel_dag_entries() .map(|(depth, channel)| ExpectedChannel { depth, name: channel.name.clone(), @@ -1192,7 +1195,7 @@ fn assert_channels_list_shape( let actual = channel_store.read_with(cx, |store, _| { store - .channels() + .channel_dag_entries() .map(|(depth, channel)| (channel.id, depth)) .collect::>() }); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index a60d3d7d7d6c14d54fb7e7129ba414e148ff90c1..2950922e7c63fb1c4e8d44da4f38bdbe859a2a94 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -86,7 +86,7 @@ impl RandomizedTest for RandomChannelBufferTest { match rng.gen_range(0..100_u32) { 0..=29 => { let channel_name = client.channel_store().read_with(cx, |store, cx| { - store.channels().find_map(|(_, channel)| { + store.channel_dag_entries().find_map(|(_, channel)| { if store.has_open_channel_buffer(channel.id, cx) { None } else { @@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest { ChannelBufferOperation::JoinChannelNotes { channel_name } => { let buffer = client.channel_store().update(cx, |store, cx| { let channel_id = store - .channels() + .channel_dag_entries() .find(|(_, c)| c.name == channel_name) .unwrap() .1 diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 4200ada36bfa939f4fc845ab039b8edbae5e5472..082702fedab6affba62dc88b9d5ee591b72b8938 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -166,8 +166,8 @@ impl ChatPanel { let selected_channel_id = this .channel_store .read(cx) - .channel_at_index(selected_ix) - .map(|e| e.0.id); + .channel_at(selected_ix) + .map(|e| e.id); if let Some(selected_channel_id) = selected_channel_id { this.select_channel(selected_channel_id, cx) .detach_and_log_err(cx); @@ -391,7 +391,7 @@ impl ChatPanel { (ItemType::Unselected, true) => &theme.channel_select.hovered_item, }; - let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().0; + let channel = &channel_store.read(cx).channel_at(ix).unwrap(); let channel_id = channel.id; let mut row = Flex::row() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 093164e3c252e83741586052f9e7bee1f46d01be..4bc4e91ae4f06dd52a18e379c668d0c9e831dc76 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -23,9 +23,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, - Stack, Svg, + Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset, + ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, + SafeStylable, Stack, Svg, }, fonts::TextStyle, geometry::{ @@ -42,7 +42,7 @@ use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use std::{borrow::Cow, hash::Hash, mem, sync::Arc}; -use theme::{components::ComponentExt, IconButton}; +use theme::{components::ComponentExt, IconButton, Interactive}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -65,6 +65,11 @@ struct RenameChannel { location: ChannelPath, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ToggleSelectedIx { + ix: usize, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { channel_id: ChannelId, @@ -96,7 +101,13 @@ struct OpenChannelBuffer { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct StartMoveChannel { +struct StartMoveChannelFor { + channel_id: ChannelId, + parent_id: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct StartLinkChannelFor { channel_id: ChannelId, parent_id: Option, } @@ -126,7 +137,10 @@ actions!( Remove, Secondary, CollapseSelectedChannel, - ExpandSelectedChannel + ExpandSelectedChannel, + StartMoveChannel, + StartLinkChannel, + MoveOrLinkToSelected, ] ); @@ -143,12 +157,27 @@ impl_actions!( JoinChannelCall, OpenChannelBuffer, LinkChannel, - StartMoveChannel, + StartMoveChannelFor, + StartLinkChannelFor, MoveChannel, - UnlinkChannel + UnlinkChannel, + ToggleSelectedIx ] ); +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct ChannelMoveClipboard { + channel_id: ChannelId, + parent_id: Option, + intent: ClipboardIntent, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum ClipboardIntent { + Move, + Link, +} + const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; pub fn init(cx: &mut AppContext) { @@ -175,17 +204,99 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::open_channel_notes); cx.add_action( - |panel: &mut CollabPanel, action: &StartMoveChannel, _: &mut ViewContext| { - panel.channel_move = Some(*action); + |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { + if panel.selection.take() != Some(action.ix) { + panel.selection = Some(action.ix) + } + + cx.notify(); + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, + action: &StartMoveChannelFor, + _: &mut ViewContext| { + panel.channel_clipboard = Some(ChannelMoveClipboard { + channel_id: action.channel_id, + parent_id: action.parent_id, + intent: ClipboardIntent::Move, + }); + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, + action: &StartLinkChannelFor, + _: &mut ViewContext| { + panel.channel_clipboard = Some(ChannelMoveClipboard { + channel_id: action.channel_id, + parent_id: action.parent_id, + intent: ClipboardIntent::Link, + }) + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { + if let Some((_, path)) = panel.selected_channel() { + panel.channel_clipboard = Some(ChannelMoveClipboard { + channel_id: path.channel_id(), + parent_id: path.parent_id(), + intent: ClipboardIntent::Move, + }) + } + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext| { + if let Some((_, path)) = panel.selected_channel() { + panel.channel_clipboard = Some(ChannelMoveClipboard { + channel_id: path.channel_id(), + parent_id: path.parent_id(), + intent: ClipboardIntent::Link, + }) + } + }, + ); + + cx.add_action( + |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext| { + let clipboard = panel.channel_clipboard.take(); + if let Some(((selected_channel, _), clipboard)) = + panel.selected_channel().zip(clipboard) + { + match clipboard.intent { + ClipboardIntent::Move if clipboard.parent_id.is_some() => { + let parent_id = clipboard.parent_id.unwrap(); + panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .move_channel( + clipboard.channel_id, + parent_id, + selected_channel.id, + cx, + ) + .detach_and_log_err(cx) + }) + } + _ => panel.channel_store.update(cx, |channel_store, cx| { + channel_store + .link_channel(clipboard.channel_id, selected_channel.id, cx) + .detach_and_log_err(cx) + }), + } + } }, ); cx.add_action( |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext| { - if let Some(move_start) = panel.channel_move.take() { + if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { channel_store - .link_channel(move_start.channel_id, action.to, cx) + .link_channel(clipboard.channel_id, action.to, cx) .detach_and_log_err(cx) }) } @@ -194,15 +305,15 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { - if let Some(move_start) = panel.channel_move.take() { + if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { - if let Some(parent) = move_start.parent_id { + if let Some(parent) = clipboard.parent_id { channel_store - .move_channel(move_start.channel_id, parent, action.to, cx) + .move_channel(clipboard.channel_id, parent, action.to, cx) .detach_and_log_err(cx) } else { channel_store - .link_channel(move_start.channel_id, action.to, cx) + .link_channel(clipboard.channel_id, action.to, cx) .detach_and_log_err(cx) } }) @@ -246,7 +357,7 @@ pub struct CollabPanel { width: Option, fs: Arc, has_focus: bool, - channel_move: Option, + channel_clipboard: Option, pending_serialization: Task>, context_menu: ViewHandle, filter_editor: ViewHandle, @@ -444,6 +555,7 @@ impl CollabPanel { path.to_owned(), &theme.collab_panel, is_selected, + ix, cx, ); @@ -510,7 +622,7 @@ impl CollabPanel { let mut this = Self { width: None, has_focus: false, - channel_move: None, + channel_clipboard: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), @@ -776,16 +888,13 @@ impl CollabPanel { if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { self.match_candidates.clear(); self.match_candidates - .extend( - channel_store - .channels() - .enumerate() - .map(|(ix, (_, channel))| StringMatchCandidate { - id: ix, - string: channel.name.clone(), - char_bag: channel.name.chars().collect(), - }), - ); + .extend(channel_store.channel_dag_entries().enumerate().map( + |(ix, (_, channel))| StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + }, + )); let matches = executor.block(match_strings( &self.match_candidates, &query, @@ -801,7 +910,9 @@ impl CollabPanel { } let mut collapse_depth = None; for mat in matches { - let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let (channel, path) = channel_store + .channel_dag_entry_at(mat.candidate_id) + .unwrap(); let depth = path.len() - 1; if collapse_depth.is_none() && self.is_channel_collapsed(path) { @@ -1627,7 +1738,7 @@ impl CollabPanel { .constrained() .with_height(theme.collab_panel.row_height) .contained() - .with_style(gpui::elements::ContainerStyle { + .with_style(ContainerStyle { background_color: Some(theme.editor.background), ..*theme.collab_panel.contact_row.default_style() }) @@ -1645,11 +1756,13 @@ impl CollabPanel { path: ChannelPath, theme: &theme::CollabPanel, is_selected: bool, + ix: usize, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; let has_children = self.channel_store.read(cx).has_children(channel_id); - + let other_selected = + self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); let is_active = iife!({ @@ -1683,6 +1796,20 @@ impl CollabPanel { MouseEventHandler::new::(path.unique_id() as usize, cx, |state, cx| { let row_hovered = state.hovered(); + let mut select_state = |interactive: &Interactive| { + if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { + interactive.clicked.as_ref().unwrap().clone() + } else if state.hovered() || other_selected { + interactive + .hovered + .as_ref() + .unwrap_or(&interactive.default) + .clone() + } else { + interactive.default.clone() + } + }; + Flex::::row() .with_child( Svg::new("icons/hash.svg") @@ -1760,11 +1887,11 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style( - *theme + .with_style(select_state( + theme .channel_row - .style_for(is_selected || is_active || is_dragged_over, state), - ) + .in_state(is_selected || is_active || is_dragged_over), + )) .with_padding_left( theme.channel_row.default_style().padding.left + theme.channel_indent * depth as f32, @@ -1778,7 +1905,7 @@ impl CollabPanel { .on_click(MouseButton::Right, { let path = path.clone(); move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), &path, cx); + this.deploy_channel_context_menu(Some(e.position), &path, ix, cx); } }) .on_up(MouseButton::Left, move |e, this, cx| { @@ -2119,15 +2246,34 @@ impl CollabPanel { .into_any() } + fn has_subchannels(&self, ix: usize) -> bool { + self.entries + .get(ix) + .zip(self.entries.get(ix + 1)) + .map(|entries| match entries { + ( + ListEntry::Channel { + path: this_path, .. + }, + ListEntry::Channel { + path: next_path, .. + }, + ) => next_path.starts_with(this_path), + _ => false, + }) + .unwrap_or(false) + } + fn deploy_channel_context_menu( &mut self, position: Option, path: &ChannelPath, + ix: usize, cx: &mut ViewContext, ) { self.context_menu_on_selected = position.is_none(); - let channel_name = self.channel_move.as_ref().and_then(|channel| { + let channel_name = self.channel_clipboard.as_ref().and_then(|channel| { let channel_name = self .channel_store .read(cx) @@ -2145,42 +2291,37 @@ impl CollabPanel { let mut items = Vec::new(); - if let Some(channel_name) = channel_name { - items.push(ContextMenuItem::action( - format!("Move '#{}' here", channel_name), - MoveChannel { - to: path.channel_id(), - }, - )); - items.push(ContextMenuItem::action( - format!("Link '#{}' here", channel_name), - LinkChannel { - to: path.channel_id(), - }, - )); - items.push(ContextMenuItem::Separator) - } - - let expand_action_name = if self.is_channel_collapsed(&path) { - "Expand Subchannels" + let select_action_name = if self.selection == Some(ix) { + "Unselect" } else { - "Collapse Subchannels" + "Select" }; - items.extend([ - ContextMenuItem::action( + items.push(ContextMenuItem::action( + select_action_name, + ToggleSelectedIx { ix }, + )); + + if self.has_subchannels(ix) { + let expand_action_name = if self.is_channel_collapsed(&path) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + items.push(ContextMenuItem::action( expand_action_name, ToggleCollapse { location: path.clone(), }, - ), - ContextMenuItem::action( - "Open Notes", - OpenChannelBuffer { - channel_id: path.channel_id(), - }, - ), - ]); + )); + } + + items.push(ContextMenuItem::action( + "Open Notes", + OpenChannelBuffer { + channel_id: path.channel_id(), + }, + )); if self.channel_store.read(cx).is_user_admin(path.channel_id()) { let parent_id = path.parent_id(); @@ -2212,13 +2353,38 @@ impl CollabPanel { )); } - items.extend([ContextMenuItem::action( - "Move this channel", - StartMoveChannel { - channel_id: path.channel_id(), - parent_id, - }, - )]); + items.extend([ + ContextMenuItem::action( + "Move this channel", + StartMoveChannelFor { + channel_id: path.channel_id(), + parent_id, + }, + ), + ContextMenuItem::action( + "Link this channel", + StartLinkChannelFor { + channel_id: path.channel_id(), + parent_id, + }, + ), + ]); + + if let Some(channel_name) = channel_name { + items.push(ContextMenuItem::Separator); + items.push(ContextMenuItem::action( + format!("Move '#{}' here", channel_name), + MoveChannel { + to: path.channel_id(), + }, + )); + items.push(ContextMenuItem::action( + format!("Link '#{}' here", channel_name), + LinkChannel { + to: path.channel_id(), + }, + )); + } items.extend([ ContextMenuItem::Separator, @@ -2598,7 +2764,7 @@ impl CollabPanel { return; }; - self.deploy_channel_context_menu(None, &path.to_owned(), cx); + self.deploy_channel_context_menu(None, &path.to_owned(), self.selection.unwrap(), cx); } fn selected_channel(&self) -> Option<(&Arc, &ChannelPath)> {