From a60fef52c47666df9157c5a8e6ddcf1b66b63d68 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 27 May 2022 18:03:51 -0700 Subject: [PATCH 01/22] Start work on private projects --- crates/collab/src/integration_tests.rs | 66 ++++++++++++++++++++++++++ crates/project/src/project.rs | 56 +++++++++++++++------- crates/workspace/src/workspace.rs | 2 + crates/zed/src/zed.rs | 1 + 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 65f60ed077..39964be671 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -504,6 +504,70 @@ async fn test_cancel_join_request( ); } +#[gpui::test(iterations = 3)] +async fn test_private_projects( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + cx_a.foreground().forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let user_a = UserId::from_proto(client_a.user_id().unwrap()); + + let fs = FakeFs::new(cx_a.background()); + fs.insert_tree("/a", json!({})).await; + + // Create a private project + let project_a = cx_a.update(|cx| { + Project::local( + false, + client_a.client.clone(), + client_a.user_store.clone(), + client_a.language_registry.clone(), + fs.clone(), + cx, + ) + }); + client_a.project = Some(project_a.clone()); + + // Private projects are not registered with the server. + deterministic.run_until_parked(); + assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_none())); + assert!(client_b + .user_store + .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); + + // The project is registered when it is made public. + project_a.update(cx_a, |project, _| project.set_public(true)); + deterministic.run_until_parked(); + assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); + assert!(!client_b + .user_store + .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); + + // The project is registered again when it loses and regains connection. + server.disconnect_client(user_a); + server.forbid_connections(); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); + // deterministic.run_until_parked(); + assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_none())); + assert!(client_b + .user_store + .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); + server.allow_connections(); + cx_b.foreground().advance_clock(Duration::from_secs(10)); + assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); + assert!(!client_b + .user_store + .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); +} + #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, @@ -4009,6 +4073,7 @@ async fn test_random_collaboration( let host = server.create_client(&mut host_cx, "host").await; let host_project = host_cx.update(|cx| { Project::local( + true, host.client.clone(), host.user_store.clone(), host_language_registry.clone(), @@ -4735,6 +4800,7 @@ impl TestClient { ) -> (ModelHandle, WorktreeId) { let project = cx.update(|cx| { Project::local( + true, self.client.clone(), self.user_store.clone(), self.language_registry.clone(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index adeb8d37f9..58dc2ced20 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; -use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; +use futures::{future::Shared, select_biased, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -120,6 +120,7 @@ enum ProjectClientState { is_shared: bool, remote_id_tx: watch::Sender>, remote_id_rx: watch::Receiver>, + public_tx: watch::Sender, _maintain_remote_id_task: Task>, }, Remote { @@ -305,6 +306,7 @@ impl Project { } pub fn local( + public: bool, client: Arc, user_store: ModelHandle, languages: Arc, @@ -312,24 +314,25 @@ impl Project { cx: &mut MutableAppContext, ) -> ModelHandle { cx.add_model(|cx: &mut ModelContext| { + let (public_tx, mut public_rx) = watch::channel_with(public); let (remote_id_tx, remote_id_rx) = watch::channel(); let _maintain_remote_id_task = cx.spawn_weak({ - let rpc = client.clone(); - move |this, mut cx| { - async move { - let mut status = rpc.status(); - while let Some(status) = status.next().await { - if let Some(this) = this.upgrade(&cx) { - if status.is_connected() { - this.update(&mut cx, |this, cx| this.register(cx)).await?; - } else { - this.update(&mut cx, |this, cx| this.unregister(cx)); - } - } + let mut status_rx = client.clone().status(); + move |this, mut cx| async move { + loop { + select_biased! { + value = status_rx.next().fuse() => { value?; } + value = public_rx.next().fuse() => { value?; } + }; + let this = this.upgrade(&cx)?; + if status_rx.borrow().is_connected() && *public_rx.borrow() { + this.update(&mut cx, |this, cx| this.register(cx)) + .await + .log_err()?; + } else { + this.update(&mut cx, |this, cx| this.unregister(cx)); } - Ok(()) } - .log_err() } }); @@ -346,6 +349,7 @@ impl Project { is_shared: false, remote_id_tx, remote_id_rx, + public_tx, _maintain_remote_id_task, }, opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), @@ -509,7 +513,7 @@ impl Project { let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = cx.update(|cx| Project::local(client, user_store, languages, fs, cx)); + let project = cx.update(|cx| Project::local(true, client, user_store, languages, fs, cx)); for path in root_paths { let (tree, _) = project .update(cx, |project, cx| { @@ -598,6 +602,20 @@ impl Project { &self.fs } + pub fn set_public(&mut self, is_public: bool) { + if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { + *public_tx.borrow_mut() = is_public; + } + } + + pub fn is_public(&mut self) -> bool { + if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { + *public_tx.borrow() + } else { + true + } + } + fn unregister(&mut self, cx: &mut ModelContext) { self.unshared(cx); for worktree in &self.worktrees { @@ -616,7 +634,11 @@ impl Project { } fn register(&mut self, cx: &mut ModelContext) -> Task> { - self.unregister(cx); + if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { + if remote_id_rx.borrow().is_some() { + return Task::ready(Ok(())); + } + } let response = self.client.request(proto::RegisterProject {}); cx.spawn(|this, mut cx| async move { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2c77c72f13..1a38cd4866 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2407,6 +2407,7 @@ pub fn open_paths( cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( + false, app_state.client.clone(), app_state.user_store.clone(), app_state.languages.clone(), @@ -2463,6 +2464,7 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( + false, app_state.client.clone(), app_state.user_store.clone(), app_state.languages.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6ebe3dc35d..63b9bb5fea 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -295,6 +295,7 @@ fn open_config_file( let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( + false, app_state.client.clone(), app_state.user_store.clone(), app_state.languages.clone(), From 7ef9de32b15e4db8874f0840dd64de733f2816e8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 May 2022 16:16:40 -0700 Subject: [PATCH 02/22] Show private projects in the contacts panel Introduce a ProjectStore that lets you iterate through all open projects. Allow projects to be made public by clicking the lock. --- assets/icons/lock-8.svg | 3 + crates/client/src/client.rs | 59 ++- crates/client/src/test.rs | 5 +- crates/collab/src/integration_tests.rs | 122 +++-- crates/contacts_panel/src/contacts_panel.rs | 487 ++++++++++++++------ crates/gpui/src/app.rs | 4 + crates/gpui/src/platform.rs | 6 + crates/project/src/project.rs | 106 ++++- crates/theme/src/theme.rs | 1 + crates/workspace/src/waiting_room.rs | 3 +- crates/workspace/src/workspace.rs | 123 ++--- crates/zed/src/main.rs | 4 +- crates/zed/src/zed.rs | 8 +- styles/src/styleTree/contactsPanel.ts | 5 + 14 files changed, 627 insertions(+), 309 deletions(-) create mode 100644 assets/icons/lock-8.svg diff --git a/assets/icons/lock-8.svg b/assets/icons/lock-8.svg new file mode 100644 index 0000000000..c98340b93a --- /dev/null +++ b/assets/icons/lock-8.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0fc0f97949..51da1f4c1c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -67,17 +67,23 @@ pub struct Client { peer: Arc, http: Arc, state: RwLock, - authenticate: + + #[cfg(any(test, feature = "test-support"))] + authenticate: RwLock< Option Task>>>, - establish_connection: Option< - Box< - dyn 'static - + Send - + Sync - + Fn( - &Credentials, - &AsyncAppContext, - ) -> Task>, + >, + #[cfg(any(test, feature = "test-support"))] + establish_connection: RwLock< + Option< + Box< + dyn 'static + + Send + + Sync + + Fn( + &Credentials, + &AsyncAppContext, + ) -> Task>, + >, >, >, } @@ -235,8 +241,11 @@ impl Client { peer: Peer::new(), http, state: Default::default(), - authenticate: None, - establish_connection: None, + + #[cfg(any(test, feature = "test-support"))] + authenticate: Default::default(), + #[cfg(any(test, feature = "test-support"))] + establish_connection: Default::default(), }) } @@ -260,23 +269,23 @@ impl Client { } #[cfg(any(test, feature = "test-support"))] - pub fn override_authenticate(&mut self, authenticate: F) -> &mut Self + pub fn override_authenticate(&self, authenticate: F) -> &Self where F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, { - self.authenticate = Some(Box::new(authenticate)); + *self.authenticate.write() = Some(Box::new(authenticate)); self } #[cfg(any(test, feature = "test-support"))] - pub fn override_establish_connection(&mut self, connect: F) -> &mut Self + pub fn override_establish_connection(&self, connect: F) -> &Self where F: 'static + Send + Sync + Fn(&Credentials, &AsyncAppContext) -> Task>, { - self.establish_connection = Some(Box::new(connect)); + *self.establish_connection.write() = Some(Box::new(connect)); self } @@ -755,11 +764,12 @@ impl Client { } fn authenticate(self: &Arc, cx: &AsyncAppContext) -> Task> { - if let Some(callback) = self.authenticate.as_ref() { - callback(cx) - } else { - self.authenticate_with_browser(cx) + #[cfg(any(test, feature = "test-support"))] + if let Some(callback) = self.authenticate.read().as_ref() { + return callback(cx); } + + self.authenticate_with_browser(cx) } fn establish_connection( @@ -767,11 +777,12 @@ impl Client { credentials: &Credentials, cx: &AsyncAppContext, ) -> Task> { - if let Some(callback) = self.establish_connection.as_ref() { - callback(credentials, cx) - } else { - self.establish_websocket_connection(credentials, cx) + #[cfg(any(test, feature = "test-support"))] + if let Some(callback) = self.establish_connection.read().as_ref() { + return callback(credentials, cx); } + + self.establish_websocket_connection(credentials, cx) } fn establish_websocket_connection( diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index face7db16e..a809bd2769 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -28,7 +28,7 @@ struct FakeServerState { impl FakeServer { pub async fn for_client( client_user_id: u64, - client: &mut Arc, + client: &Arc, cx: &TestAppContext, ) -> Self { let server = Self { @@ -38,8 +38,7 @@ impl FakeServer { executor: cx.foreground(), }; - Arc::get_mut(client) - .unwrap() + client .override_authenticate({ let state = Arc::downgrade(&server.state); move |cx| { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 39964be671..2ba324eed8 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -30,7 +30,7 @@ use project::{ fs::{FakeFs, Fs as _}, search::SearchQuery, worktree::WorktreeHandle, - DiagnosticSummary, Project, ProjectPath, WorktreeId, + DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId, }; use rand::prelude::*; use rpc::PeerId; @@ -174,9 +174,10 @@ async fn test_share_project( project_id, client_b2.client.clone(), client_b2.user_store.clone(), + client_b2.project_store.clone(), client_b2.language_registry.clone(), FakeFs::new(cx_b2.background()), - &mut cx_b2.to_async(), + cx_b2.to_async(), ) .await .unwrap(); @@ -310,16 +311,16 @@ async fn test_host_disconnect( .unwrap(); // Request to join that project as client C - let project_c = cx_c.spawn(|mut cx| async move { + let project_c = cx_c.spawn(|cx| { Project::remote( project_id, client_c.client.clone(), client_c.user_store.clone(), + client_c.project_store.clone(), client_c.language_registry.clone(), FakeFs::new(cx.background()), - &mut cx, + cx, ) - .await }); deterministic.run_until_parked(); @@ -372,21 +373,16 @@ async fn test_decline_join_request( let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); // Request to join that project as client B - let project_b = cx_b.spawn(|mut cx| { - let client = client_b.client.clone(); - let user_store = client_b.user_store.clone(); - let language_registry = client_b.language_registry.clone(); - async move { - Project::remote( - project_id, - client, - user_store, - language_registry, - FakeFs::new(cx.background()), - &mut cx, - ) - .await - } + let project_b = cx_b.spawn(|cx| { + Project::remote( + project_id, + client_b.client.clone(), + client_b.user_store.clone(), + client_b.project_store.clone(), + client_b.language_registry.clone(), + FakeFs::new(cx.background()), + cx, + ) }); deterministic.run_until_parked(); project_a.update(cx_a, |project, cx| { @@ -398,20 +394,16 @@ async fn test_decline_join_request( )); // Request to join the project again as client B - let project_b = cx_b.spawn(|mut cx| { - let client = client_b.client.clone(); - let user_store = client_b.user_store.clone(); - async move { - Project::remote( - project_id, - client, - user_store, - client_b.language_registry.clone(), - FakeFs::new(cx.background()), - &mut cx, - ) - .await - } + let project_b = cx_b.spawn(|cx| { + Project::remote( + project_id, + client_b.client.clone(), + client_b.user_store.clone(), + client_b.project_store.clone(), + client_b.language_registry.clone(), + FakeFs::new(cx.background()), + cx, + ) }); // Close the project on the host @@ -467,21 +459,16 @@ async fn test_cancel_join_request( }); // Request to join that project as client B - let project_b = cx_b.spawn(|mut cx| { - let client = client_b.client.clone(); - let user_store = client_b.user_store.clone(); - let language_registry = client_b.language_registry.clone(); - async move { - Project::remote( - project_id, - client, - user_store, - language_registry.clone(), - FakeFs::new(cx.background()), - &mut cx, - ) - .await - } + let project_b = cx_b.spawn(|cx| { + Project::remote( + project_id, + client_b.client.clone(), + client_b.user_store.clone(), + client_b.project_store.clone(), + client_b.language_registry.clone().clone(), + FakeFs::new(cx.background()), + cx, + ) }); deterministic.run_until_parked(); assert_eq!( @@ -529,6 +516,7 @@ async fn test_private_projects( false, client_a.client.clone(), client_a.user_store.clone(), + client_a.project_store.clone(), client_a.language_registry.clone(), fs.clone(), cx, @@ -4076,6 +4064,7 @@ async fn test_random_collaboration( true, host.client.clone(), host.user_store.clone(), + host.project_store.clone(), host_language_registry.clone(), fs.clone(), cx, @@ -4311,9 +4300,10 @@ async fn test_random_collaboration( host_project_id, guest.client.clone(), guest.user_store.clone(), + guest.project_store.clone(), guest_lang_registry.clone(), FakeFs::new(cx.background()), - &mut guest_cx.to_async(), + guest_cx.to_async(), ) .await .unwrap(); @@ -4614,9 +4604,11 @@ impl TestServer { }); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + let project_store = cx.add_model(|_| ProjectStore::default()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), + project_store: project_store.clone(), languages: Arc::new(LanguageRegistry::new(Task::ready(()))), themes: ThemeRegistry::new((), cx.font_cache()), fs: FakeFs::new(cx.background()), @@ -4639,6 +4631,7 @@ impl TestServer { peer_id, username: name.to_string(), user_store, + project_store, language_registry: Arc::new(LanguageRegistry::test()), project: Default::default(), buffers: Default::default(), @@ -4732,6 +4725,7 @@ struct TestClient { username: String, pub peer_id: PeerId, pub user_store: ModelHandle, + pub project_store: ModelHandle, language_registry: Arc, project: Option>, buffers: HashSet>, @@ -4803,6 +4797,7 @@ impl TestClient { true, self.client.clone(), self.user_store.clone(), + self.project_store.clone(), self.language_registry.clone(), fs, cx, @@ -4835,27 +4830,22 @@ impl TestClient { .await; let guest_user_id = self.user_id().unwrap(); let languages = host_project.read_with(host_cx, |project, _| project.languages().clone()); - let project_b = guest_cx.spawn(|mut cx| { - let user_store = self.user_store.clone(); - let guest_client = self.client.clone(); - async move { - Project::remote( - host_project_id, - guest_client, - user_store.clone(), - languages, - FakeFs::new(cx.background()), - &mut cx, - ) - .await - .unwrap() - } + let project_b = guest_cx.spawn(|cx| { + Project::remote( + host_project_id, + self.client.clone(), + self.user_store.clone(), + self.project_store.clone(), + languages, + FakeFs::new(cx.background()), + cx, + ) }); host_cx.foreground().run_until_parked(); host_project.update(host_cx, |project, cx| { project.respond_to_join_request(guest_user_id, true, cx) }); - let project = project_b.await; + let project = project_b.await.unwrap(); self.project = Some(project.clone()); project } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 13485e96f2..ffa3300e70 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -13,15 +13,16 @@ use gpui::{ impl_actions, impl_internal_actions, platform::CursorStyle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext, - RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + RenderContext, Subscription, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; use join_project_notification::JoinProjectNotification; use menu::{Confirm, SelectNext, SelectPrev}; +use project::{Project, ProjectStore}; use serde::Deserialize; use settings::Settings; -use std::sync::Arc; +use std::{ops::DerefMut, sync::Arc}; use theme::IconButton; -use workspace::{sidebar::SidebarItem, JoinProject, Workspace}; +use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectPublic, Workspace}; impl_actions!( contacts_panel, @@ -37,13 +38,14 @@ enum Section { Offline, } -#[derive(Clone, Debug)] +#[derive(Clone)] enum ContactEntry { Header(Section), IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), - ContactProject(Arc, usize), + ContactProject(Arc, usize, Option>), + PrivateProject(WeakModelHandle), } #[derive(Clone)] @@ -54,6 +56,7 @@ pub struct ContactsPanel { match_candidates: Vec, list_state: ListState, user_store: ModelHandle, + project_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, selection: Option, @@ -89,6 +92,7 @@ pub fn init(cx: &mut MutableAppContext) { impl ContactsPanel { pub fn new( user_store: ModelHandle, + project_store: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { @@ -148,93 +152,88 @@ impl ContactsPanel { } }); - cx.subscribe(&user_store, { - let user_store = user_store.downgrade(); - move |_, _, event, cx| { - if let Some((workspace, user_store)) = - workspace.upgrade(cx).zip(user_store.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| match event { - client::Event::Contact { user, kind } => match kind { - ContactEventKind::Requested | ContactEventKind::Accepted => workspace - .show_notification(user.id as usize, cx, |cx| { - cx.add_view(|cx| { - ContactNotification::new( - user.clone(), - *kind, - user_store, - cx, - ) - }) - }), - _ => {} - }, - _ => {} - }); - } + cx.observe(&project_store, |this, _, cx| this.update_entries(cx)) + .detach(); - if let client::Event::ShowContacts = event { - cx.emit(Event::Activate); - } + cx.subscribe(&user_store, move |_, user_store, event, cx| { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| match event { + client::Event::Contact { user, kind } => match kind { + ContactEventKind::Requested | ContactEventKind::Accepted => workspace + .show_notification(user.id as usize, cx, |cx| { + cx.add_view(|cx| { + ContactNotification::new(user.clone(), *kind, user_store, cx) + }) + }), + _ => {} + }, + _ => {} + }); + } + + if let client::Event::ShowContacts = event { + cx.emit(Event::Activate); } }) .detach(); - let mut this = Self { - list_state: ListState::new(0, Orientation::Top, 1000., cx, { - move |this, ix, cx| { - let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; - let current_user_id = - this.user_store.read(cx).current_user().map(|user| user.id); - let is_selected = this.selection == Some(ix); + let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { + let theme = cx.global::().theme.clone(); + let theme = &theme.contacts_panel; + let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id); + let is_selected = this.selection == Some(ix); - match &this.entries[ix] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(§ion); - Self::render_header(*section, theme, is_selected, is_collapsed, cx) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - theme, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - theme, - false, - is_selected, - cx, - ), - ContactEntry::Contact(contact) => { - Self::render_contact(contact.clone(), theme, is_selected) - } - ContactEntry::ContactProject(contact, project_ix) => { - let is_last_project_for_contact = - this.entries.get(ix + 1).map_or(true, |next| { - if let ContactEntry::ContactProject(next_contact, _) = next { - next_contact.user.id != contact.user.id - } else { - true - } - }); - Self::render_contact_project( - contact.clone(), - current_user_id, - *project_ix, - theme, - is_last_project_for_contact, - is_selected, - cx, - ) - } - } + match &this.entries[ix] { + ContactEntry::Header(section) => { + let is_collapsed = this.collapsed_sections.contains(§ion); + Self::render_header(*section, theme, is_selected, is_collapsed, cx) } - }), + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + theme, + true, + is_selected, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + theme, + false, + is_selected, + cx, + ), + ContactEntry::Contact(contact) => { + Self::render_contact(&contact.user, theme, is_selected) + } + ContactEntry::ContactProject(contact, project_ix, _) => { + let is_last_project_for_contact = + this.entries.get(ix + 1).map_or(true, |next| { + if let ContactEntry::ContactProject(next_contact, _, _) = next { + next_contact.user.id != contact.user.id + } else { + true + } + }); + Self::render_contact_project( + contact.clone(), + current_user_id, + *project_ix, + theme, + is_last_project_for_contact, + is_selected, + cx, + ) + } + ContactEntry::PrivateProject(project) => { + Self::render_private_project(project.clone(), theme, is_selected, cx) + } + } + }); + + let mut this = Self { + list_state, selection: None, collapsed_sections: Default::default(), entries: Default::default(), @@ -242,6 +241,7 @@ impl ContactsPanel { filter_editor, _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), user_store, + project_store, }; this.update_entries(cx); this @@ -300,13 +300,9 @@ impl ContactsPanel { .boxed() } - fn render_contact( - contact: Arc, - theme: &theme::ContactsPanel, - is_selected: bool, - ) -> ElementBox { + fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox { Flex::row() - .with_children(contact.user.avatar.clone().map(|avatar| { + .with_children(user.avatar.clone().map(|avatar| { Image::new(avatar) .with_style(theme.contact_avatar) .aligned() @@ -315,7 +311,7 @@ impl ContactsPanel { })) .with_child( Label::new( - contact.user.github_login.clone(), + user.github_login.clone(), theme.contact_username.text.clone(), ) .contained() @@ -446,6 +442,84 @@ impl ContactsPanel { .boxed() } + fn render_private_project( + project: WeakModelHandle, + theme: &theme::ContactsPanel, + is_selected: bool, + cx: &mut RenderContext, + ) -> ElementBox { + let project = if let Some(project) = project.upgrade(cx.deref_mut()) { + project + } else { + return Empty::new().boxed(); + }; + + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + enum LocalProject {} + enum TogglePublic {} + + let project_id = project.id(); + MouseEventHandler::new::(project_id, cx, |state, cx| { + let row = theme.project_row.style_for(state, is_selected); + let mut worktree_root_names = String::new(); + let project = project.read(cx); + let is_public = project.is_public(); + for tree in project.visible_worktrees(cx) { + if !worktree_root_names.is_empty() { + worktree_root_names.push_str(", "); + } + worktree_root_names.push_str(tree.read(cx).root_name()); + } + + Flex::row() + .with_child( + MouseEventHandler::new::(project_id, cx, |state, _| { + if is_public { + Empty::new().constrained() + } else { + render_icon_button( + theme.private_button.style_for(state, false), + "icons/lock-8.svg", + ) + .aligned() + .constrained() + } + .with_width(host_avatar_height) + .boxed() + }) + .with_cursor_style(if is_public { + CursorStyle::default() + } else { + CursorStyle::PointingHand + }) + .on_click(move |_, _, cx| { + cx.dispatch_action(ToggleProjectPublic { project: None }) + }) + .boxed(), + ) + .with_child( + Label::new(worktree_root_names, row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(row.container) + .boxed() + }) + .boxed() + } + fn render_contact_request( user: Arc, user_store: ModelHandle, @@ -557,6 +631,7 @@ impl ContactsPanel { fn update_entries(&mut self, cx: &mut ViewContext) { let user_store = self.user_store.read(cx); + let project_store = self.project_store.read(cx); let query = self.filter_editor.read(cx).text(cx); let executor = cx.background().clone(); @@ -629,20 +704,37 @@ impl ContactsPanel { } } + let current_user = user_store.current_user(); + let contacts = user_store.contacts(); if !contacts.is_empty() { + // Always put the current user first. self.match_candidates.clear(); - self.match_candidates - .extend( - contacts - .iter() - .enumerate() - .map(|(ix, contact)| StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }), - ); + self.match_candidates.reserve(contacts.len()); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: Default::default(), + char_bag: Default::default(), + }); + for (ix, contact) in contacts.iter().enumerate() { + let candidate = StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }; + if current_user + .as_ref() + .map_or(false, |current_user| current_user.id == contact.user.id) + { + self.match_candidates[0] = candidate; + } else { + self.match_candidates.push(candidate); + } + } + if self.match_candidates[0].string.is_empty() { + self.match_candidates.remove(0); + } + let matches = executor.block(match_strings( &self.match_candidates, &query, @@ -666,16 +758,60 @@ impl ContactsPanel { for mat in matches { let contact = &contacts[mat.candidate_id]; self.entries.push(ContactEntry::Contact(contact.clone())); - self.entries - .extend(contact.projects.iter().enumerate().filter_map( - |(ix, project)| { - if project.worktree_root_names.is_empty() { + + let is_current_user = current_user + .as_ref() + .map_or(false, |user| user.id == contact.user.id); + if is_current_user { + let mut open_projects = + project_store.projects(cx).collect::>(); + self.entries.extend( + contact.projects.iter().enumerate().filter_map( + |(ix, project)| { + let open_project = open_projects + .iter() + .position(|p| { + p.read(cx).remote_id() == Some(project.id) + }) + .map(|ix| open_projects.remove(ix).downgrade()); + if project.worktree_root_names.is_empty() { + None + } else { + Some(ContactEntry::ContactProject( + contact.clone(), + ix, + open_project, + )) + } + }, + ), + ); + self.entries.extend(open_projects.into_iter().filter_map( + |project| { + if project.read(cx).visible_worktrees(cx).next().is_none() { None } else { - Some(ContactEntry::ContactProject(contact.clone(), ix)) + Some(ContactEntry::PrivateProject(project.downgrade())) } }, )); + } else { + self.entries.extend( + contact.projects.iter().enumerate().filter_map( + |(ix, project)| { + if project.worktree_root_names.is_empty() { + None + } else { + Some(ContactEntry::ContactProject( + contact.clone(), + ix, + None, + )) + } + }, + ), + ); + } } } } @@ -757,11 +893,18 @@ impl ContactsPanel { let section = *section; self.toggle_expanded(&ToggleExpanded(section), cx); } - ContactEntry::ContactProject(contact, project_index) => cx - .dispatch_global_action(JoinProject { - contact: contact.clone(), - project_index: *project_index, - }), + ContactEntry::ContactProject(contact, project_index, open_project) => { + if let Some(open_project) = open_project { + workspace::activate_workspace_for_project(cx, |_, cx| { + cx.model_id() == open_project.id() + }); + } else { + cx.dispatch_global_action(JoinProject { + contact: contact.clone(), + project_index: *project_index, + }) + } + } _ => {} } } @@ -952,11 +1095,16 @@ impl PartialEq for ContactEntry { return contact_1.user.id == contact_2.user.id; } } - ContactEntry::ContactProject(contact_1, ix_1) => { - if let ContactEntry::ContactProject(contact_2, ix_2) = other { + ContactEntry::ContactProject(contact_1, ix_1, _) => { + if let ContactEntry::ContactProject(contact_2, ix_2, _) = other { return contact_1.user.id == contact_2.user.id && ix_1 == ix_2; } } + ContactEntry::PrivateProject(project_1) => { + if let ContactEntry::PrivateProject(project_2) = other { + return project_1.id() == project_2.id(); + } + } } false } @@ -965,20 +1113,55 @@ impl PartialEq for ContactEntry { #[cfg(test)] mod tests { use super::*; - use client::{proto, test::FakeServer, Client}; - use gpui::TestAppContext; + use client::{ + proto, + test::{FakeHttpClient, FakeServer}, + Client, + }; + use gpui::{serde_json::json, TestAppContext}; use language::LanguageRegistry; - use project::Project; - use theme::ThemeRegistry; - use workspace::AppState; + use project::{FakeFs, Project}; #[gpui::test] async fn test_contact_panel(cx: &mut TestAppContext) { - let (app_state, server) = init(cx).await; - let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = cx.add_view(0, |cx| Workspace::new(project, cx)); + Settings::test_async(cx); + let current_user_id = 100; + + let languages = Arc::new(LanguageRegistry::test()); + let http_client = FakeHttpClient::with_404_response(); + let client = Client::new(http_client.clone()); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let project_store = cx.add_model(|_| ProjectStore::default()); + let server = FakeServer::for_client(current_user_id, &client, &cx).await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/private_dir", json!({ "one.rs": "" })) + .await; + let project = cx.update(|cx| { + Project::local( + false, + client.clone(), + user_store.clone(), + project_store.clone(), + languages, + fs, + cx, + ) + }); + project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/private_dir", true, cx) + }) + .await + .unwrap(); + + let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx)); let panel = cx.add_view(0, |cx| { - ContactsPanel::new(app_state.user_store.clone(), workspace.downgrade(), cx) + ContactsPanel::new( + user_store.clone(), + project_store.clone(), + workspace.downgrade(), + cx, + ) }); let get_users_request = server.receive::().await.unwrap(); @@ -1001,6 +1184,11 @@ mod tests { github_login: name.to_string(), ..Default::default() }) + .chain([proto::User { + id: current_user_id, + github_login: "the_current_user".to_string(), + ..Default::default() + }]) .collect(), }, ) @@ -1039,6 +1227,16 @@ mod tests { should_notify: false, projects: vec![], }, + proto::Contact { + user_id: current_user_id, + online: true, + should_notify: false, + projects: vec![proto::ProjectMetadata { + id: 103, + worktree_root_names: vec!["dir3".to_string()], + guests: vec![3], + }], + }, ], ..Default::default() }); @@ -1052,6 +1250,9 @@ mod tests { " incoming user_one", " outgoing user_two", "v Online", + " the_current_user", + " dir3", + " 🔒 private_dir", " user_four", " dir2", " user_three", @@ -1133,12 +1334,24 @@ mod tests { ContactEntry::Contact(contact) => { format!(" {}", contact.user.github_login) } - ContactEntry::ContactProject(contact, project_ix) => { + ContactEntry::ContactProject(contact, project_ix, _) => { format!( " {}", contact.projects[*project_ix].worktree_root_names.join(", ") ) } + ContactEntry::PrivateProject(project) => cx.read(|cx| { + format!( + " 🔒 {}", + project + .upgrade(cx) + .unwrap() + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join(", ") + ) + }), }; if panel.selection == Some(ix) { @@ -1150,28 +1363,4 @@ mod tests { entries }) } - - async fn init(cx: &mut TestAppContext) -> (Arc, FakeServer) { - cx.update(|cx| cx.set_global(Settings::test(cx))); - let themes = ThemeRegistry::new((), cx.font_cache()); - let fs = project::FakeFs::new(cx.background().clone()); - let languages = Arc::new(LanguageRegistry::test()); - let http_client = client::test::FakeHttpClient::with_404_response(); - let mut client = Client::new(http_client.clone()); - let server = FakeServer::for_client(100, &mut client, &cx).await; - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - - ( - Arc::new(AppState { - languages, - themes, - client, - user_store: user_store.clone(), - fs, - build_window_options: || Default::default(), - initialize_workspace: |_, _, _| {}, - }), - server, - ) - } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 19de98b3ce..bcdce61a05 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4604,6 +4604,10 @@ impl WeakViewHandle { self.view_id } + pub fn window_id(&self) -> usize { + self.window_id + } + pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option> { cx.upgrade_view_handle(self) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 16a6481a43..10de12fbe1 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -147,6 +147,12 @@ pub struct AppVersion { patch: usize, } +impl Default for CursorStyle { + fn default() -> Self { + Self::Arrow + } +} + impl FromStr for AppVersion { type Err = anyhow::Error; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 58dc2ced20..9f8ba97ea8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -59,6 +59,11 @@ pub trait Item: Entity { fn entry_id(&self, cx: &AppContext) -> Option; } +#[derive(Default)] +pub struct ProjectStore { + projects: Vec>, +} + pub struct Project { worktrees: Vec, active_entry: Option, @@ -75,6 +80,7 @@ pub struct Project { next_entry_id: Arc, next_diagnostic_group_id: usize, user_store: ModelHandle, + project_store: ModelHandle, fs: Arc, client_state: ProjectClientState, collaborators: HashMap, @@ -121,6 +127,7 @@ enum ProjectClientState { remote_id_tx: watch::Sender>, remote_id_rx: watch::Receiver>, public_tx: watch::Sender, + public_rx: watch::Receiver, _maintain_remote_id_task: Task>, }, Remote { @@ -309,15 +316,17 @@ impl Project { public: bool, client: Arc, user_store: ModelHandle, + project_store: ModelHandle, languages: Arc, fs: Arc, cx: &mut MutableAppContext, ) -> ModelHandle { cx.add_model(|cx: &mut ModelContext| { - let (public_tx, mut public_rx) = watch::channel_with(public); + let (public_tx, public_rx) = watch::channel_with(public); let (remote_id_tx, remote_id_rx) = watch::channel(); let _maintain_remote_id_task = cx.spawn_weak({ let mut status_rx = client.clone().status(); + let mut public_rx = public_rx.clone(); move |this, mut cx| async move { loop { select_biased! { @@ -336,6 +345,9 @@ impl Project { } }); + let handle = cx.weak_handle(); + project_store.update(cx, |store, cx| store.add(handle, cx)); + let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); Self { worktrees: Default::default(), @@ -350,6 +362,7 @@ impl Project { remote_id_tx, remote_id_rx, public_tx, + public_rx, _maintain_remote_id_task, }, opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), @@ -358,6 +371,7 @@ impl Project { languages, client, user_store, + project_store, fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -376,9 +390,10 @@ impl Project { remote_id: u64, client: Arc, user_store: ModelHandle, + project_store: ModelHandle, languages: Arc, fs: Arc, - cx: &mut AsyncAppContext, + mut cx: AsyncAppContext, ) -> Result, JoinProjectError> { client.authenticate_and_connect(true, &cx).await?; @@ -418,6 +433,9 @@ impl Project { let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); let this = cx.add_model(|cx: &mut ModelContext| { + let handle = cx.weak_handle(); + project_store.update(cx, |store, cx| store.add(handle, cx)); + let mut this = Self { worktrees: Vec::new(), loading_buffers: Default::default(), @@ -428,6 +446,7 @@ impl Project { collaborators: Default::default(), languages, user_store: user_store.clone(), + project_store, fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -488,15 +507,15 @@ impl Project { .map(|peer| peer.user_id) .collect(); user_store - .update(cx, |user_store, cx| user_store.get_users(user_ids, cx)) + .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx)) .await?; let mut collaborators = HashMap::default(); for message in response.collaborators { - let collaborator = Collaborator::from_proto(message, &user_store, cx).await?; + let collaborator = Collaborator::from_proto(message, &user_store, &mut cx).await?; collaborators.insert(collaborator.peer_id, collaborator); } - this.update(cx, |this, _| { + this.update(&mut cx, |this, _| { this.collaborators = collaborators; }); @@ -513,7 +532,10 @@ impl Project { let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = cx.update(|cx| Project::local(true, client, user_store, languages, fs, cx)); + let project_store = cx.add_model(|_| ProjectStore::default()); + let project = cx.update(|cx| { + Project::local(true, client, user_store, project_store, languages, fs, cx) + }); for path in root_paths { let (tree, _) = project .update(cx, |project, cx| { @@ -608,11 +630,10 @@ impl Project { } } - pub fn is_public(&mut self) -> bool { - if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { - *public_tx.borrow() - } else { - true + pub fn is_public(&self) -> bool { + match &self.client_state { + ProjectClientState::Local { public_rx, .. } => *public_rx.borrow(), + ProjectClientState::Remote { .. } => true, } } @@ -752,6 +773,11 @@ impl Project { }) } + pub fn worktree_root_names<'a>(&'a self, cx: &'a AppContext) -> impl Iterator { + self.visible_worktrees(cx) + .map(|tree| tree.read(cx).root_name()) + } + pub fn worktree_for_id( &self, id: WorktreeId, @@ -779,6 +805,20 @@ impl Project { .map(|worktree| worktree.read(cx).id()) } + pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool { + paths.iter().all(|path| self.contains_path(&path, cx)) + } + + pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool { + for worktree in self.worktrees(cx) { + let worktree = worktree.read(cx).as_local(); + if worktree.map_or(false, |w| w.contains_abs_path(path)) { + return true; + } + } + false + } + pub fn create_entry( &mut self, project_path: impl Into, @@ -5154,6 +5194,42 @@ impl Project { } } +impl ProjectStore { + pub fn projects<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.projects + .iter() + .filter_map(|project| project.upgrade(cx)) + } + + fn add(&mut self, project: WeakModelHandle, cx: &mut ModelContext) { + if let Err(ix) = self + .projects + .binary_search_by_key(&project.id(), WeakModelHandle::id) + { + self.projects.insert(ix, project); + } + cx.notify(); + } + + fn prune(&mut self, cx: &mut ModelContext) { + let mut did_change = false; + self.projects.retain(|project| { + if project.is_upgradable(cx) { + true + } else { + did_change = true; + false + } + }); + if did_change { + cx.notify(); + } + } +} + impl WorktreeHandle { pub fn upgrade(&self, cx: &AppContext) -> Option> { match self { @@ -5232,10 +5308,16 @@ impl<'a> Iterator for CandidateSetIter<'a> { } } +impl Entity for ProjectStore { + type Event = (); +} + impl Entity for Project { type Event = Event; - fn release(&mut self, _: &mut gpui::MutableAppContext) { + fn release(&mut self, cx: &mut gpui::MutableAppContext) { + self.project_store.update(cx, ProjectStore::prune); + match &self.client_state { ProjectClientState::Local { remote_id_rx, .. } => { if let Some(project_id) = *remote_id_rx.borrow() { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3639127633..a9bd2b2b48 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -281,6 +281,7 @@ pub struct ContactsPanel { pub contact_button_spacing: f32, pub disabled_contact_button: IconButton, pub tree_branch: Interactive, + pub private_button: Interactive, pub section_icon_size: f32, pub invite_row: Interactive, } diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index 3720d9ec43..c3d1e3c7e6 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/crates/workspace/src/waiting_room.rs @@ -85,9 +85,10 @@ impl WaitingRoom { project_id, app_state.client.clone(), app_state.user_store.clone(), + app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), - &mut cx, + cx.clone(), ) .await; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1a38cd4866..0889077a1e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -17,19 +17,20 @@ use gpui::{ color::Color, elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, - impl_internal_actions, + impl_actions, impl_internal_actions, json::{self, ToJson}, platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, - ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, + Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; -use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; +use serde::Deserialize; use settings::Settings; use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus}; use smallvec::SmallVec; @@ -98,6 +99,12 @@ pub struct OpenPaths { pub paths: Vec, } +#[derive(Clone, Deserialize)] +pub struct ToggleProjectPublic { + #[serde(skip_deserializing)] + pub project: Option>, +} + #[derive(Clone)] pub struct ToggleFollow(pub PeerId); @@ -116,6 +123,7 @@ impl_internal_actions!( RemoveFolderFromProject ] ); +impl_actions!(workspace, [ToggleProjectPublic]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -160,6 +168,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::add_folder_to_project); cx.add_action(Workspace::remove_folder_from_project); + cx.add_action(Workspace::toggle_project_public); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -222,6 +231,7 @@ pub struct AppState { pub themes: Arc, pub client: Arc, pub user_store: ModelHandle, + pub project_store: ModelHandle, pub fs: Arc, pub build_window_options: fn() -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), @@ -682,6 +692,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); + let project_store = cx.add_model(|_| ProjectStore::default()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); Arc::new(Self { @@ -690,6 +701,7 @@ impl AppState { fs, languages, user_store, + project_store, initialize_workspace: |_, _, _| {}, build_window_options: || Default::default(), }) @@ -837,10 +849,7 @@ impl Workspace { _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); - - cx.defer(|this, cx| { - this.update_window_title(cx); - }); + cx.defer(|this, cx| this.update_window_title(cx)); this } @@ -876,20 +885,6 @@ impl Workspace { self.project.read(cx).worktrees(cx) } - pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool { - paths.iter().all(|path| self.contains_path(&path, cx)) - } - - pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool { - for worktree in self.worktrees(cx) { - let worktree = worktree.read(cx).as_local(); - if worktree.map_or(false, |w| w.contains_abs_path(path)) { - return true; - } - } - false - } - pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { let futures = self .worktrees(cx) @@ -1054,6 +1049,23 @@ impl Workspace { .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx)); } + fn toggle_project_public(&mut self, action: &ToggleProjectPublic, cx: &mut ViewContext) { + let project = if let Some(project) = action.project { + if let Some(project) = project.upgrade(cx) { + project + } else { + return; + } + } else { + self.project.clone() + }; + + project.update(cx, |project, _| { + let is_public = project.is_public(); + project.set_public(!is_public); + }); + } + fn project_path_for_path( &self, abs_path: &Path, @@ -1668,8 +1680,15 @@ impl Workspace { } fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { + let project = &self.project.read(cx); + let replica_id = project.replica_id(); let mut worktree_root_names = String::new(); - self.worktree_root_names(&mut worktree_root_names, cx); + for (i, name) in project.worktree_root_names(cx).enumerate() { + if i > 0 { + worktree_root_names.push_str(", "); + } + worktree_root_names.push_str(name); + } ConstrainedBox::new( Container::new( @@ -1686,7 +1705,7 @@ impl Workspace { .with_children(self.render_collaborators(theme, cx)) .with_children(self.render_current_user( self.user_store.read(cx).current_user().as_ref(), - self.project.read(cx).replica_id(), + replica_id, theme, cx, )) @@ -1714,6 +1733,7 @@ impl Workspace { fn update_window_title(&mut self, cx: &mut ViewContext) { let mut title = String::new(); + let project = self.project().read(cx); if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { let filename = path .path @@ -1721,8 +1741,7 @@ impl Workspace { .map(|s| s.to_string_lossy()) .or_else(|| { Some(Cow::Borrowed( - self.project() - .read(cx) + project .worktree_for_id(path.worktree_id, cx)? .read(cx) .root_name(), @@ -1733,22 +1752,18 @@ impl Workspace { title.push_str(" — "); } } - self.worktree_root_names(&mut title, cx); + for (i, name) in project.worktree_root_names(cx).enumerate() { + if i > 0 { + title.push_str(", "); + } + title.push_str(name); + } if title.is_empty() { title = "empty project".to_string(); } cx.set_window_title(&title); } - fn worktree_root_names(&self, string: &mut String, cx: &mut MutableAppContext) { - for (i, worktree) in self.project.read(cx).visible_worktrees(cx).enumerate() { - if i != 0 { - string.push_str(", "); - } - string.push_str(worktree.read(cx).root_name()); - } - } - fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext) -> Vec { let mut collaborators = self .project @@ -2365,6 +2380,22 @@ fn open(_: &Open, cx: &mut MutableAppContext) { pub struct WorkspaceCreated(WeakViewHandle); +pub fn activate_workspace_for_project( + cx: &mut MutableAppContext, + predicate: impl Fn(&mut Project, &mut ModelContext) -> bool, +) -> Option> { + for window_id in cx.window_ids().collect::>() { + if let Some(workspace_handle) = cx.root_view::(window_id) { + let project = workspace_handle.read(cx).project.clone(); + if project.update(cx, &predicate) { + cx.activate_window(window_id); + return Some(workspace_handle); + } + } + } + None +} + pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, @@ -2376,22 +2407,8 @@ pub fn open_paths( log::info!("open paths {:?}", abs_paths); // Open paths in existing workspace if possible - let mut existing = None; - for window_id in cx.window_ids().collect::>() { - if let Some(workspace_handle) = cx.root_view::(window_id) { - if workspace_handle.update(cx, |workspace, cx| { - if workspace.contains_paths(abs_paths, cx.as_ref()) { - cx.activate_window(window_id); - existing = Some(workspace_handle.clone()); - true - } else { - false - } - }) { - break; - } - } - } + let existing = + activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx)); let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); @@ -2410,6 +2427,7 @@ pub fn open_paths( false, app_state.client.clone(), app_state.user_store.clone(), + app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), cx, @@ -2467,6 +2485,7 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { false, app_state.client.clone(), app_state.user_store.clone(), + app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), cx, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 20d467f276..1427343e4b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -23,7 +23,7 @@ use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task}; use isahc::{config::Configurable, AsyncBody, Request}; use log::LevelFilter; use parking_lot::Mutex; -use project::Fs; +use project::{Fs, ProjectStore}; use serde_json::json; use settings::{self, KeymapFileContent, Settings, SettingsFileContent}; use smol::process::Command; @@ -136,6 +136,7 @@ fn main() { let client = client::Client::new(http.clone()); let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); + let project_store = cx.add_model(|_| ProjectStore::default()); context_menu::init(cx); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); @@ -195,6 +196,7 @@ fn main() { themes, client: client.clone(), user_store, + project_store, fs, build_window_options, initialize_workspace, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 63b9bb5fea..5261d191a3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -181,7 +181,12 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::new(workspace.project().clone(), cx); let contact_panel = cx.add_view(|cx| { - ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx) + ContactsPanel::new( + app_state.user_store.clone(), + app_state.project_store.clone(), + workspace.weak_handle(), + cx, + ) }); workspace.left_sidebar().update(cx, |sidebar, cx| { @@ -298,6 +303,7 @@ fn open_config_file( false, app_state.client.clone(), app_state.user_store.clone(), + app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), cx, diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index d52f7f92b2..5253a1185c 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -68,6 +68,11 @@ export default function contactsPanel(theme: Theme) { buttonWidth: 8, iconWidth: 8, }, + privateButton: { + iconWidth: 8, + color: iconColor(theme, "primary"), + buttonWidth: 8, + }, rowHeight: 28, sectionIconSize: 8, headerRow: { From 8d46edd26c9674069178409fd7e7db23b18a609d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 May 2022 15:31:47 -0700 Subject: [PATCH 03/22] Avoid holding RefCell borrow while calling TestAppContext::spawn callback --- crates/gpui/src/app.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index bcdce61a05..695ce7e238 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -499,7 +499,14 @@ impl TestAppContext { Fut: 'static + Future, T: 'static, { - self.cx.borrow_mut().spawn(f) + let foreground = self.foreground(); + let future = f(self.to_async()); + let cx = self.to_async(); + foreground.spawn(async move { + let result = future.await; + cx.0.borrow_mut().flush_effects(); + result + }) } pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option) { From 8f676e76b34ab41078a19ba8709c83b6bb97053d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 May 2022 15:32:54 -0700 Subject: [PATCH 04/22] Fix mismatched client/context in integration test --- crates/collab/src/integration_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 2ba324eed8..ce3a1df16e 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -3194,7 +3194,7 @@ async fn test_contacts( // Add a local project as client B let fs = FakeFs::new(cx_b.background()); fs.create_dir(Path::new("/b")).await.unwrap(); - let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_a).await; + let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_b).await; deterministic.run_until_parked(); for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { From 3ea061a11e13c517f48bfe0dd357adce9bd4cd68 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 May 2022 16:47:06 -0700 Subject: [PATCH 05/22] Allow making projects private --- crates/collab/src/integration_tests.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 119 +++++++++++++------- crates/project/src/project.rs | 47 +++++--- crates/project/src/worktree.rs | 18 +-- crates/theme/src/theme.rs | 2 +- crates/workspace/src/workspace.rs | 22 ++-- styles/src/styleTree/contactsPanel.ts | 3 +- 7 files changed, 131 insertions(+), 82 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index ce3a1df16e..96ed7714c2 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -532,7 +532,7 @@ async fn test_private_projects( .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); // The project is registered when it is made public. - project_a.update(cx_a, |project, _| project.set_public(true)); + project_a.update(cx_a, |project, cx| project.set_public(true, cx)); deterministic.run_until_parked(); assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); assert!(!client_b diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index ffa3300e70..575639643c 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -207,7 +207,7 @@ impl ContactsPanel { ContactEntry::Contact(contact) => { Self::render_contact(&contact.user, theme, is_selected) } - ContactEntry::ContactProject(contact, project_ix, _) => { + ContactEntry::ContactProject(contact, project_ix, open_project) => { let is_last_project_for_contact = this.entries.get(ix + 1).map_or(true, |next| { if let ContactEntry::ContactProject(next_contact, _, _) = next { @@ -216,10 +216,11 @@ impl ContactsPanel { true } }); - Self::render_contact_project( + Self::render_project( contact.clone(), current_user_id, *project_ix, + open_project.clone(), theme, is_last_project_for_contact, is_selected, @@ -328,10 +329,11 @@ impl ContactsPanel { .boxed() } - fn render_contact_project( + fn render_project( contact: Arc, current_user_id: Option, project_index: usize, + open_project: Option>, theme: &theme::ContactsPanel, is_last_project: bool, is_selected: bool, @@ -340,6 +342,7 @@ impl ContactsPanel { let project = &contact.projects[project_index]; let project_id = project.id; let is_host = Some(contact.user.id) == current_user_id; + let open_project = open_project.and_then(|p| p.upgrade(cx.deref_mut())); let font_cache = cx.font_cache(); let host_avatar_height = theme @@ -354,48 +357,78 @@ impl ContactsPanel { let baseline_offset = row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.style_for(mouse_state, is_selected); let row = theme.project_row.style_for(mouse_state, is_selected); Flex::row() .with_child( - Canvas::new(move |bounds, _, cx| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + Stack::new() + .with_child( + Canvas::new(move |bounds, _, cx| { + let start_x = bounds.min_x() + (bounds.width() / 2.) + - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last_project { - end_y - } else { - bounds.max_y() + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last_project { + end_y + } else { + bounds.max_y() + }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + }) + .boxed(), + ) + .with_children(if mouse_state.hovered && open_project.is_some() { + Some( + MouseEventHandler::new::( + project_id as usize, + cx, + |state, _| { + let mut icon_style = + *theme.private_button.style_for(state, false); + icon_style.container.background_color = + row.container.background_color; + render_icon_button(&icon_style, "icons/lock-8.svg") + .aligned() + .boxed() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - }) - .constrained() - .with_width(host_avatar_height) - .boxed(), + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| { + cx.dispatch_action(ToggleProjectPublic { + project: open_project.clone(), + }) + }) + .boxed(), + ) + } else { + None + }) + .constrained() + .with_width(host_avatar_height) + .boxed(), ) .with_child( Label::new( @@ -467,9 +500,9 @@ impl ContactsPanel { MouseEventHandler::new::(project_id, cx, |state, cx| { let row = theme.project_row.style_for(state, is_selected); let mut worktree_root_names = String::new(); - let project = project.read(cx); - let is_public = project.is_public(); - for tree in project.visible_worktrees(cx) { + let project_ = project.read(cx); + let is_public = project_.is_public(); + for tree in project_.visible_worktrees(cx) { if !worktree_root_names.is_empty() { worktree_root_names.push_str(", "); } @@ -498,7 +531,9 @@ impl ContactsPanel { CursorStyle::PointingHand }) .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { project: None }) + cx.dispatch_action(ToggleProjectPublic { + project: Some(project.clone()), + }) }) .boxed(), ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9f8ba97ea8..6a8adbb2ae 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; -use futures::{future::Shared, select_biased, Future, FutureExt, StreamExt, TryFutureExt}; +use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -25,6 +25,7 @@ use language::{ use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; use parking_lot::Mutex; +use postage::stream::Stream; use postage::watch; use rand::prelude::*; use search::SearchQuery; @@ -325,14 +326,12 @@ impl Project { let (public_tx, public_rx) = watch::channel_with(public); let (remote_id_tx, remote_id_rx) = watch::channel(); let _maintain_remote_id_task = cx.spawn_weak({ - let mut status_rx = client.clone().status(); - let mut public_rx = public_rx.clone(); + let status_rx = client.clone().status(); + let public_rx = public_rx.clone(); move |this, mut cx| async move { - loop { - select_biased! { - value = status_rx.next().fuse() => { value?; } - value = public_rx.next().fuse() => { value?; } - }; + let mut stream = Stream::map(status_rx.clone(), drop) + .merge(Stream::map(public_rx.clone(), drop)); + while stream.recv().await.is_some() { let this = this.upgrade(&cx)?; if status_rx.borrow().is_connected() && *public_rx.borrow() { this.update(&mut cx, |this, cx| this.register(cx)) @@ -342,11 +341,12 @@ impl Project { this.update(&mut cx, |this, cx| this.unregister(cx)); } } + None } }); let handle = cx.weak_handle(); - project_store.update(cx, |store, cx| store.add(handle, cx)); + project_store.update(cx, |store, cx| store.add_project(handle, cx)); let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); Self { @@ -434,7 +434,7 @@ impl Project { let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); let this = cx.add_model(|cx: &mut ModelContext| { let handle = cx.weak_handle(); - project_store.update(cx, |store, cx| store.add(handle, cx)); + project_store.update(cx, |store, cx| store.add_project(handle, cx)); let mut this = Self { worktrees: Vec::new(), @@ -624,9 +624,10 @@ impl Project { &self.fs } - pub fn set_public(&mut self, is_public: bool) { + pub fn set_public(&mut self, is_public: bool, cx: &mut ModelContext) { if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { *public_tx.borrow_mut() = is_public; + self.metadata_changed(cx); } } @@ -648,10 +649,19 @@ impl Project { } if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state { - *remote_id_tx.borrow_mut() = None; + let mut remote_id = remote_id_tx.borrow_mut(); + if let Some(remote_id) = *remote_id { + self.client + .send(proto::UnregisterProject { + project_id: remote_id, + }) + .log_err(); + } + *remote_id = None; } self.subscriptions.clear(); + self.metadata_changed(cx); } fn register(&mut self, cx: &mut ModelContext) -> Task> { @@ -671,6 +681,7 @@ impl Project { *remote_id_tx.borrow_mut() = Some(remote_id); } + this.metadata_changed(cx); cx.emit(Event::RemoteIdChanged(Some(remote_id))); this.subscriptions @@ -745,6 +756,10 @@ impl Project { } } + fn metadata_changed(&mut self, cx: &mut ModelContext) { + self.project_store.update(cx, |_, cx| cx.notify()); + } + pub fn collaborators(&self) -> &HashMap { &self.collaborators } @@ -3743,6 +3758,7 @@ impl Project { false } }); + self.metadata_changed(cx); cx.notify(); } @@ -3772,6 +3788,7 @@ impl Project { self.worktrees .push(WorktreeHandle::Weak(worktree.downgrade())); } + self.metadata_changed(cx); cx.emit(Event::WorktreeAdded); cx.notify(); } @@ -5204,7 +5221,7 @@ impl ProjectStore { .filter_map(|project| project.upgrade(cx)) } - fn add(&mut self, project: WeakModelHandle, cx: &mut ModelContext) { + fn add_project(&mut self, project: WeakModelHandle, cx: &mut ModelContext) { if let Err(ix) = self .projects .binary_search_by_key(&project.id(), WeakModelHandle::id) @@ -5214,7 +5231,7 @@ impl ProjectStore { cx.notify(); } - fn prune(&mut self, cx: &mut ModelContext) { + fn prune_projects(&mut self, cx: &mut ModelContext) { let mut did_change = false; self.projects.retain(|project| { if project.is_upgradable(cx) { @@ -5316,7 +5333,7 @@ impl Entity for Project { type Event = Event; fn release(&mut self, cx: &mut gpui::MutableAppContext) { - self.project_store.update(cx, ProjectStore::prune); + self.project_store.update(cx, ProjectStore::prune_projects); match &self.client_state { ProjectClientState::Local { remote_id_rx, .. } => { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cadfaa520d..05eaecbc97 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -151,14 +151,7 @@ impl Entity for Worktree { fn release(&mut self, _: &mut MutableAppContext) { if let Some(worktree) = self.as_local_mut() { - if let Registration::Done { project_id } = worktree.registration { - let client = worktree.client.clone(); - let unregister_message = proto::UnregisterWorktree { - project_id, - worktree_id: worktree.id().to_proto(), - }; - client.send(unregister_message).log_err(); - } + worktree.unregister(); } } } @@ -1063,6 +1056,15 @@ impl LocalWorktree { pub fn unregister(&mut self) { self.unshare(); + if let Registration::Done { project_id } = self.registration { + self.client + .clone() + .send(proto::UnregisterWorktree { + project_id, + worktree_id: self.id().to_proto(), + }) + .log_err(); + } self.registration = Registration::None; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a9bd2b2b48..52b5e8df36 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -319,7 +319,7 @@ pub struct Icon { pub path: String, } -#[derive(Clone, Deserialize, Default)] +#[derive(Deserialize, Clone, Copy, Default)] pub struct IconButton { #[serde(flatten)] pub container: ContainerStyle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0889077a1e..f6b8c5db09 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -22,7 +22,7 @@ use gpui::{ platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, - Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -102,7 +102,7 @@ pub struct OpenPaths { #[derive(Clone, Deserialize)] pub struct ToggleProjectPublic { #[serde(skip_deserializing)] - pub project: Option>, + pub project: Option>, } #[derive(Clone)] @@ -1050,19 +1050,13 @@ impl Workspace { } fn toggle_project_public(&mut self, action: &ToggleProjectPublic, cx: &mut ViewContext) { - let project = if let Some(project) = action.project { - if let Some(project) = project.upgrade(cx) { - project - } else { - return; - } - } else { - self.project.clone() - }; - - project.update(cx, |project, _| { + let project = action + .project + .clone() + .unwrap_or_else(|| self.project.clone()); + project.update(cx, |project, cx| { let is_public = project.is_public(); - project.set_public(!is_public); + project.set_public(!is_public, cx); }); } diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 5253a1185c..cf08e770ab 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -71,7 +71,8 @@ export default function contactsPanel(theme: Theme) { privateButton: { iconWidth: 8, color: iconColor(theme, "primary"), - buttonWidth: 8, + cornerRadius: 5, + buttonWidth: 12, }, rowHeight: 28, sectionIconSize: 8, From b70396b8fb66e736fbf5d4e6952ab077ce5dec1d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 May 2022 18:09:33 -0700 Subject: [PATCH 06/22] Disconnect FakeServer when dropping it This prevents memory leak errors in tests, due to parked tasks waiting for RPC responses. --- crates/client/src/test.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index a809bd2769..92183e2566 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -178,6 +178,12 @@ impl FakeServer { } } +impl Drop for FakeServer { + fn drop(&mut self) { + self.disconnect(); + } +} + pub struct FakeHttpClient { handler: Box< dyn 'static From 4d4ec793e25be12d311bf624438ba6bdbbbd56dd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Jun 2022 14:52:51 -0700 Subject: [PATCH 07/22] Remove stray println --- crates/collab/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index b3be40040c..f8a9fedb66 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -82,7 +82,6 @@ pub fn init_tracing(config: &Config) -> Option<()> { use tracing_subscriber::layer::SubscriberExt; let rust_log = config.rust_log.clone()?; - println!("HEY!"); LogTracer::init().log_err()?; let subscriber = tracing_subscriber::Registry::default() From d11beb3c02e06c164e62df4026ee50b83660901a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Jun 2022 13:41:48 -0700 Subject: [PATCH 08/22] Change project registration RPC APIs to smooth out UI updates * Make `UnregisterProject` a request. This way the client-side project can wait to clear out its remote id until the request has completed, so that the contacts panel can avoid showing duplicate private/public projects in the brief time after unregistering a project, before the next UpdateCollaborators message is received. * Remove the `RegisterWorktree` and `UnregisterWorktree` methods and replace them with a single `UpdateProject` method that idempotently updates the Project's list of worktrees. --- crates/collab/src/rpc.rs | 71 ++++--------- crates/collab/src/rpc/store.rs | 54 ++++------ crates/project/src/project.rs | 187 ++++++++++++++++----------------- crates/project/src/worktree.rs | 77 ++------------ crates/rpc/proto/zed.proto | 26 +++-- crates/rpc/src/proto.rs | 8 +- 6 files changed, 162 insertions(+), 261 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5e0c6f7789..c19538f55c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -141,12 +141,11 @@ impl Server { server .add_request_handler(Server::ping) .add_request_handler(Server::register_project) - .add_message_handler(Server::unregister_project) + .add_request_handler(Server::unregister_project) .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) .add_message_handler(Server::respond_to_join_project_request) - .add_request_handler(Server::register_worktree) - .add_message_handler(Server::unregister_worktree) + .add_message_handler(Server::update_project) .add_request_handler(Server::update_worktree) .add_message_handler(Server::start_language_server) .add_message_handler(Server::update_language_server) @@ -484,14 +483,15 @@ impl Server { user_id = state.user_id_for_connection(request.sender_id)?; project_id = state.register_project(request.sender_id, user_id); }; - self.update_user_contacts(user_id).await?; response.send(proto::RegisterProjectResponse { project_id })?; + self.update_user_contacts(user_id).await?; Ok(()) } async fn unregister_project( self: Arc, request: TypedEnvelope, + response: Response, ) -> Result<()> { let (user_id, project) = { let mut state = self.store_mut().await; @@ -529,6 +529,7 @@ impl Server { } self.update_user_contacts(user_id).await?; + response.send(proto::Ack {})?; Ok(()) } @@ -568,6 +569,7 @@ impl Server { response: Response, ) -> Result<()> { let project_id = request.payload.project_id; + let host_user_id; let guest_user_id; let host_connection_id; @@ -768,63 +770,28 @@ impl Server { Ok(()) } - async fn register_worktree( + async fn update_project( self: Arc, - request: TypedEnvelope, - response: Response, + request: TypedEnvelope, ) -> Result<()> { - let host_user_id; + let user_id; { let mut state = self.store_mut().await; - host_user_id = state.user_id_for_connection(request.sender_id)?; - + user_id = state.user_id_for_connection(request.sender_id)?; let guest_connection_ids = state .read_project(request.payload.project_id, request.sender_id)? .guest_connection_ids(); - state.register_worktree( + state.update_project( request.payload.project_id, - request.payload.worktree_id, + &request.payload.worktrees, request.sender_id, - Worktree { - root_name: request.payload.root_name.clone(), - visible: request.payload.visible, - ..Default::default() - }, )?; - broadcast(request.sender_id, guest_connection_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - } - self.update_user_contacts(host_user_id).await?; - response.send(proto::Ack {})?; - Ok(()) - } - - async fn unregister_worktree( - self: Arc, - request: TypedEnvelope, - ) -> Result<()> { - let host_user_id; - let project_id = request.payload.project_id; - let worktree_id = request.payload.worktree_id; - { - let mut state = self.store_mut().await; - let (_, guest_connection_ids) = - state.unregister_worktree(project_id, worktree_id, request.sender_id)?; - host_user_id = state.user_id_for_connection(request.sender_id)?; - broadcast(request.sender_id, guest_connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::UnregisterWorktree { - project_id, - worktree_id, - }, - ) - }); - } - self.update_user_contacts(host_user_id).await?; + }; + self.update_user_contacts(user_id).await?; Ok(()) } @@ -833,10 +800,11 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let connection_ids = self.store_mut().await.update_worktree( + let (connection_ids, metadata_changed) = self.store_mut().await.update_worktree( request.sender_id, request.payload.project_id, request.payload.worktree_id, + &request.payload.root_name, &request.payload.removed_entries, &request.payload.updated_entries, request.payload.scan_id, @@ -846,6 +814,13 @@ impl Server { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); + if metadata_changed { + let user_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + self.update_user_contacts(user_id).await?; + } response.send(proto::Ack {})?; Ok(()) } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 78227999bc..03e529df3e 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -312,19 +312,32 @@ impl Store { project_id } - pub fn register_worktree( + pub fn update_project( &mut self, project_id: u64, - worktree_id: u64, + worktrees: &[proto::WorktreeMetadata], connection_id: ConnectionId, - worktree: Worktree, ) -> Result<()> { let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { - project.worktrees.insert(worktree_id, worktree); + let mut old_worktrees = mem::take(&mut project.worktrees); + for worktree in worktrees { + if let Some(old_worktree) = old_worktrees.remove(&worktree.id) { + project.worktrees.insert(worktree.id, old_worktree); + } else { + project.worktrees.insert( + worktree.id, + Worktree { + root_name: worktree.root_name.clone(), + visible: worktree.visible, + ..Default::default() + }, + ); + } + } Ok(()) } else { Err(anyhow!("no such project"))? @@ -374,27 +387,6 @@ impl Store { } } - pub fn unregister_worktree( - &mut self, - project_id: u64, - worktree_id: u64, - acting_connection_id: ConnectionId, - ) -> Result<(Worktree, Vec)> { - let project = self - .projects - .get_mut(&project_id) - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection_id != acting_connection_id { - Err(anyhow!("not your worktree"))?; - } - - let worktree = project - .worktrees - .remove(&worktree_id) - .ok_or_else(|| anyhow!("no such worktree"))?; - Ok((worktree, project.guest_connection_ids())) - } - pub fn update_diagnostic_summary( &mut self, project_id: u64, @@ -573,15 +565,15 @@ impl Store { connection_id: ConnectionId, project_id: u64, worktree_id: u64, + worktree_root_name: &str, removed_entries: &[u64], updated_entries: &[proto::Entry], scan_id: u64, - ) -> Result> { + ) -> Result<(Vec, bool)> { let project = self.write_project(project_id, connection_id)?; - let worktree = project - .worktrees - .get_mut(&worktree_id) - .ok_or_else(|| anyhow!("no such worktree"))?; + let mut worktree = project.worktrees.entry(worktree_id).or_default(); + let metadata_changed = worktree_root_name != worktree.root_name; + worktree.root_name = worktree_root_name.to_string(); for entry_id in removed_entries { worktree.entries.remove(&entry_id); } @@ -590,7 +582,7 @@ impl Store { } worktree.scan_id = scan_id; let connection_ids = project.connection_ids(); - Ok(connection_ids) + Ok((connection_ids, metadata_changed)) } pub fn project_connection_ids( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6a8adbb2ae..85b6660021 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -282,8 +282,7 @@ impl Project { client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_remove_collaborator); client.add_model_message_handler(Self::handle_join_project_request_cancelled); - client.add_model_message_handler(Self::handle_register_worktree); - client.add_model_message_handler(Self::handle_unregister_worktree); + client.add_model_message_handler(Self::handle_update_project); client.add_model_message_handler(Self::handle_unregister_project); client.add_model_message_handler(Self::handle_project_unshared); client.add_model_message_handler(Self::handle_update_buffer_file); @@ -338,7 +337,9 @@ impl Project { .await .log_err()?; } else { - this.update(&mut cx, |this, cx| this.unregister(cx)); + this.update(&mut cx, |this, cx| this.unregister(cx)) + .await + .log_err(); } } None @@ -638,30 +639,29 @@ impl Project { } } - fn unregister(&mut self, cx: &mut ModelContext) { + fn unregister(&mut self, cx: &mut ModelContext) -> Task> { self.unshared(cx); - for worktree in &self.worktrees { - if let Some(worktree) = worktree.upgrade(cx) { - worktree.update(cx, |worktree, _| { - worktree.as_local_mut().unwrap().unregister(); + if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state { + if let Some(remote_id) = *remote_id_rx.borrow() { + let request = self.client.request(proto::UnregisterProject { + project_id: remote_id, + }); + return cx.spawn(|this, mut cx| async move { + let response = request.await; + this.update(&mut cx, |this, cx| { + if let ProjectClientState::Local { remote_id_tx, .. } = + &mut this.client_state + { + *remote_id_tx.borrow_mut() = None; + } + this.subscriptions.clear(); + this.metadata_changed(cx); + }); + response.map(drop) }); } } - - if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state { - let mut remote_id = remote_id_tx.borrow_mut(); - if let Some(remote_id) = *remote_id { - self.client - .send(proto::UnregisterProject { - project_id: remote_id, - }) - .log_err(); - } - *remote_id = None; - } - - self.subscriptions.clear(); - self.metadata_changed(cx); + Task::ready(Ok(())) } fn register(&mut self, cx: &mut ModelContext) -> Task> { @@ -674,8 +674,6 @@ impl Project { let response = self.client.request(proto::RegisterProject {}); cx.spawn(|this, mut cx| async move { let remote_id = response.await?.project_id; - - let mut registrations = Vec::new(); this.update(&mut cx, |this, cx| { if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state { *remote_id_tx.borrow_mut() = Some(remote_id); @@ -683,22 +681,10 @@ impl Project { this.metadata_changed(cx); cx.emit(Event::RemoteIdChanged(Some(remote_id))); - this.subscriptions .push(this.client.add_model_for_remote_entity(remote_id, cx)); - - for worktree in &this.worktrees { - if let Some(worktree) = worktree.upgrade(cx) { - registrations.push(worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.register(remote_id, cx) - })); - } - } - }); - - futures::future::try_join_all(registrations).await?; - Ok(()) + Ok(()) + }) }) } @@ -757,7 +743,27 @@ impl Project { } fn metadata_changed(&mut self, cx: &mut ModelContext) { + cx.notify(); self.project_store.update(cx, |_, cx| cx.notify()); + + if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { + if let Some(project_id) = *remote_id_rx.borrow() { + self.client + .send(proto::UpdateProject { + project_id, + worktrees: self + .worktrees + .iter() + .filter_map(|worktree| { + worktree.upgrade(&cx).map(|worktree| { + worktree.read(cx).as_local().unwrap().metadata_proto() + }) + }) + .collect(), + }) + .log_err(); + } + } } pub fn collaborators(&self) -> &HashMap { @@ -3696,37 +3702,19 @@ impl Project { }); let worktree = worktree?; - let remote_project_id = project.update(&mut cx, |project, cx| { + let project_id = project.update(&mut cx, |project, cx| { project.add_worktree(&worktree, cx); - project.remote_id() + project.shared_remote_id() }); - if let Some(project_id) = remote_project_id { - // Because sharing is async, we may have *unshared* the project by the time it completes, - // in which case we need to register the worktree instead. - loop { - if project.read_with(&cx, |project, _| project.is_shared()) { - if worktree - .update(&mut cx, |worktree, cx| { - worktree.as_local_mut().unwrap().share(project_id, cx) - }) - .await - .is_ok() - { - break; - } - } else { - worktree - .update(&mut cx, |worktree, cx| { - worktree - .as_local_mut() - .unwrap() - .register(project_id, cx) - }) - .await?; - break; - } - } + // Because sharing is async, we may have *unshared* the project by the time it completes. + if let Some(project_id) = project_id { + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_local_mut().unwrap().share(project_id, cx) + }) + .await + .log_err(); } Ok(worktree) @@ -4071,40 +4059,51 @@ impl Project { Ok(()) } - async fn handle_register_worktree( + async fn handle_update_project( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, client: Arc, mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, cx| { - let remote_id = this.remote_id().ok_or_else(|| anyhow!("invalid project"))?; let replica_id = this.replica_id(); - let worktree = proto::Worktree { - id: envelope.payload.worktree_id, - root_name: envelope.payload.root_name, - entries: Default::default(), - diagnostic_summaries: Default::default(), - visible: envelope.payload.visible, - scan_id: 0, - }; - let (worktree, load_task) = - Worktree::remote(remote_id, replica_id, worktree, client, cx); - this.add_worktree(&worktree, cx); - load_task.detach(); - Ok(()) - }) - } + let remote_id = this.remote_id().ok_or_else(|| anyhow!("invalid project"))?; + + let mut old_worktrees_by_id = this + .worktrees + .drain(..) + .filter_map(|worktree| { + let worktree = worktree.upgrade(cx)?; + Some((worktree.read(cx).id(), worktree)) + }) + .collect::>(); + + for worktree in envelope.payload.worktrees { + if let Some(old_worktree) = + old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id)) + { + this.worktrees.push(WorktreeHandle::Strong(old_worktree)); + } else { + let worktree = proto::Worktree { + id: worktree.id, + root_name: worktree.root_name, + entries: Default::default(), + diagnostic_summaries: Default::default(), + visible: worktree.visible, + scan_id: 0, + }; + let (worktree, load_task) = + Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx); + this.add_worktree(&worktree, cx); + load_task.detach(); + } + } + + this.metadata_changed(cx); + for (id, _) in old_worktrees_by_id { + cx.emit(Event::WorktreeRemoved(id)); + } - async fn handle_unregister_worktree( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - this.remove_worktree(worktree_id, cx); Ok(()) }) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 05eaecbc97..c1d892c283 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -68,7 +68,6 @@ pub struct LocalWorktree { last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, poll_task: Option>, - registration: Registration, share: Option, diagnostics: HashMap, Vec>>, diagnostic_summaries: TreeMap, @@ -129,13 +128,6 @@ enum ScanState { Err(Arc), } -#[derive(Debug, Eq, PartialEq)] -enum Registration { - None, - Pending, - Done { project_id: u64 }, -} - struct ShareState { project_id: u64, snapshots_tx: Sender, @@ -148,12 +140,6 @@ pub enum Event { impl Entity for Worktree { type Event = Event; - - fn release(&mut self, _: &mut MutableAppContext) { - if let Some(worktree) = self.as_local_mut() { - worktree.unregister(); - } - } } impl Worktree { @@ -479,7 +465,6 @@ impl LocalWorktree { background_snapshot: Arc::new(Mutex::new(snapshot)), last_scan_state_rx, _background_scanner_task: None, - registration: Registration::None, share: None, poll_task: None, diagnostics: Default::default(), @@ -601,6 +586,14 @@ impl LocalWorktree { self.snapshot.clone() } + pub fn metadata_proto(&self) -> proto::WorktreeMetadata { + proto::WorktreeMetadata { + id: self.id().to_proto(), + root_name: self.root_name().to_string(), + visible: self.visible, + } + } + fn load(&self, path: &Path, cx: &mut ModelContext) -> Task> { let handle = cx.handle(); let path = Arc::from(path); @@ -897,46 +890,7 @@ impl LocalWorktree { }) } - pub fn register( - &mut self, - project_id: u64, - cx: &mut ModelContext, - ) -> Task> { - if self.registration != Registration::None { - return Task::ready(Ok(())); - } - - self.registration = Registration::Pending; - let client = self.client.clone(); - let register_message = proto::RegisterWorktree { - project_id, - worktree_id: self.id().to_proto(), - root_name: self.root_name().to_string(), - visible: self.visible, - }; - let request = client.request(register_message); - cx.spawn(|this, mut cx| async move { - let response = request.await; - this.update(&mut cx, |this, _| { - let worktree = this.as_local_mut().unwrap(); - match response { - Ok(_) => { - if worktree.registration == Registration::Pending { - worktree.registration = Registration::Done { project_id }; - } - Ok(()) - } - Err(error) => { - worktree.registration = Registration::None; - Err(error) - } - } - }) - }) - } - pub fn share(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { - let register = self.register(project_id, cx); let (share_tx, share_rx) = oneshot::channel(); let (snapshots_to_send_tx, snapshots_to_send_rx) = smol::channel::unbounded::(); @@ -1041,7 +995,6 @@ impl LocalWorktree { } cx.spawn_weak(|this, cx| async move { - register.await?; if let Some(this) = this.upgrade(&cx) { this.read_with(&cx, |this, _| { let this = this.as_local().unwrap(); @@ -1054,20 +1007,6 @@ impl LocalWorktree { }) } - pub fn unregister(&mut self) { - self.unshare(); - if let Registration::Done { project_id } = self.registration { - self.client - .clone() - .send(proto::UnregisterWorktree { - project_id, - worktree_id: self.id().to_proto(), - }) - .log_err(); - } - self.registration = Registration::None; - } - pub fn unshare(&mut self) { self.share.take(); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3b4d8cc4f9..44bc673fce 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -35,8 +35,7 @@ message Envelope { OpenBufferForSymbol open_buffer_for_symbol = 28; OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; - RegisterWorktree register_worktree = 30; - UnregisterWorktree unregister_worktree = 31; + UpdateProject update_project = 30; UpdateWorktree update_worktree = 32; CreateProjectEntry create_project_entry = 33; @@ -129,6 +128,11 @@ message UnregisterProject { uint64 project_id = 1; } +message UpdateProject { + uint64 project_id = 1; + repeated WorktreeMetadata worktrees = 2; +} + message RequestJoinProject { uint64 requester_id = 1; uint64 project_id = 2; @@ -177,18 +181,6 @@ message LeaveProject { uint64 project_id = 1; } -message RegisterWorktree { - uint64 project_id = 1; - uint64 worktree_id = 2; - string root_name = 3; - bool visible = 4; -} - -message UnregisterWorktree { - uint64 project_id = 1; - uint64 worktree_id = 2; -} - message UpdateWorktree { uint64 project_id = 1; uint64 worktree_id = 2; @@ -934,3 +926,9 @@ message ProjectMetadata { repeated string worktree_root_names = 3; repeated uint64 guests = 4; } + +message WorktreeMetadata { + uint64 id = 1; + string root_name = 2; + bool visible = 3; +} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index b6d7836427..0fcb2eaea3 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -132,7 +132,6 @@ messages!( (Ping, Foreground), (ProjectUnshared, Foreground), (RegisterProject, Foreground), - (RegisterWorktree, Foreground), (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), @@ -151,7 +150,6 @@ messages!( (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), - (UnregisterWorktree, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), @@ -159,6 +157,7 @@ messages!( (UpdateFollowers, Foreground), (UpdateInviteInfo, Foreground), (UpdateLanguageServer, Foreground), + (UpdateProject, Foreground), (UpdateWorktree, Foreground), ); @@ -192,7 +191,6 @@ request_messages!( (PerformRename, PerformRenameResponse), (PrepareRename, PrepareRenameResponse), (RegisterProject, RegisterProjectResponse), - (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), (RemoveContact, Ack), @@ -202,6 +200,7 @@ request_messages!( (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), (Test, Test), + (UnregisterProject, Ack), (UpdateBuffer, Ack), (UpdateWorktree, Ack), ); @@ -242,13 +241,12 @@ entity_messages!( StartLanguageServer, Unfollow, UnregisterProject, - UnregisterWorktree, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, UpdateFollowers, UpdateLanguageServer, - RegisterWorktree, + UpdateProject, UpdateWorktree, ); From d45db1718e00eab833133941d19171ee99182781 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Jun 2022 12:46:26 -0700 Subject: [PATCH 09/22] Style the contact panel while public/private operations are in-flight --- Cargo.lock | 1 + crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contacts_panel.rs | 448 +++++++++++++++----- crates/project/src/project.rs | 10 +- crates/theme/src/theme.rs | 2 +- styles/src/styleTree/contactsPanel.ts | 2 +- 6 files changed, 343 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 882558c051..148b446d7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,6 +956,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "collections", "editor", "futures", "fuzzy", diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index d34e599593..b68f48bb97 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -9,6 +9,7 @@ doctest = false [dependencies] client = { path = "../client" } +collections = { path = "../collections" } editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 575639643c..9be9906f8f 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -400,32 +400,41 @@ impl ContactsPanel { }) .boxed(), ) - .with_children(if mouse_state.hovered && open_project.is_some() { - Some( - MouseEventHandler::new::( - project_id as usize, - cx, - |state, _| { - let mut icon_style = - *theme.private_button.style_for(state, false); - icon_style.container.background_color = - row.container.background_color; - render_icon_button(&icon_style, "icons/lock-8.svg") - .aligned() - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { - project: open_project.clone(), - }) - }) - .boxed(), - ) - } else { - None - }) + .with_children(open_project.and_then(|open_project| { + let is_becoming_private = !open_project.read(cx).is_public(); + if !mouse_state.hovered && !is_becoming_private { + return None; + } + + let mut button = MouseEventHandler::new::( + project_id as usize, + cx, + |state, _| { + let mut icon_style = + *theme.private_button.style_for(state, false); + icon_style.container.background_color = + row.container.background_color; + if is_becoming_private { + icon_style.color = theme.disabled_button.color; + } + render_icon_button(&icon_style, "icons/lock-8.svg") + .aligned() + .boxed() + }, + ); + + if !is_becoming_private { + button = button + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| { + cx.dispatch_action(ToggleProjectPublic { + project: Some(open_project.clone()), + }) + }); + } + + Some(button.boxed()) + })) .constrained() .with_width(host_avatar_height) .boxed(), @@ -501,7 +510,7 @@ impl ContactsPanel { let row = theme.project_row.style_for(state, is_selected); let mut worktree_root_names = String::new(); let project_ = project.read(cx); - let is_public = project_.is_public(); + let is_becoming_public = project_.is_public(); for tree in project_.visible_worktrees(cx) { if !worktree_root_names.is_empty() { worktree_root_names.push_str(", "); @@ -510,33 +519,32 @@ impl ContactsPanel { } Flex::row() - .with_child( - MouseEventHandler::new::(project_id, cx, |state, _| { - if is_public { - Empty::new().constrained() - } else { - render_icon_button( - theme.private_button.style_for(state, false), - "icons/lock-8.svg", - ) - .aligned() - .constrained() - } - .with_width(host_avatar_height) - .boxed() - }) - .with_cursor_style(if is_public { - CursorStyle::default() - } else { - CursorStyle::PointingHand - }) - .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { - project: Some(project.clone()), - }) - }) - .boxed(), - ) + .with_child({ + let mut button = + MouseEventHandler::new::(project_id, cx, |state, _| { + let mut style = *theme.private_button.style_for(state, false); + if is_becoming_public { + style.color = theme.disabled_button.color; + } + render_icon_button(&style, "icons/lock-8.svg") + .aligned() + .constrained() + .with_width(host_avatar_height) + .boxed() + }); + + if !is_becoming_public { + button = button + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| { + cx.dispatch_action(ToggleProjectPublic { + project: Some(project.clone()), + }) + }); + } + + button.boxed() + }) .with_child( Label::new(worktree_root_names, row.name.text.clone()) .aligned() @@ -596,7 +604,7 @@ impl ContactsPanel { row.add_children([ MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { - &theme.disabled_contact_button + &theme.disabled_button } else { &theme.contact_button.style_for(mouse_state, false) }; @@ -618,7 +626,7 @@ impl ContactsPanel { .boxed(), MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { - &theme.disabled_contact_button + &theme.disabled_button } else { &theme.contact_button.style_for(mouse_state, false) }; @@ -640,7 +648,7 @@ impl ContactsPanel { row.add_child( MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { - &theme.disabled_contact_button + &theme.disabled_button } else { &theme.contact_button.style_for(mouse_state, false) }; @@ -1153,6 +1161,7 @@ mod tests { test::{FakeHttpClient, FakeServer}, Client, }; + use collections::HashSet; use gpui::{serde_json::json, TestAppContext}; use language::LanguageRegistry; use project::{FakeFs, Project}; @@ -1182,12 +1191,14 @@ mod tests { cx, ) }); - project + let worktree_id = project .update(cx, |project, cx| { project.find_or_create_local_worktree("/private_dir", true, cx) }) .await - .unwrap(); + .unwrap() + .0 + .read_with(cx, |worktree, _| worktree.id().to_proto()); let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx)); let panel = cx.add_view(0, |cx| { @@ -1199,6 +1210,18 @@ mod tests { ) }); + workspace.update(cx, |_, cx| { + cx.observe(&panel, |_, panel, cx| { + let entries = render_to_strings(&panel, cx); + assert!( + entries.iter().collect::>().len() == entries.len(), + "Duplicate contact panel entries {:?}", + entries + ) + }) + .detach(); + }); + let get_users_request = server.receive::().await.unwrap(); server .respond( @@ -1278,9 +1301,196 @@ mod tests { cx.foreground().run_until_parked(); assert_eq!( - render_to_strings(&panel, cx), + cx.read(|cx| render_to_strings(&panel, cx)), + &[ + "v Requests", + " incoming user_one", + " outgoing user_two", + "v Online", + " the_current_user", + " dir3", + " 🔒 private_dir", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + // Make a project public. It appears as loading, since the project + // isn't yet visible to other contacts. + project.update(cx, |project, cx| project.set_public(true, cx)); + cx.foreground().run_until_parked(); + assert_eq!( + cx.read(|cx| render_to_strings(&panel, cx)), + &[ + "v Requests", + " incoming user_one", + " outgoing user_two", + "v Online", + " the_current_user", + " dir3", + " 🔒 private_dir (becoming public...)", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + // The server responds, assigning the project a remote id. It still appears + // as loading, because the server hasn't yet sent out the updated contact + // state for the current user. + let request = server.receive::().await.unwrap(); + server + .respond( + request.receipt(), + proto::RegisterProjectResponse { project_id: 200 }, + ) + .await; + cx.foreground().run_until_parked(); + assert_eq!( + cx.read(|cx| render_to_strings(&panel, cx)), + &[ + "v Requests", + " incoming user_one", + " outgoing user_two", + "v Online", + " the_current_user", + " dir3", + " 🔒 private_dir (becoming public...)", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + // The server receives the project's metadata and updates the contact metadata + // for the current user. Now the project appears as public. + assert_eq!( + server + .receive::() + .await + .unwrap() + .payload + .worktrees, + &[proto::WorktreeMetadata { + id: worktree_id, + root_name: "private_dir".to_string(), + visible: true, + }], + ); + server.send(proto::UpdateContacts { + contacts: vec![proto::Contact { + user_id: current_user_id, + online: true, + should_notify: false, + projects: vec![ + proto::ProjectMetadata { + id: 103, + worktree_root_names: vec!["dir3".to_string()], + guests: vec![3], + }, + proto::ProjectMetadata { + id: 200, + worktree_root_names: vec!["private_dir".to_string()], + guests: vec![3], + }, + ], + }], + ..Default::default() + }); + cx.foreground().run_until_parked(); + assert_eq!( + cx.read(|cx| render_to_strings(&panel, cx)), + &[ + "v Requests", + " incoming user_one", + " outgoing user_two", + "v Online", + " the_current_user", + " dir3", + " private_dir", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + // Make the project private. It appears as loading. + project.update(cx, |project, cx| project.set_public(false, cx)); + cx.foreground().run_until_parked(); + assert_eq!( + cx.read(|cx| render_to_strings(&panel, cx)), + &[ + "v Requests", + " incoming user_one", + " outgoing user_two", + "v Online", + " the_current_user", + " dir3", + " private_dir (becoming private...)", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + // The server receives the unregister request and updates the contact + // metadata for the current user. The project is now private. + let request = server.receive::().await.unwrap(); + server.send(proto::UpdateContacts { + contacts: vec![proto::Contact { + user_id: current_user_id, + online: true, + should_notify: false, + projects: vec![proto::ProjectMetadata { + id: 103, + worktree_root_names: vec!["dir3".to_string()], + guests: vec![3], + }], + }], + ..Default::default() + }); + cx.foreground().run_until_parked(); + assert_eq!( + cx.read(|cx| render_to_strings(&panel, cx)), + &[ + "v Requests", + " incoming user_one", + " outgoing user_two", + "v Online", + " the_current_user", + " dir3", + " 🔒 private_dir", + " user_four", + " dir2", + " user_three", + " dir1", + "v Offline", + " user_five", + ] + ); + + // The server responds to the unregister request. + server.respond(request.receipt(), proto::Ack {}).await; + cx.foreground().run_until_parked(); + assert_eq!( + cx.read(|cx| render_to_strings(&panel, cx)), &[ - "+", "v Requests", " incoming user_one", " outgoing user_two", @@ -1304,9 +1514,8 @@ mod tests { }); cx.foreground().run_until_parked(); assert_eq!( - render_to_strings(&panel, cx), + cx.read(|cx| render_to_strings(&panel, cx)), &[ - "+", "v Online", " user_four <=== selected", " dir2", @@ -1319,9 +1528,8 @@ mod tests { panel.select_next(&Default::default(), cx); }); assert_eq!( - render_to_strings(&panel, cx), + cx.read(|cx| render_to_strings(&panel, cx)), &[ - "+", "v Online", " user_four", " dir2 <=== selected", @@ -1334,9 +1542,8 @@ mod tests { panel.select_next(&Default::default(), cx); }); assert_eq!( - render_to_strings(&panel, cx), + cx.read(|cx| render_to_strings(&panel, cx)), &[ - "+", "v Online", " user_four", " dir2", @@ -1346,56 +1553,65 @@ mod tests { ); } - fn render_to_strings(panel: &ViewHandle, cx: &TestAppContext) -> Vec { - panel.read_with(cx, |panel, _| { - let mut entries = Vec::new(); - entries.push("+".to_string()); - entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| { - let mut string = match entry { - ContactEntry::Header(name) => { - let icon = if panel.collapsed_sections.contains(name) { - ">" - } else { - "v" - }; - format!("{} {:?}", icon, name) - } - ContactEntry::IncomingRequest(user) => { - format!(" incoming {}", user.github_login) - } - ContactEntry::OutgoingRequest(user) => { - format!(" outgoing {}", user.github_login) - } - ContactEntry::Contact(contact) => { - format!(" {}", contact.user.github_login) - } - ContactEntry::ContactProject(contact, project_ix, _) => { - format!( - " {}", - contact.projects[*project_ix].worktree_root_names.join(", ") - ) - } - ContactEntry::PrivateProject(project) => cx.read(|cx| { - format!( - " 🔒 {}", - project - .upgrade(cx) - .unwrap() - .read(cx) - .worktree_root_names(cx) - .collect::>() - .join(", ") - ) - }), - }; - - if panel.selection == Some(ix) { - string.push_str(" <=== selected"); + fn render_to_strings(panel: &ViewHandle, cx: &AppContext) -> Vec { + let panel = panel.read(cx); + let mut entries = Vec::new(); + entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| { + let mut string = match entry { + ContactEntry::Header(name) => { + let icon = if panel.collapsed_sections.contains(name) { + ">" + } else { + "v" + }; + format!("{} {:?}", icon, name) } + ContactEntry::IncomingRequest(user) => { + format!(" incoming {}", user.github_login) + } + ContactEntry::OutgoingRequest(user) => { + format!(" outgoing {}", user.github_login) + } + ContactEntry::Contact(contact) => { + format!(" {}", contact.user.github_login) + } + ContactEntry::ContactProject(contact, project_ix, project) => { + let project = project + .and_then(|p| p.upgrade(cx)) + .map(|project| project.read(cx)); + format!( + " {}{}", + contact.projects[*project_ix].worktree_root_names.join(", "), + if project.map_or(true, |project| project.is_public()) { + "" + } else { + " (becoming private...)" + }, + ) + } + ContactEntry::PrivateProject(project) => { + let project = project.upgrade(cx).unwrap().read(cx); + format!( + " 🔒 {}{}", + project + .worktree_root_names(cx) + .collect::>() + .join(", "), + if project.is_public() { + " (becoming public...)" + } else { + "" + }, + ) + } + }; - string - })); - entries - }) + if panel.selection == Some(ix) { + string.push_str(" <=== selected"); + } + + string + })); + entries } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 85b6660021..a9f7b59a31 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -746,8 +746,13 @@ impl Project { cx.notify(); self.project_store.update(cx, |_, cx| cx.notify()); - if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { - if let Some(project_id) = *remote_id_rx.borrow() { + if let ProjectClientState::Local { + remote_id_rx, + public_rx, + .. + } = &self.client_state + { + if let (Some(project_id), true) = (*remote_id_rx.borrow(), *public_rx.borrow()) { self.client .send(proto::UpdateProject { project_id, @@ -3707,7 +3712,6 @@ impl Project { project.shared_remote_id() }); - // Because sharing is async, we may have *unshared* the project by the time it completes. if let Some(project_id) = project_id { worktree .update(&mut cx, |worktree, cx| { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 52b5e8df36..f469423921 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -279,7 +279,7 @@ pub struct ContactsPanel { pub contact_username: ContainedText, pub contact_button: Interactive, pub contact_button_spacing: f32, - pub disabled_contact_button: IconButton, + pub disabled_button: IconButton, pub tree_branch: Interactive, pub private_button: Interactive, pub section_icon_size: f32, diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index cf08e770ab..2bfa4e94e2 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -124,7 +124,7 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 100, "hovered"), }, }, - disabledContactButton: { + disabledButton: { ...contactButton, background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), From 724affc442ed9fec707c8fe19400fcae15777d2f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Jun 2022 17:24:57 -0700 Subject: [PATCH 10/22] Upgrade deps to avoid multiple versions of transitive deps * env_logger * prost-build * bindgen --- Cargo.lock | 98 +++++++++++++++---------------- crates/collab/Cargo.toml | 2 +- crates/command_palette/Cargo.toml | 2 +- crates/editor/Cargo.toml | 2 +- crates/file_finder/Cargo.toml | 2 +- crates/gpui/Cargo.toml | 6 +- crates/language/Cargo.toml | 2 +- crates/lsp/Cargo.toml | 2 +- crates/picker/Cargo.toml | 2 +- crates/rpc/Cargo.toml | 2 +- crates/sum_tree/Cargo.toml | 2 +- crates/text/Cargo.toml | 2 +- crates/zed/Cargo.toml | 4 +- 13 files changed, 61 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 148b446d7c..9e3da4e673 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,9 +485,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.58.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" dependencies = [ "bitflags", "cexpr", @@ -503,7 +503,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "which 3.1.1", + "which", ] [[package]] @@ -639,11 +639,11 @@ dependencies = [ [[package]] name = "cexpr" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 5.1.2", + "nom 7.1.1", ] [[package]] @@ -1445,9 +1445,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ "atty", "humantime", @@ -1540,9 +1540,9 @@ dependencies = [ [[package]] name = "fixedbitset" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" [[package]] name = "flate2" @@ -1988,12 +1988,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" - [[package]] name = "hashbrown" version = "0.11.2" @@ -2009,7 +2003,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown 0.11.2", + "hashbrown", ] [[package]] @@ -2241,12 +2235,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.2" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg 1.0.1", - "hashbrown 0.9.1", + "hashbrown", ] [[package]] @@ -2482,9 +2476,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.119" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libloading" @@ -2722,6 +2716,12 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.3.7" @@ -2848,16 +2848,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "nom" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" -dependencies = [ - "memchr", - "version_check", -] - [[package]] name = "nom" version = "6.1.2" @@ -2871,6 +2861,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -3191,9 +3191,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "petgraph" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset", "indexmap", @@ -3474,20 +3474,22 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes", "heck 0.3.3", "itertools", + "lazy_static", "log", "multimap", "petgraph", - "prost 0.8.0", + "prost 0.9.0", "prost-types", + "regex", "tempfile", - "which 4.1.0", + "which", ] [[package]] @@ -3518,12 +3520,12 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ "bytes", - "prost 0.8.0", + "prost 0.9.0", ] [[package]] @@ -5812,20 +5814,12 @@ dependencies = [ [[package]] name = "which" -version = "3.1.1" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" -dependencies = [ - "libc", -] - -[[package]] -name = "which" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" dependencies = [ "either", + "lazy_static", "libc", ] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a2a3a377ab..a3c9e689ec 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -65,7 +65,7 @@ settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" util = { path = "../util" } lazy_static = "1.4" serde_json = { version = "1.0", features = ["preserve_order"] } diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 52fd8bbdc7..25260f4390 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -25,4 +25,4 @@ project = { path = "../project", features = ["test-support"] } serde_json = { version = "1.0", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index bb487d5d2c..741dd93f33 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -59,7 +59,7 @@ project = { path = "../project", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" rand = "0.8" unindent = "0.1.7" tree-sitter = "0.20" diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 554cf433a2..6fd792940d 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -25,4 +25,4 @@ gpui = { path = "../gpui", features = ["test-support"] } serde_json = { version = "1.0", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 7c7f6f257b..6caddfb26b 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -20,7 +20,7 @@ async-task = "4.0.3" backtrace = { version = "0.3", optional = true } ctor = "0.1" dhat = { version = "0.3", optional = true } -env_logger = { version = "0.8", optional = true } +env_logger = { version = "0.9", optional = true } etagere = "0.2" futures = "0.3" image = "0.23" @@ -47,14 +47,14 @@ usvg = "0.14" waker-fn = "1.1.0" [build-dependencies] -bindgen = "0.58.1" +bindgen = "0.59.2" cc = "1.0.67" [dev-dependencies] backtrace = "0.3" collections = { path = "../collections", features = ["test-support"] } dhat = "0.3" -env_logger = "0.8" +env_logger = "0.9" png = "0.16" simplelog = "0.9" diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index e105e25225..eed1297cea 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -57,7 +57,7 @@ lsp = { path = "../lsp", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" rand = "0.8.3" tree-sitter-json = "*" tree-sitter-rust = "*" diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 1c663e6b7e..94780a9472 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -30,5 +30,5 @@ gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" unindent = "0.1.7" diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index c74b6927ae..bceb263e23 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -21,4 +21,4 @@ gpui = { path = "../gpui", features = ["test-support"] } serde_json = { version = "1.0", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 1fac205280..a6b146e1cc 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -31,7 +31,7 @@ tracing = { version = "0.1.34", features = ["log"] } zstd = "0.9" [build-dependencies] -prost-build = "0.8" +prost-build = "0.9" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/sum_tree/Cargo.toml b/crates/sum_tree/Cargo.toml index b430f2e6b0..02cad4fb9d 100644 --- a/crates/sum_tree/Cargo.toml +++ b/crates/sum_tree/Cargo.toml @@ -13,5 +13,5 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } [dev-dependencies] ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" rand = "0.8.3" diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index a7209a7507..5f4bb4715d 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -28,5 +28,5 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor = "0.1" -env_logger = "0.8" +env_logger = "0.9" rand = "0.8.3" diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a9a266a6b7..a8006483e1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -59,7 +59,7 @@ chrono = "0.4" ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" -env_logger = "0.8" +env_logger = "0.9" futures = "0.3" http-auth-basic = "0.1.3" ignore = "0.4" @@ -107,7 +107,7 @@ client = { path = "../client", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } -env_logger = "0.8" +env_logger = "0.9" serde_json = { version = "1.0", features = ["preserve_order"] } unindent = "0.1.7" From f7e7a7c6a7a97e94ce3b21d9a9e1c13e21bb1323 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Jun 2022 17:28:28 -0700 Subject: [PATCH 11/22] Use rocksdb to store project paths' public/private state --- Cargo.lock | 45 +++++- crates/collab/src/integration_tests.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 2 +- crates/project/Cargo.toml | 1 + crates/project/src/db.rs | 161 ++++++++++++++++++++ crates/project/src/project.rs | 73 ++++++++- crates/workspace/src/workspace.rs | 53 +++++-- crates/zed/src/main.rs | 15 +- 8 files changed, 324 insertions(+), 28 deletions(-) create mode 100644 crates/project/src/db.rs diff --git a/Cargo.lock b/Cargo.lock index 9e3da4e673..37a14513cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cache-padded" version = "1.1.1" @@ -2506,6 +2517,21 @@ dependencies = [ "libc", ] +[[package]] +name = "librocksdb-sys" +version = "0.6.1+6.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc587013734dadb7cf23468e531aa120788b87243648be42e2d3a072186291" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "zstd-sys", +] + [[package]] name = "libz-sys" version = "1.1.3" @@ -3395,6 +3421,7 @@ dependencies = [ "postage", "rand 0.8.3", "regex", + "rocksdb", "rpc", "serde", "serde_json", @@ -3713,9 +3740,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -3733,9 +3760,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -3822,6 +3849,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rocksdb" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620f4129485ff1a7128d184bc687470c21c7951b64779ebc9cfdad3dcd920290" +dependencies = [ + "libc", + "librocksdb-sys", +] + [[package]] name = "roxmltree" version = "0.14.1" diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 96ed7714c2..a9e319681f 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4604,7 +4604,7 @@ impl TestServer { }); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 9be9906f8f..e2a7971704 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1175,7 +1175,7 @@ mod tests { let http_client = FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let server = FakeServer::for_client(current_user_id, &client, &cx).await; let fs = FakeFs::new(cx.background()); fs.insert_tree("/private_dir", json!({ "one.rs": "" })) diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 921eb9ddc5..1fb989ee9c 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -47,6 +47,7 @@ similar = "1.3" smol = "1.2.5" thiserror = "1.0.29" toml = "0.5" +rocksdb = "0.18" [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/project/src/db.rs b/crates/project/src/db.rs new file mode 100644 index 0000000000..bc125e4303 --- /dev/null +++ b/crates/project/src/db.rs @@ -0,0 +1,161 @@ +use anyhow::Result; +use std::path::PathBuf; +use std::sync::Arc; + +pub struct Db(DbStore); + +enum DbStore { + Null, + Real(rocksdb::DB), + + #[cfg(any(test, feature = "test-support"))] + Fake { + data: parking_lot::Mutex, Vec>>, + }, +} + +impl Db { + /// Open or create a database at the given file path. + pub fn open(path: PathBuf) -> Result> { + let db = rocksdb::DB::open_default(&path)?; + Ok(Arc::new(Self(DbStore::Real(db)))) + } + + /// Open a null database that stores no data, for use as a fallback + /// when there is an error opening the real database. + pub fn null() -> Arc { + Arc::new(Self(DbStore::Null)) + } + + /// Open a fake database for testing. + #[cfg(any(test, feature = "test-support"))] + pub fn open_fake() -> Arc { + Arc::new(Self(DbStore::Fake { + data: Default::default(), + })) + } + + pub fn read(&self, keys: I) -> Result>>> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + match &self.0 { + DbStore::Real(db) => db + .multi_get(keys) + .into_iter() + .map(|e| e.map_err(Into::into)) + .collect(), + + DbStore::Null => Ok(keys.into_iter().map(|_| None).collect()), + + #[cfg(any(test, feature = "test-support"))] + DbStore::Fake { data: db } => { + let db = db.lock(); + Ok(keys + .into_iter() + .map(|key| db.get(key.as_ref()).cloned()) + .collect()) + } + } + } + + pub fn delete(&self, keys: I) -> Result<()> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + match &self.0 { + DbStore::Real(db) => { + let mut batch = rocksdb::WriteBatch::default(); + for key in keys { + batch.delete(key); + } + db.write(batch)?; + } + + DbStore::Null => {} + + #[cfg(any(test, feature = "test-support"))] + DbStore::Fake { data: db } => { + let mut db = db.lock(); + for key in keys { + db.remove(key.as_ref()); + } + } + } + Ok(()) + } + + pub fn write(&self, entries: I) -> Result<()> + where + K: AsRef<[u8]>, + V: AsRef<[u8]>, + I: IntoIterator, + { + match &self.0 { + DbStore::Real(db) => { + let mut batch = rocksdb::WriteBatch::default(); + for (key, value) in entries { + batch.put(key, value); + } + db.write(batch)?; + } + + DbStore::Null => {} + + #[cfg(any(test, feature = "test-support"))] + DbStore::Fake { data: db } => { + let mut db = db.lock(); + for (key, value) in entries { + db.insert(key.as_ref().into(), value.as_ref().into()); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + + #[gpui::test] + fn test_db() { + let dir = TempDir::new("db-test").unwrap(); + let fake_db = Db::open_fake(); + let real_db = Db::open(dir.path().join("test.db")).unwrap(); + + for db in [&real_db, &fake_db] { + assert_eq!( + db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[None, None, None] + ); + + db.write([("key-1", "one"), ("key-3", "three")]).unwrap(); + assert_eq!( + db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[ + Some("one".as_bytes().to_vec()), + None, + Some("three".as_bytes().to_vec()) + ] + ); + + db.delete(["key-3", "key-4"]).unwrap(); + assert_eq!( + db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[Some("one".as_bytes().to_vec()), None, None,] + ); + } + + drop(real_db); + + let real_db = Db::open(dir.path().join("test.db")).unwrap(); + assert_eq!( + real_db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[Some("one".as_bytes().to_vec()), None, None,] + ); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a9f7b59a31..a000b32492 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,3 +1,4 @@ +mod db; pub mod fs; mod ignore; mod lsp_command; @@ -53,6 +54,7 @@ use std::{ use thiserror::Error; use util::{post_inc, ResultExt, TryFutureExt as _}; +pub use db::Db; pub use fs::*; pub use worktree::*; @@ -60,8 +62,8 @@ pub trait Item: Entity { fn entry_id(&self, cx: &AppContext) -> Option; } -#[derive(Default)] pub struct ProjectStore { + db: Arc, projects: Vec>, } @@ -533,7 +535,7 @@ impl Project { let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake())); let project = cx.update(|cx| { Project::local(true, client, user_store, project_store, languages, fs, cx) }); @@ -568,6 +570,10 @@ impl Project { self.user_store.clone() } + pub fn project_store(&self) -> ModelHandle { + self.project_store.clone() + } + #[cfg(any(test, feature = "test-support"))] pub fn check_invariants(&self, cx: &AppContext) { if self.is_local() { @@ -743,9 +749,6 @@ impl Project { } fn metadata_changed(&mut self, cx: &mut ModelContext) { - cx.notify(); - self.project_store.update(cx, |_, cx| cx.notify()); - if let ProjectClientState::Local { remote_id_rx, public_rx, @@ -768,6 +771,9 @@ impl Project { }) .log_err(); } + + self.project_store.update(cx, |_, cx| cx.notify()); + cx.notify(); } } @@ -5215,6 +5221,13 @@ impl Project { } impl ProjectStore { + pub fn new(db: Arc) -> Self { + Self { + db, + projects: Default::default(), + } + } + pub fn projects<'a>( &'a self, cx: &'a AppContext, @@ -5248,6 +5261,56 @@ impl ProjectStore { cx.notify(); } } + + pub fn are_all_project_paths_public( + &self, + project: &Project, + cx: &AppContext, + ) -> Task> { + let project_path_keys = self.project_path_keys(project, cx); + let db = self.db.clone(); + cx.background().spawn(async move { + let values = db.read(project_path_keys)?; + Ok(values.into_iter().all(|e| e.is_some())) + }) + } + + pub fn set_project_paths_public( + &self, + project: &Project, + public: bool, + cx: &AppContext, + ) -> Task> { + let project_path_keys = self.project_path_keys(project, cx); + let db = self.db.clone(); + cx.background().spawn(async move { + if public { + db.write(project_path_keys.into_iter().map(|key| (key, &[]))) + } else { + db.delete(project_path_keys) + } + }) + } + + fn project_path_keys(&self, project: &Project, cx: &AppContext) -> Vec { + project + .worktrees + .iter() + .filter_map(|worktree| { + worktree.upgrade(&cx).map(|worktree| { + format!( + "public-project-path:{}", + worktree + .read(cx) + .as_local() + .unwrap() + .abs_path() + .to_string_lossy() + ) + }) + }) + .collect::>() + } } impl WorktreeHandle { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f6b8c5db09..6cb3d36e10 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -692,7 +692,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); Arc::new(Self { @@ -1055,8 +1055,15 @@ impl Workspace { .clone() .unwrap_or_else(|| self.project.clone()); project.update(cx, |project, cx| { - let is_public = project.is_public(); - project.set_public(!is_public, cx); + let public = !project.is_public(); + eprintln!("toggle_project_public => {}", public); + project.set_public(public, cx); + project.project_store().update(cx, |store, cx| { + store + .set_project_paths_public(project, public, cx) + .detach_and_log_err(cx); + cx.notify(); + }); }); } @@ -2407,6 +2414,7 @@ pub fn open_paths( let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); cx.spawn(|mut cx| async move { + let mut new_project = None; let workspace = if let Some(existing) = existing { existing } else { @@ -2416,18 +2424,17 @@ pub fn open_paths( .contains(&false); cx.add_window((app_state.build_window_options)(), |cx| { - let mut workspace = Workspace::new( - Project::local( - false, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.project_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ), + let project = Project::local( + false, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.project_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), cx, ); + new_project = Some(project.clone()); + let mut workspace = Workspace::new(project, cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); if contains_directory { workspace.toggle_sidebar_item( @@ -2446,6 +2453,26 @@ pub fn open_paths( let items = workspace .update(&mut cx, |workspace, cx| workspace.open_paths(abs_paths, cx)) .await; + + if let Some(project) = new_project { + let public = project + .read_with(&cx, |project, cx| { + app_state + .project_store + .read(cx) + .are_all_project_paths_public(project, cx) + }) + .await + .log_err() + .unwrap_or(false); + if public { + project.update(&mut cx, |project, cx| { + eprintln!("initialize new project public"); + project.set_public(true, cx); + }); + } + } + (workspace, items) }) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 1427343e4b..dbe797c9fb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -48,9 +48,10 @@ use zed::{ fn main() { let http = http::client(); - let logs_dir_path = dirs::home_dir() - .expect("could not find home dir") - .join("Library/Logs/Zed"); + let home_dir = dirs::home_dir().expect("could not find home dir"); + let db_dir_path = home_dir.join("Library/Application Support/Zed"); + let logs_dir_path = home_dir.join("Library/Logs/Zed"); + fs::create_dir_all(&db_dir_path).expect("could not create database path"); fs::create_dir_all(&logs_dir_path).expect("could not create logs path"); init_logger(&logs_dir_path); @@ -59,6 +60,11 @@ fn main() { .or_else(|| app.platform().app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); init_panic_hook(logs_dir_path, app_version, http.clone(), app.background()); + let db = app.background().spawn(async move { + project::Db::open(db_dir_path.join("zed.db")) + .log_err() + .unwrap_or(project::Db::null()) + }); load_embedded_fonts(&app); @@ -136,7 +142,6 @@ fn main() { let client = client::Client::new(http.clone()); let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); context_menu::init(cx); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); @@ -156,6 +161,7 @@ fn main() { search::init(cx); vim::init(cx); + let db = cx.background().block(db); let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); let mut settings_rx = settings_from_files( default_settings, @@ -191,6 +197,7 @@ fn main() { .detach(); cx.set_global(settings); + let project_store = cx.add_model(|_| ProjectStore::new(db)); let app_state = Arc::new(AppState { languages, themes, From db97dcd76f74580af3ec5f210c3b37dcd517b5ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Jun 2022 17:41:21 -0700 Subject: [PATCH 12/22] Don't update contacts when a project is first registered Until the host has sent an UpdateProject message to populate the project's metadata, there is no reason to update contacts. --- crates/collab/src/rpc.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c19538f55c..a3eb64034b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -476,15 +476,15 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let user_id; let project_id; { let mut state = self.store_mut().await; - user_id = state.user_id_for_connection(request.sender_id)?; + let user_id = state.user_id_for_connection(request.sender_id)?; project_id = state.register_project(request.sender_id, user_id); }; + response.send(proto::RegisterProjectResponse { project_id })?; - self.update_user_contacts(user_id).await?; + Ok(()) } @@ -528,8 +528,13 @@ impl Server { } } + // Send out the `UpdateContacts` message before responding to the unregister + // request. This way, when the project's host can keep track of the project's + // remote id until after they've received the `UpdateContacts` message for + // themself. self.update_user_contacts(user_id).await?; response.send(proto::Ack {})?; + Ok(()) } From 36a4d31b5b477269dd47b7643b8b0830287e1a1d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Jun 2022 18:04:54 -0700 Subject: [PATCH 13/22] Keep unregistered projects' ids until pending contact updates are done --- crates/client/src/user.rs | 15 +++++++++++++++ crates/project/src/project.rs | 13 +++++++++++++ 2 files changed, 28 insertions(+) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index b8a320f524..9ac4c9a0ed 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -99,6 +99,7 @@ impl Entity for UserStore { enum UpdateContacts { Update(proto::UpdateContacts), + Wait(postage::barrier::Sender), Clear(postage::barrier::Sender), } @@ -217,6 +218,10 @@ impl UserStore { cx: &mut ModelContext, ) -> Task> { match message { + UpdateContacts::Wait(barrier) => { + drop(barrier); + Task::ready(Ok(())) + } UpdateContacts::Clear(barrier) => { self.contacts.clear(); self.incoming_contact_requests.clear(); @@ -497,6 +502,16 @@ impl UserStore { } } + pub fn contact_updates_done(&mut self) -> impl Future { + let (tx, mut rx) = postage::barrier::channel(); + self.update_contacts_tx + .unbounded_send(UpdateContacts::Wait(tx)) + .unwrap(); + async move { + rx.recv().await; + } + } + pub fn get_users( &mut self, mut user_ids: Vec, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a000b32492..c16cb8422e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -654,6 +654,19 @@ impl Project { }); return cx.spawn(|this, mut cx| async move { let response = request.await; + + // Unregistering the project causes the server to send out a + // contact update removing this project from the host's list + // of public projects. Wait until this contact update has been + // processed before clearing out this project's remote id, so + // that there is no moment where this project appears in the + // contact metadata and *also* has no remote id. + this.update(&mut cx, |this, cx| { + this.user_store() + .update(cx, |store, _| store.contact_updates_done()) + }) + .await; + this.update(&mut cx, |this, cx| { if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state From 98b54763b9419a8008f80c08eabddcb4f68bbd5b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Jun 2022 18:06:29 -0700 Subject: [PATCH 14/22] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 9512a43043..b3bc95b1cd 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 21; +pub const PROTOCOL_VERSION: u32 = 22; From 6a3a3a1124e9fb81c2ae7e1115f8e00f3381bd1c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 10:36:29 -0700 Subject: [PATCH 15/22] Add tooltip to the toggle public button in the contacts panel Co-authored-by: Antonio Scandurra --- crates/contacts_panel/src/contacts_panel.rs | 80 ++++++++++++++------- crates/workspace/src/workspace.rs | 2 - 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index e2a7971704..b8faa53cc9 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -179,19 +179,24 @@ impl ContactsPanel { let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id); let is_selected = this.selection == Some(ix); match &this.entries[ix] { ContactEntry::Header(section) => { let is_collapsed = this.collapsed_sections.contains(§ion); - Self::render_header(*section, theme, is_selected, is_collapsed, cx) + Self::render_header( + *section, + &theme.contacts_panel, + is_selected, + is_collapsed, + cx, + ) } ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), - theme, + &theme.contacts_panel, true, is_selected, cx, @@ -199,13 +204,13 @@ impl ContactsPanel { ContactEntry::OutgoingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), - theme, + &theme.contacts_panel, false, is_selected, cx, ), ContactEntry::Contact(contact) => { - Self::render_contact(&contact.user, theme, is_selected) + Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) } ContactEntry::ContactProject(contact, project_ix, open_project) => { let is_last_project_for_contact = @@ -221,15 +226,20 @@ impl ContactsPanel { current_user_id, *project_ix, open_project.clone(), - theme, + &theme.contacts_panel, + &theme.tooltip, is_last_project_for_contact, is_selected, cx, ) } - ContactEntry::PrivateProject(project) => { - Self::render_private_project(project.clone(), theme, is_selected, cx) - } + ContactEntry::PrivateProject(project) => Self::render_private_project( + project.clone(), + &theme.contacts_panel, + &theme.tooltip, + is_selected, + cx, + ), } }); @@ -335,6 +345,7 @@ impl ContactsPanel { project_index: usize, open_project: Option>, theme: &theme::ContactsPanel, + tooltip_style: &TooltipStyle, is_last_project: bool, is_selected: bool, cx: &mut RenderContext, @@ -406,7 +417,7 @@ impl ContactsPanel { return None; } - let mut button = MouseEventHandler::new::( + let button = MouseEventHandler::new::( project_id as usize, cx, |state, _| { @@ -423,17 +434,27 @@ impl ContactsPanel { }, ); - if !is_becoming_private { - button = button - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { - project: Some(open_project.clone()), + if is_becoming_private { + Some(button.boxed()) + } else { + Some( + button + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| { + cx.dispatch_action(ToggleProjectPublic { + project: Some(open_project.clone()), + }) }) - }); + .with_tooltip( + project_id as usize, + "Make project private".to_string(), + None, + tooltip_style.clone(), + cx, + ) + .boxed(), + ) } - - Some(button.boxed()) })) .constrained() .with_width(host_avatar_height) @@ -487,6 +508,7 @@ impl ContactsPanel { fn render_private_project( project: WeakModelHandle, theme: &theme::ContactsPanel, + tooltip_style: &TooltipStyle, is_selected: bool, cx: &mut RenderContext, ) -> ElementBox { @@ -520,7 +542,7 @@ impl ContactsPanel { Flex::row() .with_child({ - let mut button = + let button = MouseEventHandler::new::(project_id, cx, |state, _| { let mut style = *theme.private_button.style_for(state, false); if is_becoming_public { @@ -533,17 +555,25 @@ impl ContactsPanel { .boxed() }); - if !is_becoming_public { - button = button + if is_becoming_public { + button.boxed() + } else { + button .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| { cx.dispatch_action(ToggleProjectPublic { project: Some(project.clone()), }) - }); + }) + .with_tooltip( + project_id, + "Make project public".to_string(), + None, + tooltip_style.clone(), + cx, + ) + .boxed() } - - button.boxed() }) .with_child( Label::new(worktree_root_names, row.name.text.clone()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6cb3d36e10..b101c8d94c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1056,7 +1056,6 @@ impl Workspace { .unwrap_or_else(|| self.project.clone()); project.update(cx, |project, cx| { let public = !project.is_public(); - eprintln!("toggle_project_public => {}", public); project.set_public(public, cx); project.project_store().update(cx, |store, cx| { store @@ -2467,7 +2466,6 @@ pub fn open_paths( .unwrap_or(false); if public { project.update(&mut cx, |project, cx| { - eprintln!("initialize new project public"); project.set_public(true, cx); }); } From afdd38605758b0596c8da46992cd3d59aa6c7ab2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 11:52:10 -0700 Subject: [PATCH 16/22] Move persistence and restoration logic from workspace into project Co-authored-by: Antonio Scandurra --- crates/project/src/project.rs | 136 +++++++++++++++++------------- crates/workspace/src/workspace.rs | 23 +---- 2 files changed, 81 insertions(+), 78 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c16cb8422e..9cada8358a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -552,6 +552,50 @@ impl Project { project } + pub fn restore_state(&mut self, cx: &mut ModelContext) -> Task> { + if self.is_remote() { + return Task::ready(Ok(())); + } + + let db = self.project_store.read(cx).db.clone(); + let project_path_keys = self.project_path_keys(cx); + let should_be_public = cx.background().spawn(async move { + let values = db.read(project_path_keys)?; + anyhow::Ok(values.into_iter().all(|e| e.is_some())) + }); + cx.spawn(|this, mut cx| async move { + let public = should_be_public.await.log_err().unwrap_or(false); + this.update(&mut cx, |this, cx| { + if let ProjectClientState::Local { public_tx, .. } = &mut this.client_state { + let mut public_tx = public_tx.borrow_mut(); + if *public_tx != public { + *public_tx = public; + drop(public_tx); + this.metadata_changed(false, cx); + } + } + }); + Ok(()) + }) + } + + fn persist_state(&mut self, cx: &mut ModelContext) -> Task> { + if self.is_remote() { + return Task::ready(Ok(())); + } + + let db = self.project_store.read(cx).db.clone(); + let project_path_keys = self.project_path_keys(cx); + let public = self.is_public(); + cx.background().spawn(async move { + if public { + db.write(project_path_keys.into_iter().map(|key| (key, &[]))) + } else { + db.delete(project_path_keys) + } + }) + } + pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { self.opened_buffers .get(&remote_id) @@ -633,8 +677,12 @@ impl Project { pub fn set_public(&mut self, is_public: bool, cx: &mut ModelContext) { if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { - *public_tx.borrow_mut() = is_public; - self.metadata_changed(cx); + let mut public_tx = public_tx.borrow_mut(); + if *public_tx != is_public { + *public_tx = is_public; + drop(public_tx); + self.metadata_changed(true, cx); + } } } @@ -674,7 +722,7 @@ impl Project { *remote_id_tx.borrow_mut() = None; } this.subscriptions.clear(); - this.metadata_changed(cx); + this.metadata_changed(false, cx); }); response.map(drop) }); @@ -698,7 +746,7 @@ impl Project { *remote_id_tx.borrow_mut() = Some(remote_id); } - this.metadata_changed(cx); + this.metadata_changed(false, cx); cx.emit(Event::RemoteIdChanged(Some(remote_id))); this.subscriptions .push(this.client.add_model_for_remote_entity(remote_id, cx)); @@ -761,7 +809,7 @@ impl Project { } } - fn metadata_changed(&mut self, cx: &mut ModelContext) { + fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext) { if let ProjectClientState::Local { remote_id_rx, public_rx, @@ -786,6 +834,9 @@ impl Project { } self.project_store.update(cx, |_, cx| cx.notify()); + if persist { + self.persist_state(cx).detach_and_log_err(cx); + } cx.notify(); } } @@ -823,6 +874,25 @@ impl Project { .map(|tree| tree.read(cx).root_name()) } + fn project_path_keys(&self, cx: &AppContext) -> Vec { + self.worktrees + .iter() + .filter_map(|worktree| { + worktree.upgrade(&cx).map(|worktree| { + format!( + "public-project-path:{}", + worktree + .read(cx) + .as_local() + .unwrap() + .abs_path() + .to_string_lossy() + ) + }) + }) + .collect::>() + } + pub fn worktree_for_id( &self, id: WorktreeId, @@ -3769,7 +3839,7 @@ impl Project { false } }); - self.metadata_changed(cx); + self.metadata_changed(true, cx); cx.notify(); } @@ -3799,7 +3869,7 @@ impl Project { self.worktrees .push(WorktreeHandle::Weak(worktree.downgrade())); } - self.metadata_changed(cx); + self.metadata_changed(true, cx); cx.emit(Event::WorktreeAdded); cx.notify(); } @@ -4122,7 +4192,7 @@ impl Project { } } - this.metadata_changed(cx); + this.metadata_changed(true, cx); for (id, _) in old_worktrees_by_id { cx.emit(Event::WorktreeRemoved(id)); } @@ -5274,56 +5344,6 @@ impl ProjectStore { cx.notify(); } } - - pub fn are_all_project_paths_public( - &self, - project: &Project, - cx: &AppContext, - ) -> Task> { - let project_path_keys = self.project_path_keys(project, cx); - let db = self.db.clone(); - cx.background().spawn(async move { - let values = db.read(project_path_keys)?; - Ok(values.into_iter().all(|e| e.is_some())) - }) - } - - pub fn set_project_paths_public( - &self, - project: &Project, - public: bool, - cx: &AppContext, - ) -> Task> { - let project_path_keys = self.project_path_keys(project, cx); - let db = self.db.clone(); - cx.background().spawn(async move { - if public { - db.write(project_path_keys.into_iter().map(|key| (key, &[]))) - } else { - db.delete(project_path_keys) - } - }) - } - - fn project_path_keys(&self, project: &Project, cx: &AppContext) -> Vec { - project - .worktrees - .iter() - .filter_map(|worktree| { - worktree.upgrade(&cx).map(|worktree| { - format!( - "public-project-path:{}", - worktree - .read(cx) - .as_local() - .unwrap() - .abs_path() - .to_string_lossy() - ) - }) - }) - .collect::>() - } } impl WorktreeHandle { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b101c8d94c..9a4ab8c703 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1057,12 +1057,6 @@ impl Workspace { project.update(cx, |project, cx| { let public = !project.is_public(); project.set_public(public, cx); - project.project_store().update(cx, |store, cx| { - store - .set_project_paths_public(project, public, cx) - .detach_and_log_err(cx); - cx.notify(); - }); }); } @@ -2454,21 +2448,10 @@ pub fn open_paths( .await; if let Some(project) = new_project { - let public = project - .read_with(&cx, |project, cx| { - app_state - .project_store - .read(cx) - .are_all_project_paths_public(project, cx) - }) + project + .update(&mut cx, |project, cx| project.restore_state(cx)) .await - .log_err() - .unwrap_or(false); - if public { - project.update(&mut cx, |project, cx| { - project.set_public(true, cx); - }); - } + .log_err(); } (workspace, items) From 8bd4a0ab819986dca1e42ae037c07ebe83c405e0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 12:28:45 -0700 Subject: [PATCH 17/22] Don't store Project on TestClient in integration tests --- crates/collab/src/integration_tests.rs | 242 +++++++++++-------------- 1 file changed, 102 insertions(+), 140 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a9e319681f..e77af135bf 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -70,8 +70,8 @@ async fn test_share_project( cx_a.foreground().forbid_parking(); let (window_b, _) = cx_b.add_window(|_| EmptyView); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -193,10 +193,7 @@ async fn test_share_project( }); // Dropping client B's first project removes only that from client A's collaborators. - cx_b.update(move |_| { - drop(client_b.project.take()); - drop(project_b); - }); + cx_b.update(move |_| drop(project_b)); deterministic.run_until_parked(); project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 1); @@ -214,8 +211,8 @@ async fn test_unshare_project( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -241,10 +238,7 @@ async fn test_unshare_project( .unwrap(); // When client B leaves the project, it gets automatically unshared. - cx_b.update(|_| { - drop(client_b.project.take()); - drop(project_b); - }); + cx_b.update(|_| drop(project_b)); deterministic.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); @@ -257,10 +251,7 @@ async fn test_unshare_project( .unwrap(); // When client A (the host) leaves, the project gets unshared and guests are notified. - cx_a.update(|_| { - drop(project_a); - client_a.project.take(); - }); + cx_a.update(|_| drop(project_a)); deterministic.run_until_parked(); project_b2.read_with(cx_b, |project, _| { assert!(project.is_read_only()); @@ -277,8 +268,8 @@ async fn test_host_disconnect( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").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; server .make_contacts(vec![ @@ -360,7 +351,7 @@ async fn test_decline_join_request( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; + let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) @@ -408,10 +399,7 @@ async fn test_decline_join_request( // Close the project on the host deterministic.run_until_parked(); - cx_a.update(|_| { - drop(project_a); - client_a.project.take(); - }); + cx_a.update(|_| drop(project_a)); deterministic.run_until_parked(); assert!(matches!( project_b.await.unwrap_err(), @@ -427,7 +415,7 @@ async fn test_cancel_join_request( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; + let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) @@ -499,7 +487,7 @@ async fn test_private_projects( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; + let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) @@ -522,7 +510,6 @@ async fn test_private_projects( cx, ) }); - client_a.project = Some(project_a.clone()); // Private projects are not registered with the server. deterministic.run_until_parked(); @@ -564,9 +551,9 @@ async fn test_propagate_saves_and_fs_changes( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; - let mut client_c = server.create_client(cx_c, "user_c").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; server .make_contacts(vec![ (&client_a, cx_a), @@ -712,8 +699,8 @@ async fn test_fs_operations( // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -973,8 +960,8 @@ async fn test_fs_operations( async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1022,8 +1009,8 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1070,8 +1057,8 @@ async fn test_editing_while_guest_opens_buffer( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1117,8 +1104,8 @@ async fn test_leaving_worktree_while_opening_buffer( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1144,10 +1131,7 @@ async fn test_leaving_worktree_while_opening_buffer( let buffer_b = cx_b .background() .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); - cx_b.update(|_| { - drop(client_b.project.take()); - drop(project_b); - }); + cx_b.update(|_| drop(project_b)); drop(buffer_b); // See that the guest has left. @@ -1160,8 +1144,8 @@ async fn test_leaving_worktree_while_opening_buffer( async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1216,9 +1200,9 @@ async fn test_collaborating_with_diagnostics( ) { deterministic.forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; - let mut client_c = server.create_client(cx_c, "user_c").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; server .make_contacts(vec![ (&client_a, cx_a), @@ -1456,8 +1440,8 @@ async fn test_collaborating_with_diagnostics( async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1623,8 +1607,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1709,8 +1693,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1775,8 +1759,8 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1883,8 +1867,8 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1981,8 +1965,8 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2057,8 +2041,8 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2153,8 +2137,8 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2258,8 +2242,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2329,8 +2313,8 @@ async fn test_collaborating_with_code_actions( cx_a.foreground().forbid_parking(); cx_b.update(|cx| editor::init(cx)); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2533,8 +2517,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T cx_a.foreground().forbid_parking(); cx_b.update(|cx| editor::init(cx)); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3125,8 +3109,8 @@ async fn test_contacts( ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").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; server .make_contacts(vec![ @@ -3218,7 +3202,6 @@ async fn test_contacts( }) .await; - client_a.project.take(); cx_a.update(move |_| drop(project_a)); deterministic.run_until_parked(); for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { @@ -3503,8 +3486,8 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { // 2 clients connect to a server. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3712,8 +3695,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // 2 clients connect to a server. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3852,8 +3835,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont // 2 clients connect to a server. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let mut client_a = server.create_client(cx_a, "user_a").await; - let mut client_b = server.create_client(cx_b, "user_b").await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4238,15 +4221,12 @@ async fn test_random_collaboration( let mut clients = futures::future::join_all(clients).await; cx.foreground().run_until_parked(); - let (host, mut host_cx, host_err) = clients.remove(0); + let (host, host_project, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { log::error!("host error - {:?}", host_err); } - host.project - .as_ref() - .unwrap() - .read_with(&host_cx, |project, _| assert!(!project.is_shared())); - for (guest, mut guest_cx, guest_err) in clients { + host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared())); + for (guest, guest_project, mut guest_cx, guest_err) in clients { if let Some(guest_err) = guest_err { log::error!("{} error - {:?}", guest.username, guest_err); } @@ -4267,14 +4247,10 @@ async fn test_random_collaboration( .iter() .flat_map(|contact| &contact.projects) .any(|project| project.id == host_project_id)); - guest - .project - .as_ref() - .unwrap() - .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - guest_cx.update(|_| drop(guest)); + guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only())); + guest_cx.update(|_| drop((guest, guest_project))); } - host_cx.update(|_| drop(host)); + host_cx.update(|_| drop((host, host_project))); return; } @@ -4330,17 +4306,13 @@ async fn test_random_collaboration( server.forbid_connections(); server.disconnect_client(removed_guest_id); cx.foreground().advance_clock(RECEIVE_TIMEOUT); - let (guest, mut guest_cx, guest_err) = guest.await; + let (guest, guest_project, mut guest_cx, guest_err) = guest.await; server.allow_connections(); if let Some(guest_err) = guest_err { log::error!("{} error - {:?}", guest.username, guest_err); } - guest - .project - .as_ref() - .unwrap() - .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); + guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only())); for user_id in &user_ids { let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap(); let contacts = server @@ -4369,7 +4341,7 @@ async fn test_random_collaboration( log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); - guest_cx.update(|_| drop(guest)); + guest_cx.update(|_| drop((guest, guest_project))); operations += 1; } @@ -4394,11 +4366,10 @@ async fn test_random_collaboration( let mut clients = futures::future::join_all(clients).await; cx.foreground().run_until_parked(); - let (host_client, mut host_cx, host_err) = clients.remove(0); + let (host_client, host_project, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { panic!("host error - {:?}", host_err); } - let host_project = host_client.project.as_ref().unwrap(); let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| { project .worktrees(cx) @@ -4409,30 +4380,21 @@ async fn test_random_collaboration( .collect::>() }); - host_client - .project - .as_ref() - .unwrap() - .read_with(&host_cx, |project, cx| project.check_invariants(cx)); + host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx)); - for (guest_client, mut guest_cx, guest_err) in clients.into_iter() { + for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() { if let Some(guest_err) = guest_err { panic!("{} error - {:?}", guest_client.username, guest_err); } - let worktree_snapshots = - guest_client - .project - .as_ref() - .unwrap() - .read_with(&guest_cx, |project, cx| { - project - .worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.snapshot()) - }) - .collect::>() - }); + let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| { + project + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + (worktree.id(), worktree.snapshot()) + }) + .collect::>() + }); assert_eq!( worktree_snapshots.keys().collect::>(), @@ -4459,11 +4421,7 @@ async fn test_random_collaboration( assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id()); } - guest_client - .project - .as_ref() - .unwrap() - .read_with(&guest_cx, |project, cx| project.check_invariants(cx)); + guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx)); for guest_buffer in &guest_client.buffers { let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id()); @@ -4494,10 +4452,10 @@ async fn test_random_collaboration( ); } - guest_cx.update(|_| drop(guest_client)); + guest_cx.update(|_| drop((guest_project, guest_client))); } - host_cx.update(|_| drop(host_client)); + host_cx.update(|_| drop((host_client, host_project))); } struct TestServer { @@ -4633,7 +4591,6 @@ impl TestServer { user_store, project_store, language_registry: Arc::new(LanguageRegistry::test()), - project: Default::default(), buffers: Default::default(), }; client.wait_for_current_user(cx).await; @@ -4727,7 +4684,6 @@ struct TestClient { pub user_store: ModelHandle, pub project_store: ModelHandle, language_registry: Arc, - project: Option>, buffers: HashSet>, } @@ -4787,7 +4743,7 @@ impl TestClient { } async fn build_local_project( - &mut self, + &self, fs: Arc, root_path: impl AsRef, cx: &mut TestAppContext, @@ -4803,7 +4759,6 @@ impl TestClient { cx, ) }); - self.project = Some(project.clone()); let (worktree, _) = project .update(cx, |p, cx| { p.find_or_create_local_worktree(root_path, true, cx) @@ -4820,7 +4775,7 @@ impl TestClient { } async fn build_remote_project( - &mut self, + &self, host_project: &ModelHandle, host_cx: &mut TestAppContext, guest_cx: &mut TestAppContext, @@ -4846,7 +4801,6 @@ impl TestClient { project.respond_to_join_request(guest_user_id, true, cx) }); let project = project_b.await.unwrap(); - self.project = Some(project.clone()); project } @@ -4865,7 +4819,12 @@ impl TestClient { op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, - ) -> (Self, TestAppContext, Option) { + ) -> ( + Self, + ModelHandle, + TestAppContext, + Option, + ) { async fn simulate_host_internal( client: &mut TestClient, project: ModelHandle, @@ -4999,8 +4958,7 @@ impl TestClient { let result = simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await; log::info!("Host done"); - self.project = Some(project); - (self, cx, result.err()) + (self, project, cx, result.err()) } pub async fn simulate_guest( @@ -5010,7 +4968,12 @@ impl TestClient { op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, - ) -> (Self, TestAppContext, Option) { + ) -> ( + Self, + ModelHandle, + TestAppContext, + Option, + ) { async fn simulate_guest_internal( client: &mut TestClient, guest_username: &str, @@ -5325,8 +5288,7 @@ impl TestClient { .await; log::info!("{}: done", guest_username); - self.project = Some(project); - (self, cx, result.err()) + (self, project, cx, result.err()) } } From b2aa8310179dd8de9544dc9c176ea6b211116aca Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 12:55:09 -0700 Subject: [PATCH 18/22] Store a FakeFs on TestClient --- crates/collab/src/integration_tests.rs | 677 ++++++++++++------------- 1 file changed, 325 insertions(+), 352 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index e77af135bf..e9c593754b 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -76,22 +76,23 @@ async fn test_share_project( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - ".gitignore": "ignored-dir", - "a.txt": "a-contents", - "b.txt": "b-contents", - "ignored-dir": { - "c.txt": "", - "d.txt": "", - } - }), - ) - .await; + client_a + .fs + .insert_tree( + "/a", + json!({ + ".gitignore": "ignored-dir", + "a.txt": "a-contents", + "b.txt": "b-contents", + "ignored-dir": { + "c.txt": "", + "d.txt": "", + } + }), + ) + .await; - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); // Join that project as client B @@ -217,17 +218,18 @@ async fn test_unshare_project( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); @@ -279,17 +281,18 @@ async fn test_host_disconnect( ]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); @@ -357,10 +360,9 @@ async fn test_decline_join_request( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree("/a", json!({})).await; + client_a.fs.insert_tree("/a", json!({})).await; - let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); // Request to join that project as client B @@ -421,10 +423,8 @@ async fn test_cancel_join_request( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree("/a", json!({})).await; - - let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a.fs.insert_tree("/a", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); let user_b = client_a @@ -562,17 +562,17 @@ async fn test_propagate_saves_and_fs_changes( ]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "file1": "", - "file2": "" - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "file1": "", + "file2": "" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap()); // Join that worktree as clients B and C. @@ -622,7 +622,7 @@ async fn test_propagate_saves_and_fs_changes( buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx)); save_b.await.unwrap(); assert_eq!( - fs.load("/a/file1".as_ref()).await.unwrap(), + client_a.fs.load("/a/file1".as_ref()).await.unwrap(), "hi-a, i-am-c, i-am-b, i-am-a" ); buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty())); @@ -632,18 +632,22 @@ async fn test_propagate_saves_and_fs_changes( worktree_a.flush_fs_events(cx_a).await; // Make changes on host's file system, see those changes on guest worktrees. - fs.rename( - "/a/file1".as_ref(), - "/a/file1-renamed".as_ref(), - Default::default(), - ) - .await - .unwrap(); - - fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) + client_a + .fs + .rename( + "/a/file1".as_ref(), + "/a/file1-renamed".as_ref(), + Default::default(), + ) .await .unwrap(); - fs.insert_file(Path::new("/a/file4"), "4".into()).await; + + client_a + .fs + .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) + .await + .unwrap(); + client_a.fs.insert_file("/a/file4", "4".into()).await; worktree_a .condition(&cx_a, |tree, _| { @@ -695,9 +699,6 @@ async fn test_fs_operations( cx_b: &mut TestAppContext, ) { executor.forbid_parking(); - let fs = FakeFs::new(cx_a.background()); - - // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -705,17 +706,17 @@ async fn test_fs_operations( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - // Share a project as client A - fs.insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + client_a + .fs + .insert_tree( + "/dir", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); @@ -966,16 +967,16 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + client_a + .fs + .insert_tree( + "/dir", + json!({ + "a.txt": "a-contents", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client B @@ -1015,16 +1016,16 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/dir", cx_a).await; + client_a + .fs + .insert_tree( + "/dir", + json!({ + "a.txt": "a-contents", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client B @@ -1037,7 +1038,9 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont assert!(!buf.has_conflict()); }); - fs.save(Path::new("/dir/a.txt"), &"new contents".into()) + client_a + .fs + .save("/dir/a.txt".as_ref(), &"new contents".into()) .await .unwrap(); buffer_b @@ -1045,9 +1048,7 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont buf.text() == "new contents" && !buf.is_dirty() }) .await; - buffer_b.read_with(cx_b, |buf, _| { - assert!(!buf.has_conflict()); - }); + buffer_b.read_with(cx_b, |buf, _| assert!(!buf.has_conflict())); } #[gpui::test(iterations = 10)] @@ -1063,16 +1064,11 @@ async fn test_editing_while_guest_opens_buffer( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + client_a + .fs + .insert_tree("/dir", json!({ "a.txt": "a-contents" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client A @@ -1110,16 +1106,11 @@ async fn test_leaving_worktree_while_opening_buffer( .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + client_a + .fs + .insert_tree("/dir", json!({ "a.txt": "a-contents" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // See that a guest has joined as client A. @@ -1150,17 +1141,17 @@ async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - - let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A sees that a guest has joined. @@ -1223,19 +1214,18 @@ async fn test_collaborating_with_diagnostics( let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - // Connect to a server as 2 clients. - // Share a project as client A - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "a.rs": "let one = two", - "other.rs": "", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "a.rs": "let one = two", + "other.rs": "", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; // Cause the language server to start. @@ -1467,17 +1457,17 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu }); client_a.language_registry.add(Arc::new(language)); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { a }", - "other.rs": "", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a }", + "other.rs": "", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a file in an editor as the guest. @@ -1613,16 +1603,11 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "a.rs": "let one = 1;", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + client_a + .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; let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await @@ -1646,7 +1631,9 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;") .await; - fs.save(Path::new("/a/a.rs"), &Rope::from("let seven = 7;")) + client_a + .fs + .save("/a/a.rs".as_ref(), &Rope::from("let seven = 7;")) .await .unwrap(); buffer_a @@ -1711,16 +1698,11 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "a.rs": "let one = two", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a + .fs + .insert_tree("/a", json!({ "a.rs": "let one = two" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b = cx_b @@ -1765,22 +1747,6 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/root-1", - json!({ - "a.rs": "const ONE: usize = b::TWO + b::THREE;", - }), - ) - .await; - fs.insert_tree( - "/root-2", - json!({ - "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;", - }), - ) - .await; - // Set up a fake language server. let mut language = Language::new( LanguageConfig { @@ -1793,7 +1759,21 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await; + client_a + .fs + .insert_tree( + "/root", + json!({ + "dir-1": { + "a.rs": "const ONE: usize = b::TWO + b::THREE;", + }, + "dir-2": { + "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;", + } + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. @@ -1808,7 +1788,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { fake_language_server.handle_request::(|_, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(), lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), ), ))) @@ -1837,7 +1817,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { fake_language_server.handle_request::(|_, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(), lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), ), ))) @@ -1873,23 +1853,6 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/root-1", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - }), - ) - .await; - fs.insert_tree( - "/root-2", - json!({ - "three.rs": "const THREE: usize = two::TWO + one::ONE;", - }), - ) - .await; - // Set up a fake language server. let mut language = Language::new( LanguageConfig { @@ -1902,7 +1865,22 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await; + client_a + .fs + .insert_tree( + "/root", + json!({ + "dir-1": { + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + }, + "dir-2": { + "three.rs": "const THREE: usize = two::TWO + one::ONE;", + } + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. @@ -1917,19 +1895,19 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { fake_language_server.handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), - "file:///root-1/one.rs" + "file:///root/dir-1/one.rs" ); Ok(Some(vec![ lsp::Location { - uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), + uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(), range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)), }, lsp::Location { - uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), + uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(), range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)), }, lsp::Location { - uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(), + uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(), range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)), }, ])) @@ -1971,29 +1949,27 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/root-1", - json!({ - "a": "hello world", - "b": "goodnight moon", - "c": "a world of goo", - "d": "world champion of clown world", - }), - ) - .await; - fs.insert_tree( - "/root-2", - json!({ - "e": "disney world is fun", - }), - ) - .await; - - let (project_a, _) = client_a.build_local_project(fs, "/root-1", cx_a).await; + client_a + .fs + .insert_tree( + "/root", + json!({ + "dir-1": { + "a": "hello world", + "b": "goodnight moon", + "c": "a world of goo", + "d": "world champion of clown world", + }, + "dir-2": { + "e": "disney world is fun", + } + }), + ) + .await; + let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await; let (worktree_2, _) = project_a .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/root-2", true, cx) + p.find_or_create_local_worktree("/root/dir-2", true, cx) }) .await .unwrap(); @@ -2029,10 +2005,10 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex assert_eq!( ranges_by_path, &[ - (PathBuf::from("root-1/a"), vec![6..11]), - (PathBuf::from("root-1/c"), vec![2..7]), - (PathBuf::from("root-1/d"), vec![0..5, 24..29]), - (PathBuf::from("root-2/e"), vec![7..12]), + (PathBuf::from("dir-1/a"), vec![6..11]), + (PathBuf::from("dir-1/c"), vec![2..7]), + (PathBuf::from("dir-1/d"), vec![0..5, 24..29]), + (PathBuf::from("dir-2/e"), vec![7..12]), ] ); } @@ -2047,14 +2023,15 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/root-1", - json!({ - "main.rs": "fn double(number: i32) -> i32 { number + number }", - }), - ) - .await; + client_a + .fs + .insert_tree( + "/root-1", + json!({ + "main.rs": "fn double(number: i32) -> i32 { number + number }", + }), + ) + .await; // Set up a fake language server. let mut language = Language::new( @@ -2068,7 +2045,7 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await; + let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. @@ -2155,26 +2132,24 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/code", - json!({ - "crate-1": { - "one.rs": "const ONE: usize = 1;", - }, - "crate-2": { - "two.rs": "const TWO: usize = 2; const THREE: usize = 3;", - }, - "private": { - "passwords.txt": "the-password", - } - }), - ) - .await; - - let (project_a, worktree_id) = client_a - .build_local_project(fs, "/code/crate-1", cx_a) + client_a + .fs + .insert_tree( + "/code", + json!({ + "crate-1": { + "one.rs": "const ONE: usize = 1;", + }, + "crate-2": { + "two.rs": "const TWO: usize = 2; const THREE: usize = 3;", + }, + "private": { + "passwords.txt": "the-password", + } + }), + ) .await; + let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Cause the language server to start. @@ -2260,17 +2235,17 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/root", - json!({ - "a.rs": "const ONE: usize = b::TWO;", - "b.rs": "const TWO: usize = 2", - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/root", cx_a).await; + client_a + .fs + .insert_tree( + "/root", + json!({ + "a.rs": "const ONE: usize = b::TWO;", + "b.rs": "const TWO: usize = 2", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b1 = cx_b @@ -2331,16 +2306,17 @@ async fn test_collaborating_with_code_actions( let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); client_a.language_registry.add(Arc::new(language)); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", - "other.rs": "pub fn foo() -> usize { 4 }", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", + "other.rs": "pub fn foo() -> usize { 4 }", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // Join the project as client B. let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; @@ -2544,17 +2520,17 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }); client_a.language_registry.add(Arc::new(language)); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;" - }), - ) - .await; - - let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; + client_a + .fs + .insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx)); @@ -3137,9 +3113,8 @@ async fn test_contacts( } // Share a project as client A. - let fs = FakeFs::new(cx_a.background()); - fs.create_dir(Path::new("/a")).await.unwrap(); - let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await; + client_a.fs.create_dir(Path::new("/a")).await.unwrap(); + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; deterministic.run_until_parked(); for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { @@ -3176,9 +3151,8 @@ async fn test_contacts( } // Add a local project as client B - let fs = FakeFs::new(cx_b.background()); - fs.create_dir(Path::new("/b")).await.unwrap(); - let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_b).await; + client_a.fs.create_dir("/b".as_ref()).await.unwrap(); + let (_project_b, _) = client_b.build_local_project("/b", cx_b).await; deterministic.run_until_parked(); for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { @@ -3482,9 +3456,6 @@ async fn test_contact_requests( #[gpui::test(iterations = 10)] async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let fs = FakeFs::new(cx_a.background()); - - // 2 clients connect to a server. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -3494,19 +3465,19 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.update(editor::init); cx_b.update(editor::init); - // Client A shares a project. - fs.insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - // Client B joins the project. let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. @@ -3691,9 +3662,6 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { #[gpui::test(iterations = 10)] async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let fs = FakeFs::new(cx_a.background()); - - // 2 clients connect to a server. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -3704,17 +3672,19 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T cx_b.update(editor::init); // Client A shares a project. - fs.insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - "4.txt": "four", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + "4.txt": "four", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // Client B joins the project. let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; @@ -3844,17 +3814,18 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont cx_b.update(editor::init); // Client A shares a project. - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + client_a + .fs + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. @@ -4561,6 +4532,7 @@ impl TestServer { }) }); + let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let app_state = Arc::new(workspace::AppState { @@ -4569,7 +4541,7 @@ impl TestServer { project_store: project_store.clone(), languages: Arc::new(LanguageRegistry::new(Task::ready(()))), themes: ThemeRegistry::new((), cx.font_cache()), - fs: FakeFs::new(cx.background()), + fs: fs.clone(), build_window_options: || Default::default(), initialize_workspace: |_, _, _| unimplemented!(), }); @@ -4590,6 +4562,7 @@ impl TestServer { username: name.to_string(), user_store, project_store, + fs, language_registry: Arc::new(LanguageRegistry::test()), buffers: Default::default(), }; @@ -4684,6 +4657,7 @@ struct TestClient { pub user_store: ModelHandle, pub project_store: ModelHandle, language_registry: Arc, + fs: Arc, buffers: HashSet>, } @@ -4744,7 +4718,6 @@ impl TestClient { async fn build_local_project( &self, - fs: Arc, root_path: impl AsRef, cx: &mut TestAppContext, ) -> (ModelHandle, WorktreeId) { @@ -4755,7 +4728,7 @@ impl TestClient { self.user_store.clone(), self.project_store.clone(), self.language_registry.clone(), - fs, + self.fs.clone(), cx, ) }); From e18bc2498999fdca5642731c0613828ffd66fb14 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 14:39:06 -0700 Subject: [PATCH 19/22] Rename project's 'public'/'private' flag to 'online'/'offline' --- crates/collab/src/integration_tests.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 40 ++++++------ crates/project/src/project.rs | 72 ++++++++++----------- crates/workspace/src/workspace.rs | 4 +- 4 files changed, 59 insertions(+), 59 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index e9c593754b..b83fe77019 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -519,7 +519,7 @@ async fn test_private_projects( .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); // The project is registered when it is made public. - project_a.update(cx_a, |project, cx| project.set_public(true, cx)); + project_a.update(cx_a, |project, cx| project.set_online(true, cx)); deterministic.run_until_parked(); assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); assert!(!client_b diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b8faa53cc9..2a4e9a6c46 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -45,7 +45,7 @@ enum ContactEntry { OutgoingRequest(Arc), Contact(Arc), ContactProject(Arc, usize, Option>), - PrivateProject(WeakModelHandle), + OfflineProject(WeakModelHandle), } #[derive(Clone)] @@ -233,7 +233,7 @@ impl ContactsPanel { cx, ) } - ContactEntry::PrivateProject(project) => Self::render_private_project( + ContactEntry::OfflineProject(project) => Self::render_offline_project( project.clone(), &theme.contacts_panel, &theme.tooltip, @@ -412,8 +412,8 @@ impl ContactsPanel { .boxed(), ) .with_children(open_project.and_then(|open_project| { - let is_becoming_private = !open_project.read(cx).is_public(); - if !mouse_state.hovered && !is_becoming_private { + let is_going_offline = !open_project.read(cx).is_online(); + if !mouse_state.hovered && !is_going_offline { return None; } @@ -425,7 +425,7 @@ impl ContactsPanel { *theme.private_button.style_for(state, false); icon_style.container.background_color = row.container.background_color; - if is_becoming_private { + if is_going_offline { icon_style.color = theme.disabled_button.color; } render_icon_button(&icon_style, "icons/lock-8.svg") @@ -434,7 +434,7 @@ impl ContactsPanel { }, ); - if is_becoming_private { + if is_going_offline { Some(button.boxed()) } else { Some( @@ -447,7 +447,7 @@ impl ContactsPanel { }) .with_tooltip( project_id as usize, - "Make project private".to_string(), + "Take project offline".to_string(), None, tooltip_style.clone(), cx, @@ -505,7 +505,7 @@ impl ContactsPanel { .boxed() } - fn render_private_project( + fn render_offline_project( project: WeakModelHandle, theme: &theme::ContactsPanel, tooltip_style: &TooltipStyle, @@ -532,7 +532,7 @@ impl ContactsPanel { let row = theme.project_row.style_for(state, is_selected); let mut worktree_root_names = String::new(); let project_ = project.read(cx); - let is_becoming_public = project_.is_public(); + let is_going_online = project_.is_online(); for tree in project_.visible_worktrees(cx) { if !worktree_root_names.is_empty() { worktree_root_names.push_str(", "); @@ -545,7 +545,7 @@ impl ContactsPanel { let button = MouseEventHandler::new::(project_id, cx, |state, _| { let mut style = *theme.private_button.style_for(state, false); - if is_becoming_public { + if is_going_online { style.color = theme.disabled_button.color; } render_icon_button(&style, "icons/lock-8.svg") @@ -555,7 +555,7 @@ impl ContactsPanel { .boxed() }); - if is_becoming_public { + if is_going_online { button.boxed() } else { button @@ -567,7 +567,7 @@ impl ContactsPanel { }) .with_tooltip( project_id, - "Make project public".to_string(), + "Take project online".to_string(), None, tooltip_style.clone(), cx, @@ -864,7 +864,7 @@ impl ContactsPanel { if project.read(cx).visible_worktrees(cx).next().is_none() { None } else { - Some(ContactEntry::PrivateProject(project.downgrade())) + Some(ContactEntry::OfflineProject(project.downgrade())) } }, )); @@ -1173,8 +1173,8 @@ impl PartialEq for ContactEntry { return contact_1.user.id == contact_2.user.id && ix_1 == ix_2; } } - ContactEntry::PrivateProject(project_1) => { - if let ContactEntry::PrivateProject(project_2) = other { + ContactEntry::OfflineProject(project_1) => { + if let ContactEntry::OfflineProject(project_2) = other { return project_1.id() == project_2.id(); } } @@ -1351,7 +1351,7 @@ mod tests { // Make a project public. It appears as loading, since the project // isn't yet visible to other contacts. - project.update(cx, |project, cx| project.set_public(true, cx)); + project.update(cx, |project, cx| project.set_online(true, cx)); cx.foreground().run_until_parked(); assert_eq!( cx.read(|cx| render_to_strings(&panel, cx)), @@ -1458,7 +1458,7 @@ mod tests { ); // Make the project private. It appears as loading. - project.update(cx, |project, cx| project.set_public(false, cx)); + project.update(cx, |project, cx| project.set_online(false, cx)); cx.foreground().run_until_parked(); assert_eq!( cx.read(|cx| render_to_strings(&panel, cx)), @@ -1612,14 +1612,14 @@ mod tests { format!( " {}{}", contact.projects[*project_ix].worktree_root_names.join(", "), - if project.map_or(true, |project| project.is_public()) { + if project.map_or(true, |project| project.is_online()) { "" } else { " (becoming private...)" }, ) } - ContactEntry::PrivateProject(project) => { + ContactEntry::OfflineProject(project) => { let project = project.upgrade(cx).unwrap().read(cx); format!( " 🔒 {}{}", @@ -1627,7 +1627,7 @@ mod tests { .worktree_root_names(cx) .collect::>() .join(", "), - if project.is_public() { + if project.is_online() { " (becoming public...)" } else { "" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9cada8358a..8a03bb9cdd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -129,8 +129,8 @@ enum ProjectClientState { is_shared: bool, remote_id_tx: watch::Sender>, remote_id_rx: watch::Receiver>, - public_tx: watch::Sender, - public_rx: watch::Receiver, + online_tx: watch::Sender, + online_rx: watch::Receiver, _maintain_remote_id_task: Task>, }, Remote { @@ -315,7 +315,7 @@ impl Project { } pub fn local( - public: bool, + online: bool, client: Arc, user_store: ModelHandle, project_store: ModelHandle, @@ -324,17 +324,17 @@ impl Project { cx: &mut MutableAppContext, ) -> ModelHandle { cx.add_model(|cx: &mut ModelContext| { - let (public_tx, public_rx) = watch::channel_with(public); + let (online_tx, online_rx) = watch::channel_with(online); let (remote_id_tx, remote_id_rx) = watch::channel(); let _maintain_remote_id_task = cx.spawn_weak({ let status_rx = client.clone().status(); - let public_rx = public_rx.clone(); + let online_rx = online_rx.clone(); move |this, mut cx| async move { let mut stream = Stream::map(status_rx.clone(), drop) - .merge(Stream::map(public_rx.clone(), drop)); + .merge(Stream::map(online_rx.clone(), drop)); while stream.recv().await.is_some() { let this = this.upgrade(&cx)?; - if status_rx.borrow().is_connected() && *public_rx.borrow() { + if status_rx.borrow().is_connected() && *online_rx.borrow() { this.update(&mut cx, |this, cx| this.register(cx)) .await .log_err()?; @@ -364,8 +364,8 @@ impl Project { is_shared: false, remote_id_tx, remote_id_rx, - public_tx, - public_rx, + online_tx, + online_rx, _maintain_remote_id_task, }, opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), @@ -558,19 +558,19 @@ impl Project { } let db = self.project_store.read(cx).db.clone(); - let project_path_keys = self.project_path_keys(cx); - let should_be_public = cx.background().spawn(async move { - let values = db.read(project_path_keys)?; + let keys = self.db_keys_for_online_state(cx); + let read_online = cx.background().spawn(async move { + let values = db.read(keys)?; anyhow::Ok(values.into_iter().all(|e| e.is_some())) }); cx.spawn(|this, mut cx| async move { - let public = should_be_public.await.log_err().unwrap_or(false); + let online = read_online.await.log_err().unwrap_or(false); this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { public_tx, .. } = &mut this.client_state { - let mut public_tx = public_tx.borrow_mut(); - if *public_tx != public { - *public_tx = public; - drop(public_tx); + if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state { + let mut online_tx = online_tx.borrow_mut(); + if *online_tx != online { + *online_tx = online; + drop(online_tx); this.metadata_changed(false, cx); } } @@ -585,13 +585,13 @@ impl Project { } let db = self.project_store.read(cx).db.clone(); - let project_path_keys = self.project_path_keys(cx); - let public = self.is_public(); + let keys = self.db_keys_for_online_state(cx); + let is_online = self.is_online(); cx.background().spawn(async move { - if public { - db.write(project_path_keys.into_iter().map(|key| (key, &[]))) + if is_online { + db.write(keys.into_iter().map(|key| (key, &[]))) } else { - db.delete(project_path_keys) + db.delete(keys) } }) } @@ -675,20 +675,20 @@ impl Project { &self.fs } - pub fn set_public(&mut self, is_public: bool, cx: &mut ModelContext) { - if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { - let mut public_tx = public_tx.borrow_mut(); - if *public_tx != is_public { - *public_tx = is_public; - drop(public_tx); + pub fn set_online(&mut self, online: bool, cx: &mut ModelContext) { + if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state { + let mut online_tx = online_tx.borrow_mut(); + if *online_tx != online { + *online_tx = online; + drop(online_tx); self.metadata_changed(true, cx); } } } - pub fn is_public(&self) -> bool { + pub fn is_online(&self) -> bool { match &self.client_state { - ProjectClientState::Local { public_rx, .. } => *public_rx.borrow(), + ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(), ProjectClientState::Remote { .. } => true, } } @@ -705,7 +705,7 @@ impl Project { // Unregistering the project causes the server to send out a // contact update removing this project from the host's list - // of public projects. Wait until this contact update has been + // of online projects. Wait until this contact update has been // processed before clearing out this project's remote id, so // that there is no moment where this project appears in the // contact metadata and *also* has no remote id. @@ -812,11 +812,11 @@ impl Project { fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext) { if let ProjectClientState::Local { remote_id_rx, - public_rx, + online_rx, .. } = &self.client_state { - if let (Some(project_id), true) = (*remote_id_rx.borrow(), *public_rx.borrow()) { + if let (Some(project_id), true) = (*remote_id_rx.borrow(), *online_rx.borrow()) { self.client .send(proto::UpdateProject { project_id, @@ -874,13 +874,13 @@ impl Project { .map(|tree| tree.read(cx).root_name()) } - fn project_path_keys(&self, cx: &AppContext) -> Vec { + fn db_keys_for_online_state(&self, cx: &AppContext) -> Vec { self.worktrees .iter() .filter_map(|worktree| { worktree.upgrade(&cx).map(|worktree| { format!( - "public-project-path:{}", + "project-path-online:{}", worktree .read(cx) .as_local() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9a4ab8c703..fe662b2354 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1055,8 +1055,8 @@ impl Workspace { .clone() .unwrap_or_else(|| self.project.clone()); project.update(cx, |project, cx| { - let public = !project.is_public(); - project.set_public(public, cx); + let public = !project.is_online(); + project.set_online(public, cx); }); } From 24aafde1e84a01cad11e1cec50045d82b62e91b0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 16:40:16 -0700 Subject: [PATCH 20/22] Avoid persisting project's state before it has been initialized --- crates/client/src/user.rs | 2 +- crates/collab/src/integration_tests.rs | 219 ++++++++++++++++++++++--- crates/collab/src/rpc/store.rs | 2 +- crates/project/src/project.rs | 6 +- 4 files changed, 202 insertions(+), 27 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 9ac4c9a0ed..803fcf3703 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -42,7 +42,7 @@ pub struct Contact { pub projects: Vec, } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ProjectMetadata { pub id: u64, pub worktree_root_names: Vec, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index b83fe77019..f03b16ce81 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -7,7 +7,7 @@ use ::rpc::Peer; use anyhow::anyhow; use client::{ self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, - Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, + Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT, }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ @@ -479,8 +479,8 @@ async fn test_cancel_join_request( ); } -#[gpui::test(iterations = 3)] -async fn test_private_projects( +#[gpui::test(iterations = 10)] +async fn test_offline_projects( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -489,58 +489,229 @@ async fn test_private_projects( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let user_a = UserId::from_proto(client_a.user_id().unwrap()); server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; - let user_a = UserId::from_proto(client_a.user_id().unwrap()); + // Set up observers of the project and user stores. Any time either of + // these models update, they should be in a consistent state with each + // other. There should not be an observable moment where the current + // user's contact entry contains a project that does not match one of + // the current open projects. That would cause a duplicate entry to be + // shown in the contacts panel. + let mut subscriptions = vec![]; + let (window_id, view) = cx_a.add_window(|cx| { + subscriptions.push(cx.observe(&client_a.user_store, { + let project_store = client_a.project_store.clone(); + let user_store = client_a.user_store.clone(); + move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx) + })); - let fs = FakeFs::new(cx_a.background()); - fs.insert_tree("/a", json!({})).await; + subscriptions.push(cx.observe(&client_a.project_store, { + let project_store = client_a.project_store.clone(); + let user_store = client_a.user_store.clone(); + move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx) + })); - // Create a private project - let project_a = cx_a.update(|cx| { + fn check_project_list( + project_store: ModelHandle, + user_store: ModelHandle, + cx: &mut gpui::MutableAppContext, + ) { + let open_project_ids = project_store + .read(cx) + .projects(cx) + .filter_map(|project| project.read(cx).remote_id()) + .collect::>(); + + let user_store = user_store.read(cx); + for contact in user_store.contacts() { + if contact.user.id == user_store.current_user().unwrap().id { + for project in &contact.projects { + if !open_project_ids.contains(&project.id) { + panic!( + concat!( + "current user's contact data has a project", + "that doesn't match any open project {:?}", + ), + project + ); + } + } + } + } + } + + EmptyView + }); + + // Build an offline project with two worktrees. + client_a + .fs + .insert_tree( + "/code", + json!({ + "crate1": { "a.rs": "" }, + "crate2": { "b.rs": "" }, + }), + ) + .await; + let project = cx_a.update(|cx| { Project::local( false, client_a.client.clone(), client_a.user_store.clone(), client_a.project_store.clone(), client_a.language_registry.clone(), - fs.clone(), + client_a.fs.clone(), cx, ) }); + project + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/code/crate1", true, cx) + }) + .await + .unwrap(); + project + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/code/crate2", true, cx) + }) + .await + .unwrap(); + project + .update(cx_a, |p, cx| p.restore_state(cx)) + .await + .unwrap(); - // Private projects are not registered with the server. + // When a project is offline, no information about it is sent to the server. deterministic.run_until_parked(); - assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_none())); + assert!(server + .store + .read() + .await + .project_metadata_for_user(user_a) + .is_empty()); + assert!(project.read_with(cx_a, |project, _| project.remote_id().is_none())); assert!(client_b .user_store .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); - // The project is registered when it is made public. - project_a.update(cx_a, |project, cx| project.set_online(true, cx)); + // When the project is taken online, its metadata is sent to the server + // and broadcasted to other users. + project.update(cx_a, |p, cx| p.set_online(true, cx)); deterministic.run_until_parked(); - assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); - assert!(!client_b - .user_store - .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); + let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap(); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + store.contacts()[0].projects, + &[ProjectMetadata { + id: project_id, + worktree_root_names: vec!["crate1".into(), "crate2".into()], + guests: Default::default(), + }] + ); + }); - // The project is registered again when it loses and regains connection. + // The project is registered again when the host loses and regains connection. server.disconnect_client(user_a); server.forbid_connections(); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); - // deterministic.run_until_parked(); - assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_none())); + assert!(server + .store + .read() + .await + .project_metadata_for_user(user_a) + .is_empty()); + assert!(project.read_with(cx_a, |p, _| p.remote_id().is_none())); assert!(client_b .user_store .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); + server.allow_connections(); cx_b.foreground().advance_clock(Duration::from_secs(10)); - assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); - assert!(!client_b - .user_store - .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); + let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap(); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + store.contacts()[0].projects, + &[ProjectMetadata { + id: project_id, + worktree_root_names: vec!["crate1".into(), "crate2".into()], + guests: Default::default(), + }] + ); + }); + + project + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/code/crate3", true, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + store.contacts()[0].projects, + &[ProjectMetadata { + id: project_id, + worktree_root_names: vec!["crate1".into(), "crate2".into(), "crate3".into()], + guests: Default::default(), + }] + ); + }); + + // Build another project using a directory which was previously part of + // an online project. Restore the project's state from the host's database. + let project2 = cx_a.update(|cx| { + Project::local( + false, + client_a.client.clone(), + client_a.user_store.clone(), + client_a.project_store.clone(), + client_a.language_registry.clone(), + client_a.fs.clone(), + cx, + ) + }); + project2 + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/code/crate3", true, cx) + }) + .await + .unwrap(); + project2 + .update(cx_a, |project, cx| project.restore_state(cx)) + .await + .unwrap(); + + // This project is now online, because its directory was previously online. + project2.read_with(cx_a, |project, _| assert!(project.is_online())); + deterministic.run_until_parked(); + let project2_id = project2.read_with(cx_a, |p, _| p.remote_id()).unwrap(); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + store.contacts()[0].projects, + &[ + ProjectMetadata { + id: project_id, + worktree_root_names: vec!["crate1".into(), "crate2".into(), "crate3".into()], + guests: Default::default(), + }, + ProjectMetadata { + id: project2_id, + worktree_root_names: vec!["crate3".into()], + guests: Default::default(), + } + ] + ); + }); + + cx_a.update(|cx| { + drop(subscriptions); + drop(view); + cx.remove_window(window_id); + }); } #[gpui::test(iterations = 10)] diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 03e529df3e..cad293876e 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -32,7 +32,7 @@ pub struct Project { #[serde(skip)] pub join_requests: HashMap>>, pub active_replica_ids: HashSet, - pub worktrees: HashMap, + pub worktrees: BTreeMap, pub language_servers: Vec, } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8a03bb9cdd..a449f303ef 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -99,6 +99,7 @@ pub struct Project { opened_buffers: HashMap, buffer_snapshots: HashMap>, nonce: u128, + initialized_persistent_state: bool, } #[derive(Error, Debug)] @@ -385,6 +386,7 @@ impl Project { language_server_settings: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), + initialized_persistent_state: false, } }) } @@ -497,6 +499,7 @@ impl Project { opened_buffers: Default::default(), buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), + initialized_persistent_state: false, }; for worktree in worktrees { this.add_worktree(&worktree, cx); @@ -566,6 +569,7 @@ impl Project { cx.spawn(|this, mut cx| async move { let online = read_online.await.log_err().unwrap_or(false); this.update(&mut cx, |this, cx| { + this.initialized_persistent_state = true; if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state { let mut online_tx = online_tx.borrow_mut(); if *online_tx != online { @@ -580,7 +584,7 @@ impl Project { } fn persist_state(&mut self, cx: &mut ModelContext) -> Task> { - if self.is_remote() { + if self.is_remote() || !self.initialized_persistent_state { return Task::ready(Ok(())); } From ed14fd6e0defee822b59d8f5fc36abda32ae7d81 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 16:57:50 -0700 Subject: [PATCH 21/22] Add setting to make projects online/offline by default --- crates/collab/src/integration_tests.rs | 3 ++- crates/project/src/project.rs | 32 +++++++++++++------------- crates/settings/src/settings.rs | 9 ++++++++ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index f03b16ce81..d335aabbe4 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4635,7 +4635,8 @@ impl TestServer { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { cx.update(|cx| { - let settings = Settings::test(cx); + let mut settings = Settings::test(cx); + settings.projects_online_by_default = false; cx.set_global(settings); }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a449f303ef..3e09436b4b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -562,9 +562,14 @@ impl Project { let db = self.project_store.read(cx).db.clone(); let keys = self.db_keys_for_online_state(cx); + let online_by_default = cx.global::().projects_online_by_default; let read_online = cx.background().spawn(async move { let values = db.read(keys)?; - anyhow::Ok(values.into_iter().all(|e| e.is_some())) + anyhow::Ok( + values + .into_iter() + .all(|e| e.map_or(online_by_default, |e| e == [true as u8])), + ) }); cx.spawn(|this, mut cx| async move { let online = read_online.await.log_err().unwrap_or(false); @@ -592,11 +597,8 @@ impl Project { let keys = self.db_keys_for_online_state(cx); let is_online = self.is_online(); cx.background().spawn(async move { - if is_online { - db.write(keys.into_iter().map(|key| (key, &[]))) - } else { - db.delete(keys) - } + let value = &[is_online as u8]; + db.write(keys.into_iter().map(|key| (key, value))) }) } @@ -882,17 +884,15 @@ impl Project { self.worktrees .iter() .filter_map(|worktree| { - worktree.upgrade(&cx).map(|worktree| { - format!( + let worktree = worktree.upgrade(&cx)?.read(cx); + if worktree.is_visible() { + Some(format!( "project-path-online:{}", - worktree - .read(cx) - .as_local() - .unwrap() - .abs_path() - .to_string_lossy() - ) - }) + worktree.as_local().unwrap().abs_path().to_string_lossy() + )) + } else { + None + } }) .collect::>() } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 58c70d32c1..0efedbfd9b 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -19,6 +19,7 @@ pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; #[derive(Clone)] pub struct Settings { + pub projects_online_by_default: bool, pub buffer_font_family: FamilyId, pub buffer_font_size: f32, pub default_buffer_font_size: f32, @@ -49,6 +50,8 @@ pub enum SoftWrap { #[derive(Clone, Debug, Default, Deserialize, JsonSchema)] pub struct SettingsFileContent { + #[serde(default)] + pub projects_online_by_default: Option, #[serde(default)] pub buffer_font_family: Option, #[serde(default)] @@ -81,6 +84,7 @@ impl Settings { preferred_line_length: 80, language_overrides: Default::default(), format_on_save: true, + projects_online_by_default: true, theme, }) } @@ -135,6 +139,7 @@ impl Settings { preferred_line_length: 80, format_on_save: true, language_overrides: Default::default(), + projects_online_by_default: true, theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()), } } @@ -164,6 +169,10 @@ impl Settings { } } + merge( + &mut self.projects_online_by_default, + data.projects_online_by_default, + ); merge(&mut self.buffer_font_size, data.buffer_font_size); merge(&mut self.default_buffer_font_size, data.buffer_font_size); merge(&mut self.vim_mode, data.vim_mode); From 41b7fd4a272660cafb0b6277cf672a2a87272f7a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Jun 2022 17:08:44 -0700 Subject: [PATCH 22/22] Rename a public/private to online/offline in a few more places --- crates/contacts_panel/src/contacts_panel.rs | 30 ++++++++++----------- crates/workspace/src/workspace.rs | 8 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 2a4e9a6c46..261a95c5ed 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -22,7 +22,7 @@ use serde::Deserialize; use settings::Settings; use std::{ops::DerefMut, sync::Arc}; use theme::IconButton; -use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectPublic, Workspace}; +use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace}; impl_actions!( contacts_panel, @@ -417,7 +417,7 @@ impl ContactsPanel { return None; } - let button = MouseEventHandler::new::( + let button = MouseEventHandler::new::( project_id as usize, cx, |state, _| { @@ -441,7 +441,7 @@ impl ContactsPanel { button .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { + cx.dispatch_action(ToggleProjectOnline { project: Some(open_project.clone()), }) }) @@ -525,7 +525,7 @@ impl ContactsPanel { .unwrap_or(0.); enum LocalProject {} - enum TogglePublic {} + enum ToggleOnline {} let project_id = project.id(); MouseEventHandler::new::(project_id, cx, |state, cx| { @@ -543,7 +543,7 @@ impl ContactsPanel { Flex::row() .with_child({ let button = - MouseEventHandler::new::(project_id, cx, |state, _| { + MouseEventHandler::new::(project_id, cx, |state, _| { let mut style = *theme.private_button.style_for(state, false); if is_going_online { style.color = theme.disabled_button.color; @@ -561,7 +561,7 @@ impl ContactsPanel { button .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { + cx.dispatch_action(ToggleProjectOnline { project: Some(project.clone()), }) }) @@ -1349,7 +1349,7 @@ mod tests { ] ); - // Make a project public. It appears as loading, since the project + // Take a project online. It appears as loading, since the project // isn't yet visible to other contacts. project.update(cx, |project, cx| project.set_online(true, cx)); cx.foreground().run_until_parked(); @@ -1362,7 +1362,7 @@ mod tests { "v Online", " the_current_user", " dir3", - " 🔒 private_dir (becoming public...)", + " 🔒 private_dir (going online...)", " user_four", " dir2", " user_three", @@ -1392,7 +1392,7 @@ mod tests { "v Online", " the_current_user", " dir3", - " 🔒 private_dir (becoming public...)", + " 🔒 private_dir (going online...)", " user_four", " dir2", " user_three", @@ -1403,7 +1403,7 @@ mod tests { ); // The server receives the project's metadata and updates the contact metadata - // for the current user. Now the project appears as public. + // for the current user. Now the project appears as online. assert_eq!( server .receive::() @@ -1457,7 +1457,7 @@ mod tests { ] ); - // Make the project private. It appears as loading. + // Take the project offline. It appears as loading. project.update(cx, |project, cx| project.set_online(false, cx)); cx.foreground().run_until_parked(); assert_eq!( @@ -1469,7 +1469,7 @@ mod tests { "v Online", " the_current_user", " dir3", - " private_dir (becoming private...)", + " private_dir (going offline...)", " user_four", " dir2", " user_three", @@ -1480,7 +1480,7 @@ mod tests { ); // The server receives the unregister request and updates the contact - // metadata for the current user. The project is now private. + // metadata for the current user. The project is now offline. let request = server.receive::().await.unwrap(); server.send(proto::UpdateContacts { contacts: vec![proto::Contact { @@ -1615,7 +1615,7 @@ mod tests { if project.map_or(true, |project| project.is_online()) { "" } else { - " (becoming private...)" + " (going offline...)" }, ) } @@ -1628,7 +1628,7 @@ mod tests { .collect::>() .join(", "), if project.is_online() { - " (becoming public...)" + " (going online...)" } else { "" }, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fe662b2354..20edeac20e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -100,7 +100,7 @@ pub struct OpenPaths { } #[derive(Clone, Deserialize)] -pub struct ToggleProjectPublic { +pub struct ToggleProjectOnline { #[serde(skip_deserializing)] pub project: Option>, } @@ -123,7 +123,7 @@ impl_internal_actions!( RemoveFolderFromProject ] ); -impl_actions!(workspace, [ToggleProjectPublic]); +impl_actions!(workspace, [ToggleProjectOnline]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -168,7 +168,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::add_folder_to_project); cx.add_action(Workspace::remove_folder_from_project); - cx.add_action(Workspace::toggle_project_public); + cx.add_action(Workspace::toggle_project_online); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -1049,7 +1049,7 @@ impl Workspace { .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx)); } - fn toggle_project_public(&mut self, action: &ToggleProjectPublic, cx: &mut ViewContext) { + fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext) { let project = action .project .clone()