diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index dfdb5fe9ed..99501bbd2a 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -51,6 +51,10 @@ impl ChannelStore { &self.channel_invitations } + pub fn channel_for_id(&self, channel_id: u64) -> Option> { + self.channels.iter().find(|c| c.id == channel_id).cloned() + } + pub fn create_channel( &self, name: &str, @@ -103,6 +107,14 @@ impl ChannelStore { false } + pub fn remove_channel(&self, channel_id: u64) -> impl Future> { + let client = self.client.clone(); + async move { + client.request(proto::RemoveChannel { channel_id }).await?; + Ok(()) + } + } + pub fn remove_member( &self, channel_id: u64, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index a48b2849ae..1e86cef4cc 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -575,7 +575,10 @@ impl Client { }), ); if prev_handler.is_some() { - panic!("registered handler for the same message twice"); + panic!( + "registered handler for the same message {} twice", + std::any::type_name::() + ); } Subscription::Message { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 12e02b06ed..066c93ec71 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -44,6 +44,7 @@ use serde::{Deserialize, Serialize}; pub use signup::{Invite, NewSignup, WaitlistSummary}; use sqlx::migrate::{Migrate, Migration, MigrationSource}; use sqlx::Connection; +use std::fmt::Write as _; use std::ops::{Deref, DerefMut}; use std::path::Path; use std::time::Duration; @@ -3131,6 +3132,74 @@ impl Database { .await } + pub async fn remove_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result<(Vec, Vec)> { + self.transaction(move |tx| async move { + let tx = tx; + + // Check if user is an admin + channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Admin.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not allowed to remove this channel"))?; + + let mut descendants = self.get_channel_descendants([channel_id], &*tx).await?; + + // Keep channels which have another active + let mut channels_to_keep = channel_parent::Entity::find() + .filter( + channel_parent::Column::ChildId + .is_in(descendants.keys().copied().filter(|&id| id != channel_id)) + .and( + channel_parent::Column::ParentId.is_not_in(descendants.keys().copied()), + ), + ) + .stream(&*tx) + .await?; + + while let Some(row) = channels_to_keep.next().await { + let row = row?; + descendants.remove(&row.child_id); + } + + drop(channels_to_keep); + + let channels_to_remove = descendants.keys().copied().collect::>(); + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } + + let members_to_notify: Vec = channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channels_to_remove.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .distinct() + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + // Channel members and parents should delete via cascade + channel::Entity::delete_many() + .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) + .exec(&*tx) + .await?; + + Ok((channels_to_remove, members_to_notify)) + }) + .await + } + pub async fn invite_channel_member( &self, channel_id: ChannelId, @@ -3256,50 +3325,32 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - // Breadth first list of all edges in this user's channels - let sql = r#" - WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS ( - SELECT channel_id as child_id, CAST(NULL as INTEGER) as parent_id, 0 - FROM channel_members - WHERE user_id = $1 AND accepted - UNION - SELECT channel_parents.child_id, channel_parents.parent_id, channel_tree.depth + 1 - FROM channel_parents, channel_tree - WHERE channel_parents.parent_id = channel_tree.child_id - ) - SELECT channel_tree.child_id, channel_tree.parent_id - FROM channel_tree - ORDER BY child_id, parent_id IS NOT NULL - "#; - - #[derive(FromQueryResult, Debug, PartialEq)] - pub struct ChannelParent { - pub child_id: ChannelId, - pub parent_id: Option, + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, } - let stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - vec![user_id.into()], - ); + let starting_channel_ids: Vec = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .select_only() + .column(channel_member::Column::ChannelId) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; - let mut parents_by_child_id = HashMap::default(); - let mut parents = channel_parent::Entity::find() - .from_raw_sql(stmt) - .into_model::() - .stream(&*tx).await?; - while let Some(parent) = parents.next().await { - let parent = parent?; - parents_by_child_id.insert(parent.child_id, parent.parent_id); - } - - drop(parents); + let parents_by_child_id = self + .get_channel_descendants(starting_channel_ids, &*tx) + .await?; 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?; + .stream(&*tx) + .await?; while let Some(row) = rows.next().await { let row = row?; @@ -3317,18 +3368,73 @@ impl Database { .await } - pub async fn get_channel(&self, channel_id: ChannelId) -> Result { + async fn get_channel_descendants( + &self, + channel_ids: impl IntoIterator, + tx: &DatabaseTransaction, + ) -> Result>> { + let mut values = String::new(); + for id in channel_ids { + if !values.is_empty() { + values.push_str(", "); + } + write!(&mut values, "({})", id).unwrap(); + } + + if values.is_empty() { + return Ok(HashMap::default()); + } + + let sql = format!( + r#" + WITH RECURSIVE channel_tree(child_id, parent_id) AS ( + SELECT root_ids.column1 as child_id, CAST(NULL as INTEGER) as parent_id + FROM (VALUES {}) as root_ids + UNION + SELECT channel_parents.child_id, channel_parents.parent_id + FROM channel_parents, channel_tree + WHERE channel_parents.parent_id = channel_tree.child_id + ) + SELECT channel_tree.child_id, channel_tree.parent_id + FROM channel_tree + ORDER BY child_id, parent_id IS NOT NULL + "#, + values + ); + + #[derive(FromQueryResult, Debug, PartialEq)] + pub struct ChannelParent { + pub child_id: ChannelId, + pub parent_id: Option, + } + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + + let mut parents_by_child_id = HashMap::default(); + let mut parents = channel_parent::Entity::find() + .from_raw_sql(stmt) + .into_model::() + .stream(tx) + .await?; + + while let Some(parent) = parents.next().await { + let parent = parent?; + parents_by_child_id.insert(parent.child_id, parent.parent_id); + } + + Ok(parents_by_child_id) + } + + pub async fn get_channel(&self, channel_id: ChannelId) -> Result> { self.transaction(|tx| async move { let tx = tx; - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such channel"))?; - Ok(Channel { + let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + + Ok(channel.map(|channel| Channel { id: channel.id, name: channel.name, parent_id: None, - }) + })) }) .await } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 64ab03e02d..3a47097f7d 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -918,6 +918,11 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .await .unwrap(); + let cargo_ra_id = db + .create_channel("cargo-ra", Some(cargo_id), "7", a_id) + .await + .unwrap(); + let channels = db.get_channels(a_id).await.unwrap(); assert_eq!( @@ -952,9 +957,28 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { 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), } ] ); + + // Remove a single channel + db.remove_channel(crdb_id, a_id).await.unwrap(); + assert!(db.get_channel(crdb_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).await.unwrap().is_none()); + assert!(db.get_channel(cargo_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_ra_id).await.unwrap().is_none()); }); test_both_dbs!( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6461f67c38..1465c66601 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -243,6 +243,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(invite_channel_member) .add_request_handler(remove_channel_member) .add_request_handler(respond_to_channel_invite) @@ -529,7 +530,6 @@ impl Server { this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_channels_update(channels, channel_invites))?; - if let Some((code, count)) = invite_code { this.peer.send(connection_id, proto::UpdateInviteInfo { url: format!("{}{}", this.app_state.config.invite_link_prefix, code), @@ -2101,7 +2101,6 @@ async fn create_channel( response: Response, session: Session, ) -> Result<()> { - dbg!(&request); let db = session.db().await; let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); @@ -2132,6 +2131,35 @@ async fn create_channel( Ok(()) } +async fn remove_channel( + request: proto::RemoveChannel, + 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) + .await?; + response.send(proto::Ack {})?; + + // Notify members of removed channels + let mut update = proto::UpdateChannels::default(); + update + .remove_channels + .extend(removed_channels.into_iter().map(|id| id.to_proto())); + + let connection_pool = session.connection_pool().await; + for member_id in member_ids { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + Ok(()) +} + async fn invite_channel_member( request: proto::InviteChannelMember, response: Response, @@ -2139,7 +2167,10 @@ async fn invite_channel_member( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db.get_channel(channel_id).await?; + let channel = db + .get_channel(channel_id) + .await? + .ok_or_else(|| anyhow!("channel not found"))?; let invitee_id = UserId::from_proto(request.user_id); db.invite_channel_member(channel_id, invitee_id, session.user_id, false) .await?; @@ -2177,7 +2208,10 @@ async fn respond_to_channel_invite( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db.get_channel(channel_id).await?; + let channel = db + .get_channel(channel_id) + .await? + .ok_or_else(|| anyhow!("no such channel"))?; db.respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 98ad2afb8a..e0346dbe7f 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -14,8 +14,8 @@ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; use gpui::{ - elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View, - ViewContext, ViewHandle, WeakViewHandle, + elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, Task, TestAppContext, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use parking_lot::Mutex; @@ -197,7 +197,7 @@ impl TestServer { languages: Arc::new(LanguageRegistry::test()), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _, _| unimplemented!(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], }); @@ -218,13 +218,9 @@ impl TestServer { .unwrap(); let client = TestClient { - client, + app_state, username: name.to_string(), state: Default::default(), - user_store, - channel_store, - fs, - language_registry: Arc::new(LanguageRegistry::test()), }; client.wait_for_current_user(cx).await; client @@ -252,6 +248,7 @@ impl TestServer { let (client_a, cx_a) = left.last_mut().unwrap(); for (client_b, cx_b) in right { client_a + .app_state .user_store .update(*cx_a, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) @@ -260,6 +257,7 @@ impl TestServer { .unwrap(); cx_a.foreground().run_until_parked(); client_b + .app_state .user_store .update(*cx_b, |store, cx| { store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) @@ -278,6 +276,7 @@ impl TestServer { ) -> u64 { let (admin_client, admin_cx) = admin; let channel_id = admin_client + .app_state .channel_store .update(admin_cx, |channel_store, _| { channel_store.create_channel(channel, None) @@ -287,6 +286,7 @@ impl TestServer { for (member_client, member_cx) in members { admin_client + .app_state .channel_store .update(admin_cx, |channel_store, _| { channel_store.invite_member(channel_id, member_client.user_id().unwrap(), false) @@ -297,6 +297,7 @@ impl TestServer { admin_cx.foreground().run_until_parked(); member_client + .app_state .channel_store .update(*member_cx, |channels, _| { channels.respond_to_channel_invite(channel_id, true) @@ -359,13 +360,9 @@ impl Drop for TestServer { } struct TestClient { - client: Arc, username: String, state: RefCell, - pub user_store: ModelHandle, - pub channel_store: ModelHandle, - language_registry: Arc, - fs: Arc, + app_state: Arc, } #[derive(Default)] @@ -379,7 +376,7 @@ impl Deref for TestClient { type Target = Arc; fn deref(&self) -> &Self::Target { - &self.client + &self.app_state.client } } @@ -390,22 +387,45 @@ struct ContactsSummary { } impl TestClient { + pub fn fs(&self) -> &FakeFs { + self.app_state.fs.as_fake() + } + + pub fn channel_store(&self) -> &ModelHandle { + &self.app_state.channel_store + } + + pub fn user_store(&self) -> &ModelHandle { + &self.app_state.user_store + } + + pub fn language_registry(&self) -> &Arc { + &self.app_state.languages + } + + pub fn client(&self) -> &Arc { + &self.app_state.client + } + pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { UserId::from_proto( - self.user_store + self.app_state + .user_store .read_with(cx, |user_store, _| user_store.current_user().unwrap().id), ) } async fn wait_for_current_user(&self, cx: &TestAppContext) { let mut authed_user = self + .app_state .user_store .read_with(cx, |user_store, _| user_store.watch_current_user()); while authed_user.next().await.unwrap().is_none() {} } async fn clear_contacts(&self, cx: &mut TestAppContext) { - self.user_store + self.app_state + .user_store .update(cx, |store, _| store.clear_contacts()) .await; } @@ -443,23 +463,25 @@ impl TestClient { } fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { - self.user_store.read_with(cx, |store, _| ContactsSummary { - current: store - .contacts() - .iter() - .map(|contact| contact.user.github_login.clone()) - .collect(), - outgoing_requests: store - .outgoing_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - incoming_requests: store - .incoming_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - }) + self.app_state + .user_store + .read_with(cx, |store, _| ContactsSummary { + current: store + .contacts() + .iter() + .map(|contact| contact.user.github_login.clone()) + .collect(), + outgoing_requests: store + .outgoing_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + incoming_requests: store + .incoming_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + }) } async fn build_local_project( @@ -469,10 +491,10 @@ impl TestClient { ) -> (ModelHandle, WorktreeId) { let project = cx.update(|cx| { Project::local( - self.client.clone(), - self.user_store.clone(), - self.language_registry.clone(), - self.fs.clone(), + self.client().clone(), + self.app_state.user_store.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), cx, ) }); @@ -498,8 +520,8 @@ impl TestClient { room.update(guest_cx, |room, cx| { room.join_project( host_project_id, - self.language_registry.clone(), - self.fs.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), cx, ) }) @@ -541,7 +563,9 @@ impl TestClient { // We use a workspace container so that we don't need to remove the window in order to // drop the workspace and we can use a ViewHandle instead. let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None }); - let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx.add_view(window_id, |cx| { + Workspace::new(0, project.clone(), self.app_state.clone(), cx) + }); container.update(cx, |container, cx| { container.workspace = Some(workspace.downgrade()); cx.notify(); @@ -552,7 +576,7 @@ impl TestClient { impl Drop for TestClient { fn drop(&mut self) { - self.client.teardown(); + self.app_state.client.teardown(); } } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index ffd517f52a..14363b74cf 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -19,14 +19,14 @@ async fn test_basic_channels( let client_b = server.create_client(cx_b, "user_b").await; let channel_a_id = client_a - .channel_store + .channel_store() .update(cx_a, |channel_store, _| { channel_store.create_channel("channel-a", None) }) .await .unwrap(); - client_a.channel_store.read_with(cx_a, |channels, _| { + client_a.channel_store().read_with(cx_a, |channels, _| { assert_eq!( channels.channels(), &[Arc::new(Channel { @@ -39,12 +39,12 @@ async fn test_basic_channels( }); client_b - .channel_store + .channel_store() .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); // Invite client B to channel A as client A. client_a - .channel_store + .channel_store() .update(cx_a, |channel_store, _| { channel_store.invite_member(channel_a_id, client_b.user_id().unwrap(), false) }) @@ -54,7 +54,7 @@ async fn test_basic_channels( // Wait for client b to see the invitation deterministic.run_until_parked(); - client_b.channel_store.read_with(cx_b, |channels, _| { + client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!( channels.channel_invitations(), &[Arc::new(Channel { @@ -68,13 +68,13 @@ async fn test_basic_channels( // Client B now sees that they are in channel A. client_b - .channel_store + .channel_store() .update(cx_b, |channels, _| { channels.respond_to_channel_invite(channel_a_id, true) }) .await .unwrap(); - client_b.channel_store.read_with(cx_b, |channels, _| { + client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!(channels.channel_invitations(), &[]); assert_eq!( channels.channels(), @@ -86,6 +86,23 @@ async fn test_basic_channels( })] ) }); + + // Client A deletes the channel + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_a_id) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + client_a + .channel_store() + .read_with(cx_a, |channels, _| assert_eq!(channels.channels(), &[])); + client_b + .channel_store() + .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); } #[gpui::test] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5a27787dbc..93ebb812ad 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -749,7 +749,7 @@ async fn test_server_restarts( let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; client_a - .fs + .fs() .insert_tree("/a", json!({ "a.txt": "a-contents" })) .await; @@ -1221,7 +1221,7 @@ async fn test_share_project( let active_call_c = cx_c.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -1388,7 +1388,7 @@ async fn test_unshare_project( let active_call_b = cx_b.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -1477,7 +1477,7 @@ async fn test_host_disconnect( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -1500,7 +1500,7 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); let (window_id_b, workspace_b) = - cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, cx) @@ -1584,7 +1584,7 @@ async fn test_project_reconnect( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/root-1", json!({ @@ -1612,7 +1612,7 @@ async fn test_project_reconnect( ) .await; client_a - .fs + .fs() .insert_tree( "/root-2", json!({ @@ -1621,7 +1621,7 @@ async fn test_project_reconnect( ) .await; client_a - .fs + .fs() .insert_tree( "/root-3", json!({ @@ -1701,7 +1701,7 @@ async fn test_project_reconnect( // While client A is disconnected, add and remove files from client A's project. client_a - .fs + .fs() .insert_tree( "/root-1/dir1/subdir2", json!({ @@ -1713,7 +1713,7 @@ async fn test_project_reconnect( ) .await; client_a - .fs + .fs() .remove_dir( "/root-1/dir1/subdir1".as_ref(), RemoveOptions { @@ -1835,11 +1835,11 @@ async fn test_project_reconnect( // While client B is disconnected, add and remove files from client A's project client_a - .fs + .fs() .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into()) .await; client_a - .fs + .fs() .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default()) .await .unwrap(); @@ -1925,8 +1925,8 @@ async fn test_active_call_events( 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; - client_a.fs.insert_tree("/a", json!({})).await; - client_b.fs.insert_tree("/b", json!({})).await; + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await; @@ -2014,8 +2014,8 @@ async fn test_room_location( 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; - client_a.fs.insert_tree("/a", json!({})).await; - client_b.fs.insert_tree("/b", json!({})).await; + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); @@ -2204,12 +2204,12 @@ async fn test_propagate_saves_and_fs_changes( Some(tree_sitter_rust::language()), )); for client in [&client_a, &client_b, &client_c] { - client.language_registry.add(rust.clone()); - client.language_registry.add(javascript.clone()); + client.language_registry().add(rust.clone()); + client.language_registry().add(javascript.clone()); } client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -2279,7 +2279,7 @@ async fn test_propagate_saves_and_fs_changes( buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( - client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(), + client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(), "hi-a, i-am-c, i-am-b, i-am-a" ); @@ -2290,7 +2290,7 @@ async fn test_propagate_saves_and_fs_changes( // Make changes on host's file system, see those changes on guest worktrees. client_a - .fs + .fs() .rename( "/a/file1.rs".as_ref(), "/a/file1.js".as_ref(), @@ -2299,11 +2299,11 @@ async fn test_propagate_saves_and_fs_changes( .await .unwrap(); client_a - .fs + .fs() .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) .await .unwrap(); - client_a.fs.insert_file("/a/file4", "4".into()).await; + client_a.fs().insert_file("/a/file4", "4".into()).await; deterministic.run_until_parked(); worktree_a.read_with(cx_a, |tree, _| { @@ -2397,7 +2397,7 @@ async fn test_git_diff_base_change( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -2441,7 +2441,7 @@ async fn test_git_diff_base_change( " .unindent(); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/.git"), &[(Path::new("a.txt"), diff_base.clone())], ); @@ -2486,7 +2486,7 @@ async fn test_git_diff_base_change( ); }); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/.git"), &[(Path::new("a.txt"), new_diff_base.clone())], ); @@ -2531,7 +2531,7 @@ async fn test_git_diff_base_change( " .unindent(); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/sub/.git"), &[(Path::new("b.txt"), diff_base.clone())], ); @@ -2576,7 +2576,7 @@ async fn test_git_diff_base_change( ); }); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/sub/.git"), &[(Path::new("b.txt"), new_diff_base.clone())], ); @@ -2635,7 +2635,7 @@ async fn test_git_branch_name( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -2654,8 +2654,7 @@ async fn test_git_branch_name( let project_remote = client_b.build_remote_project(project_id, cx_b).await; client_a - .fs - .as_fake() + .fs() .set_branch_name(Path::new("/dir/.git"), Some("branch-1")); // Wait for it to catch up to the new branch @@ -2680,8 +2679,7 @@ async fn test_git_branch_name( }); client_a - .fs - .as_fake() + .fs() .set_branch_name(Path::new("/dir/.git"), Some("branch-2")); // Wait for buffer_local_a to receive it @@ -2720,7 +2718,7 @@ async fn test_git_status_sync( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -2734,7 +2732,7 @@ async fn test_git_status_sync( const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; - client_a.fs.as_fake().set_status_for_repo_via_git_operation( + client_a.fs().set_status_for_repo_via_git_operation( Path::new("/dir/.git"), &[ (&Path::new(A_TXT), GitFileStatus::Added), @@ -2780,16 +2778,13 @@ async fn test_git_status_sync( assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); - client_a - .fs - .as_fake() - .set_status_for_repo_via_working_copy_change( - Path::new("/dir/.git"), - &[ - (&Path::new(A_TXT), GitFileStatus::Modified), - (&Path::new(B_TXT), GitFileStatus::Modified), - ], - ); + client_a.fs().set_status_for_repo_via_working_copy_change( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitFileStatus::Modified), + (&Path::new(B_TXT), GitFileStatus::Modified), + ], + ); // Wait for buffer_local_a to receive it deterministic.run_until_parked(); @@ -2860,7 +2855,7 @@ async fn test_fs_operations( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3133,7 +3128,7 @@ async fn test_local_settings( // As client A, open a project that contains some local settings files client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3175,7 +3170,7 @@ async fn test_local_settings( // As client A, update a settings file. As Client B, see the changed settings. client_a - .fs + .fs() .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) .await; deterministic.run_until_parked(); @@ -3192,17 +3187,17 @@ async fn test_local_settings( // As client A, create and remove some settings files. As client B, see the changed settings. client_a - .fs + .fs() .remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) .await .unwrap(); client_a - .fs + .fs() .create_dir("/dir/b/.zed".as_ref()) .await .unwrap(); client_a - .fs + .fs() .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) .await; deterministic.run_until_parked(); @@ -3223,11 +3218,11 @@ async fn test_local_settings( // As client A, change and remove settings files while client B is disconnected. client_a - .fs + .fs() .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) .await; client_a - .fs + .fs() .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) .await .unwrap(); @@ -3261,7 +3256,7 @@ async fn test_buffer_conflict_after_save( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3323,7 +3318,7 @@ async fn test_buffer_reloading( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3351,7 +3346,7 @@ async fn test_buffer_reloading( let new_contents = Rope::from("d\ne\nf"); client_a - .fs + .fs() .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) .await .unwrap(); @@ -3380,7 +3375,7 @@ async fn test_editing_while_guest_opens_buffer( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; @@ -3429,7 +3424,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; @@ -3527,7 +3522,7 @@ async fn test_leaving_worktree_while_opening_buffer( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; @@ -3570,7 +3565,7 @@ async fn test_canceling_buffer_opening( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3626,7 +3621,7 @@ async fn test_leaving_project( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -3714,9 +3709,9 @@ async fn test_leaving_project( cx_b.spawn(|cx| { Project::remote( project_id, - client_b.client.clone(), - client_b.user_store.clone(), - client_b.language_registry.clone(), + client_b.app_state.client.clone(), + client_b.user_store().clone(), + client_b.language_registry().clone(), FakeFs::new(cx.background()), cx, ) @@ -3768,11 +3763,11 @@ async fn test_collaborating_with_diagnostics( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); // Share a project as client A client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -4040,11 +4035,11 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"]; client_a - .fs + .fs() .insert_tree( "/test", json!({ @@ -4181,10 +4176,10 @@ async fn test_collaborating_with_completion( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -4342,7 +4337,7 @@ async fn test_reloading_buffer_manually( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/a", json!({ "a.rs": "let one = 1;" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; @@ -4373,7 +4368,7 @@ async fn test_reloading_buffer_manually( buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;")); client_a - .fs + .fs() .save( "/a/a.rs".as_ref(), &Rope::from("let seven = 7;"), @@ -4444,14 +4439,14 @@ async fn test_formatting_buffer( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); // Here we insert a fake tree with a directory that exists on disk. This is needed // because later we'll invoke a command, which requires passing a working directory // that points to a valid location on disk. let directory = env::current_dir().unwrap(); client_a - .fs + .fs() .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; @@ -4553,10 +4548,10 @@ async fn test_definition( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -4701,10 +4696,10 @@ async fn test_references( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -4797,7 +4792,7 @@ async fn test_project_search( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -4883,7 +4878,7 @@ async fn test_document_highlights( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/root-1", json!({ @@ -4902,7 +4897,7 @@ async fn test_document_highlights( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -4989,7 +4984,7 @@ async fn test_lsp_hover( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/root-1", json!({ @@ -5008,7 +5003,7 @@ async fn test_lsp_hover( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -5107,10 +5102,10 @@ async fn test_project_symbols( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/code", json!({ @@ -5218,10 +5213,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -5278,6 +5273,7 @@ async fn test_collaborating_with_code_actions( 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; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) @@ -5296,10 +5292,10 @@ async fn test_collaborating_with_code_actions( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -5316,7 +5312,8 @@ async fn test_collaborating_with_code_actions( // Join the project as client B. let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let (_window_b, workspace_b) = + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -5521,10 +5518,10 @@ async fn test_collaborating_with_renames( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -5540,7 +5537,8 @@ async fn test_collaborating_with_renames( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let (_window_b, workspace_b) = + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "one.rs"), None, true, cx) @@ -5706,10 +5704,10 @@ async fn test_language_server_statuses( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -6166,7 +6164,7 @@ async fn test_contacts( // Test removing a contact client_b - .user_store + .user_store() .update(cx_b, |store, cx| { store.remove_contact(client_c.user_id().unwrap(), cx) }) @@ -6189,7 +6187,7 @@ async fn test_contacts( client: &TestClient, cx: &TestAppContext, ) -> Vec<(String, &'static str, &'static str)> { - client.user_store.read_with(cx, |store, _| { + client.user_store().read_with(cx, |store, _| { store .contacts() .iter() @@ -6232,14 +6230,14 @@ async fn test_contact_requests( // User A and User C request that user B become their contact. client_a - .user_store + .user_store() .update(cx_a, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); client_c - .user_store + .user_store() .update(cx_c, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) }) @@ -6293,7 +6291,7 @@ async fn test_contact_requests( // User B accepts the request from user A. client_b - .user_store + .user_store() .update(cx_b, |store, cx| { store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) }) @@ -6337,7 +6335,7 @@ async fn test_contact_requests( // User B rejects the request from user C. client_b - .user_store + .user_store() .update(cx_b, |store, cx| { store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx) }) @@ -6419,7 +6417,7 @@ async fn test_basic_following( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -6980,7 +6978,7 @@ async fn test_join_call_after_screen_was_shared( .await .unwrap(); - client_b.user_store.update(cx_b, |user_store, _| { + client_b.user_store().update(cx_b, |user_store, _| { user_store.clear_cache(); }); @@ -7040,7 +7038,7 @@ async fn test_following_tab_order( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7163,7 +7161,7 @@ async fn test_peers_following_each_other( // Client A shares a project. client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7336,7 +7334,7 @@ async fn test_auto_unfollowing( // Client A shares a project. client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7500,7 +7498,7 @@ async fn test_peers_simultaneously_following_each_other( cx_a.update(editor::init); cx_b.update(editor::init); - client_a.fs.insert_tree("/a", json!({})).await; + client_a.fs().insert_tree("/a", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let workspace_a = client_a.build_workspace(&project_a, cx_a); let project_id = active_call_a @@ -7577,10 +7575,10 @@ async fn test_on_input_format_from_host_to_guest( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7706,10 +7704,10 @@ async fn test_on_input_format_from_guest_to_host( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7862,11 +7860,11 @@ async fn test_mutual_editor_inlay_hint_cache_update( })) .await; let language = Arc::new(language); - client_a.language_registry.add(Arc::clone(&language)); - client_b.language_registry.add(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -8169,11 +8167,11 @@ async fn test_inlay_hint_refresh_is_forwarded( })) .await; let language = Arc::new(language); - client_a.language_registry.add(Arc::clone(&language)); - client_b.language_registry.add(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); client_a - .fs + .fs() .insert_tree( "/a", json!({ diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 8062a12b83..8202b53fdc 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -396,9 +396,9 @@ async fn apply_client_operation( ); let root_path = Path::new("/").join(&first_root_name); - client.fs.create_dir(&root_path).await.unwrap(); + client.fs().create_dir(&root_path).await.unwrap(); client - .fs + .fs() .create_file(&root_path.join("main.rs"), Default::default()) .await .unwrap(); @@ -422,8 +422,8 @@ async fn apply_client_operation( ); ensure_project_shared(&project, client, cx).await; - if !client.fs.paths(false).contains(&new_root_path) { - client.fs.create_dir(&new_root_path).await.unwrap(); + if !client.fs().paths(false).contains(&new_root_path) { + client.fs().create_dir(&new_root_path).await.unwrap(); } project .update(cx, |project, cx| { @@ -475,7 +475,7 @@ async fn apply_client_operation( Some(room.update(cx, |room, cx| { room.join_project( project_id, - client.language_registry.clone(), + client.language_registry().clone(), FakeFs::new(cx.background().clone()), cx, ) @@ -743,7 +743,7 @@ async fn apply_client_operation( content, } => { if !client - .fs + .fs() .directories(false) .contains(&path.parent().unwrap().to_owned()) { @@ -752,14 +752,14 @@ async fn apply_client_operation( if is_dir { log::info!("{}: creating dir at {:?}", client.username, path); - client.fs.create_dir(&path).await.unwrap(); + client.fs().create_dir(&path).await.unwrap(); } else { - let exists = client.fs.metadata(&path).await?.is_some(); + let exists = client.fs().metadata(&path).await?.is_some(); let verb = if exists { "updating" } else { "creating" }; log::info!("{}: {} file at {:?}", verb, client.username, path); client - .fs + .fs() .save(&path, &content.as_str().into(), fs::LineEnding::Unix) .await .unwrap(); @@ -771,12 +771,12 @@ async fn apply_client_operation( repo_path, contents, } => { - if !client.fs.directories(false).contains(&repo_path) { + if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } for (path, _) in contents.iter() { - if !client.fs.files().contains(&repo_path.join(path)) { + if !client.fs().files().contains(&repo_path.join(path)) { return Err(TestError::Inapplicable); } } @@ -793,16 +793,16 @@ async fn apply_client_operation( .iter() .map(|(path, contents)| (path.as_path(), contents.clone())) .collect::>(); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; } - client.fs.set_index_for_repo(&dot_git_dir, &contents); + client.fs().set_index_for_repo(&dot_git_dir, &contents); } GitOperation::WriteGitBranch { repo_path, new_branch, } => { - if !client.fs.directories(false).contains(&repo_path) { + if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } @@ -814,21 +814,21 @@ async fn apply_client_operation( ); let dot_git_dir = repo_path.join(".git"); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; } - client.fs.set_branch_name(&dot_git_dir, new_branch); + client.fs().set_branch_name(&dot_git_dir, new_branch); } GitOperation::WriteGitStatuses { repo_path, statuses, git_operation, } => { - if !client.fs.directories(false).contains(&repo_path) { + if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } for (path, _) in statuses.iter() { - if !client.fs.files().contains(&repo_path.join(path)) { + if !client.fs().files().contains(&repo_path.join(path)) { return Err(TestError::Inapplicable); } } @@ -847,16 +847,16 @@ async fn apply_client_operation( .map(|(path, val)| (path.as_path(), val.clone())) .collect::>(); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; } if git_operation { client - .fs + .fs() .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice()); } else { - client.fs.set_status_for_repo_via_working_copy_change( + client.fs().set_status_for_repo_via_working_copy_change( &dot_git_dir, statuses.as_slice(), ); @@ -1499,7 +1499,7 @@ impl TestPlan { // Invite a contact to the current call 0..=70 => { let available_contacts = - client.user_store.read_with(cx, |user_store, _| { + client.user_store().read_with(cx, |user_store, _| { user_store .contacts() .iter() @@ -1596,7 +1596,7 @@ impl TestPlan { .choose(&mut self.rng) .cloned() else { continue }; let project_root_name = root_name_for_project(&project, cx); - let mut paths = client.fs.paths(false); + let mut paths = client.fs().paths(false); paths.remove(0); let new_root_path = if paths.is_empty() || self.rng.gen() { Path::new("/").join(&self.next_root_dir_name(user_id)) @@ -1776,7 +1776,7 @@ impl TestPlan { let is_dir = self.rng.gen::(); let content; let mut path; - let dir_paths = client.fs.directories(false); + let dir_paths = client.fs().directories(false); if is_dir { content = String::new(); @@ -1786,7 +1786,7 @@ impl TestPlan { content = Alphanumeric.sample_string(&mut self.rng, 16); // Create a new file or overwrite an existing file - let file_paths = client.fs.files(); + let file_paths = client.fs().files(); if file_paths.is_empty() || self.rng.gen_bool(0.5) { path = dir_paths.choose(&mut self.rng).unwrap().clone(); path.push(gen_file_name(&mut self.rng)); @@ -1812,7 +1812,7 @@ impl TestPlan { client: &TestClient, ) -> Vec { let mut paths = client - .fs + .fs() .files() .into_iter() .filter(|path| path.starts_with(repo_path)) @@ -1829,7 +1829,7 @@ impl TestPlan { } let repo_path = client - .fs + .fs() .directories(false) .choose(&mut self.rng) .unwrap() @@ -1928,7 +1928,7 @@ async fn simulate_client( name: "the-fake-language-server", capabilities: lsp::LanguageServer::full_capabilities(), initializer: Some(Box::new({ - let fs = client.fs.clone(); + let fs = client.app_state.fs.clone(); move |fake_server: &mut FakeLanguageServer| { fake_server.handle_request::( |_, _| async move { @@ -1973,7 +1973,7 @@ async fn simulate_client( let background = cx.background(); let mut rng = background.rng(); let count = rng.gen_range::(1..3); - let files = fs.files(); + let files = fs.as_fake().files(); let files = (0..count) .map(|_| files.choose(&mut *rng).unwrap().clone()) .collect::>(); @@ -2023,7 +2023,7 @@ async fn simulate_client( ..Default::default() })) .await; - client.language_registry.add(Arc::new(language)); + client.app_state.languages.add(Arc::new(language)); while let Some(batch_id) = operation_rx.next().await { let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index edbb89e339..c42ed34de6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -3,9 +3,9 @@ mod contact_notification; mod face_pile; mod incoming_call_notification; mod notifications; +pub mod panel; mod project_shared_notification; mod sharing_status_indicator; -pub mod panel; use call::{ActiveCall, Room}; pub use collab_titlebar_item::CollabTitlebarItem; diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 53f7eee79a..c6940fbd14 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -6,7 +6,7 @@ use anyhow::Result; use call::ActiveCall; use client::{proto::PeerId, Channel, ChannelStore, Client, Contact, User, UserStore}; use contact_finder::build_contact_finder; -use context_menu::ContextMenu; +use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; use futures::StreamExt; @@ -18,6 +18,7 @@ use gpui::{ MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, }, geometry::{rect::RectF, vector::vec2f}, + impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -36,8 +37,15 @@ use workspace::{ Workspace, }; +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct RemoveChannel { + channel_id: u64, +} + actions!(collab_panel, [ToggleFocus]); +impl_actions!(collab_panel, [RemoveChannel]); + const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { @@ -49,6 +57,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::select_next); cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); + cx.add_action(CollabPanel::remove_channel); } #[derive(Debug, Default)] @@ -305,6 +314,8 @@ impl CollabPanel { let active_call = ActiveCall::global(cx); this.subscriptions .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx))); + this.subscriptions + .push(cx.observe(&this.channel_store, |this, _, cx| this.update_entries(cx))); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); @@ -1278,6 +1289,19 @@ impl CollabPanel { .on_click(MouseButton::Left, move |_, this, cx| { this.join_channel(channel_id, cx); }) + .on_click(MouseButton::Right, move |e, this, cx| { + this.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + e.position, + gpui::elements::AnchorCorner::BottomLeft, + vec![ContextMenuItem::action( + "Remove Channel", + RemoveChannel { channel_id }, + )], + cx, + ); + }); + }) .into_any() } @@ -1564,14 +1588,13 @@ impl CollabPanel { } } } else if let Some((_editing_state, channel_name)) = self.take_editing_state(cx) { - dbg!(&channel_name); let create_channel = self.channel_store.update(cx, |channel_store, cx| { channel_store.create_channel(&channel_name, None) }); cx.foreground() .spawn(async move { - dbg!(create_channel.await).ok(); + create_channel.await.ok(); }) .detach(); } @@ -1600,6 +1623,36 @@ impl CollabPanel { } } + fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { + let channel_id = action.channel_id; + let channel_store = self.channel_store.clone(); + if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { + let prompt_message = format!( + "Are you sure you want to remove the channel \"{}\"?", + channel.name + ); + let mut answer = + cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + let window_id = cx.window_id(); + cx.spawn(|_, mut cx| async move { + if answer.next().await == Some(0) { + if let Err(e) = channel_store + .update(&mut cx, |channels, cx| channels.remove_channel(channel_id)) + .await + { + cx.prompt( + window_id, + PromptLevel::Info, + &format!("Failed to remove channel: {}", e), + &["Ok"], + ); + } + } + }) + .detach(); + } + } + fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( diff --git a/crates/collab_ui/src/panel/channel_modal.rs b/crates/collab_ui/src/panel/channel_modal.rs index 562536d58c..fff1dc8624 100644 --- a/crates/collab_ui/src/panel/channel_modal.rs +++ b/crates/collab_ui/src/panel/channel_modal.rs @@ -1,5 +1,5 @@ use editor::Editor; -use gpui::{elements::*, AnyViewHandle, Entity, View, ViewContext, ViewHandle, AppContext}; +use gpui::{elements::*, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use menu::Cancel; use workspace::{item::ItemHandle, Modal}; @@ -62,12 +62,10 @@ impl View for ChannelModal { .constrained() .with_max_width(540.) .with_max_height(420.) - }) .on_click(gpui::platform::MouseButton::Left, |_, _, _| {}) // Capture click and down events - .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| { - v.dismiss(cx) - }).into_any_named("channel modal") + .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| v.dismiss(cx)) + .into_any_named("channel modal") } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8a4a72c268..f49a879dc7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -137,6 +137,7 @@ message Envelope { RespondToChannelInvite respond_to_channel_invite = 123; UpdateChannels update_channels = 124; JoinChannel join_channel = 125; + RemoveChannel remove_channel = 126; } } @@ -875,6 +876,11 @@ message JoinChannel { uint64 channel_id = 1; } +message RemoveChannel { + uint64 channel_id = 1; +} + + message CreateChannel { string name = 1; optional uint64 parent_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d71ddeed83..f6985d6906 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -231,6 +231,7 @@ messages!( (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), + (RemoveChannel, Foreground), (UpdateChannels, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateFollowers, Foreground), @@ -296,6 +297,7 @@ request_messages!( (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), (JoinChannel, JoinRoomResponse), + (RemoveChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 95077649a8..4fe8b5d0f4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3412,6 +3412,7 @@ impl Workspace { pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); + let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let app_state = Arc::new(AppState {