From 71da81c74326f1f9763803a6d1cd776b48b58125 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 26 Sep 2024 12:03:57 -0700 Subject: [PATCH] SSH Remoting: Fix bugs in worktree syncing (#18406) Release Notes: - N/A --------- Co-authored-by: conrad --- crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/projects.rs | 2 +- .../collab/src/tests/channel_buffer_tests.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 30 ++++---- crates/collab/src/tests/following_tests.rs | 16 ++-- crates/collab/src/tests/integration_tests.rs | 74 +++++++++---------- .../remote_editing_collaboration_tests.rs | 25 ++++++- crates/collab/src/tests/test_server.rs | 2 +- crates/project/src/worktree_store.rs | 32 ++++---- .../remote_server/src/remote_editing_tests.rs | 42 +++++++++++ crates/worktree/src/worktree.rs | 7 +- script/zed-local | 16 ++-- 12 files changed, 157 insertions(+), 92 deletions(-) diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 1434bc07cf..9bf767329d 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -32,6 +32,7 @@ macro_rules! id_type { #[allow(unused)] #[allow(missing_docs)] pub fn from_proto(value: u64) -> Self { + debug_assert!(value != 0); Self(value as i32) } diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index b514d4bb03..8091c66205 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -285,7 +285,7 @@ impl Database { ) .one(&*tx) .await? - .ok_or_else(|| anyhow!("no such project"))?; + .ok_or_else(|| anyhow!("no such project: {project_id}"))?; // Update metadata. worktree::Entity::update(worktree::ActiveModel { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 1ba41c45bb..b5bfd0f03b 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -246,7 +246,7 @@ async fn test_channel_notes_participant_indices( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); // Clients A and B open the same file. diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index d2835edc61..f9bc21efb1 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -76,7 +76,7 @@ async fn test_host_disconnect( .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; cx_a.background_executor.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); @@ -192,7 +192,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a buffer as client A let buffer_a = project_a @@ -308,7 +308,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b @@ -565,7 +565,7 @@ async fn test_collaborating_with_code_actions( .unwrap(); // Join the project as client B. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { @@ -780,7 +780,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b @@ -1030,7 +1030,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes .await .unwrap(); executor.run_until_parked(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; @@ -1126,9 +1126,7 @@ async fn test_share_project( .await .unwrap(); let client_b_peer_id = client_b.peer_id().unwrap(); - let project_b = client_b - .build_dev_server_project(initial_project.id, cx_b) - .await; + let project_b = client_b.join_remote_project(initial_project.id, cx_b).await; let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); @@ -1230,9 +1228,7 @@ async fn test_share_project( .update(cx_c, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - let _project_c = client_c - .build_dev_server_project(initial_project.id, cx_c) - .await; + let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await; // Client B closes the editor, and client A sees client B's selections removed. cx_b.update(move |_| drop(editor_b)); @@ -1291,7 +1287,7 @@ async fn test_on_input_format_from_host_to_guest( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a file in an editor as the host. let buffer_a = project_a @@ -1411,7 +1407,7 @@ async fn test_on_input_format_from_guest_to_host( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b @@ -1574,7 +1570,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); // Client B joins the project - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -1836,7 +1832,7 @@ async fn test_inlay_hint_refresh_is_forwarded( .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -2050,7 +2046,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA .unwrap(); // Join the project as client B. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 9a39d6f3eb..5e9c001491 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -74,7 +74,7 @@ async fn test_basic_following( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -162,7 +162,7 @@ async fn test_basic_following( executor.run_until_parked(); let active_call_c = cx_c.read(ActiveCall::global); - let project_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_c = client_c.join_remote_project(project_id, cx_c).await; let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); active_call_c .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) @@ -175,7 +175,7 @@ async fn test_basic_following( cx_d.executor().run_until_parked(); let active_call_d = cx_d.read(ActiveCall::global); - let project_d = client_d.build_dev_server_project(project_id, cx_d).await; + let project_d = client_d.join_remote_project(project_id, cx_d).await; let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d); active_call_d .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) @@ -569,7 +569,7 @@ async fn test_following_tab_order( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -686,7 +686,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T .unwrap(); // Client B joins the project. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -1199,7 +1199,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -1335,7 +1335,7 @@ async fn test_peers_simultaneously_following_each_other( .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); executor.run_until_parked(); @@ -1685,7 +1685,7 @@ async fn test_following_into_excluded_file( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d5cef3589c..afc3e7cfb8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1372,7 +1372,7 @@ async fn test_unshare_project( .unwrap(); let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; executor.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); @@ -1392,7 +1392,7 @@ async fn test_unshare_project( assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected())); // Client C opens the project. - let project_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_c = client_c.join_remote_project(project_id, cx_c).await; // When client A unshares the project, client C's project becomes read-only. project_a @@ -1409,7 +1409,7 @@ async fn test_unshare_project( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await; + let project_c2 = client_c.join_remote_project(project_id, cx_c).await; executor.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); @@ -1514,9 +1514,9 @@ async fn test_project_reconnect( .await .unwrap(); - let project_b1 = client_b.build_dev_server_project(project1_id, cx_b).await; - let project_b2 = client_b.build_dev_server_project(project2_id, cx_b).await; - let project_b3 = client_b.build_dev_server_project(project3_id, cx_b).await; + let project_b1 = client_b.join_remote_project(project1_id, cx_b).await; + let project_b2 = client_b.join_remote_project(project2_id, cx_b).await; + let project_b3 = client_b.join_remote_project(project3_id, cx_b).await; executor.run_until_parked(); let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| { @@ -2310,8 +2310,8 @@ async fn test_propagate_saves_and_fs_changes( .unwrap(); // Join that worktree as clients B and C. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; - let project_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; + let project_c = client_c.join_remote_project(project_id, cx_c).await; let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); @@ -2535,7 +2535,7 @@ async fn test_git_diff_base_change( .await .unwrap(); - let project_remote = client_b.build_dev_server_project(project_id, cx_b).await; + let project_remote = client_b.join_remote_project(project_id, cx_b).await; let diff_base = " one @@ -2791,7 +2791,7 @@ async fn test_git_branch_name( .await .unwrap(); - let project_remote = client_b.build_dev_server_project(project_id, cx_b).await; + let project_remote = client_b.join_remote_project(project_id, cx_b).await; client_a .fs() .set_branch_name(Path::new("/dir/.git"), Some("branch-1")); @@ -2836,7 +2836,7 @@ async fn test_git_branch_name( assert_branch(Some("branch-2"), project, cx) }); - let project_remote_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_remote_c = client_c.join_remote_project(project_id, cx_c).await; executor.run_until_parked(); project_remote_c.read_with(cx_c, |project, cx| { @@ -2891,7 +2891,7 @@ async fn test_git_status_sync( .await .unwrap(); - let project_remote = client_b.build_dev_server_project(project_id, cx_b).await; + let project_remote = client_b.join_remote_project(project_id, cx_b).await; // Wait for it to catch up to the new status executor.run_until_parked(); @@ -2967,7 +2967,7 @@ async fn test_git_status_sync( }); // And synchronization while joining - let project_remote_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_remote_c = client_c.join_remote_project(project_id, cx_c).await; executor.run_until_parked(); project_remote_c.read_with(cx_c, |project, cx| { @@ -3015,7 +3015,7 @@ async fn test_fs_operations( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); @@ -3316,7 +3316,7 @@ async fn test_local_settings( executor.run_until_parked(); // As client B, join that project and observe the local settings. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); executor.run_until_parked(); @@ -3439,7 +3439,7 @@ async fn test_buffer_conflict_after_save( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a buffer as client B let buffer_b = project_b @@ -3503,7 +3503,7 @@ async fn test_buffer_reloading( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a buffer as client B let buffer_b = project_b @@ -3557,7 +3557,7 @@ async fn test_editing_while_guest_opens_buffer( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open a buffer as client A let buffer_a = project_a @@ -3605,7 +3605,7 @@ async fn test_leaving_worktree_while_opening_buffer( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // See that a guest has joined as client A. executor.run_until_parked(); @@ -3652,7 +3652,7 @@ async fn test_canceling_buffer_opening( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) @@ -3709,8 +3709,8 @@ async fn test_leaving_project( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b1 = client_b.build_dev_server_project(project_id, cx_b).await; - let project_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_b1 = client_b.join_remote_project(project_id, cx_b).await; + let project_c = client_c.join_remote_project(project_id, cx_c).await; // Client A sees that a guest has joined. executor.run_until_parked(); @@ -3751,7 +3751,7 @@ async fn test_leaving_project( }); // Client B re-joins the project and can open buffers as before. - let project_b2 = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b2 = client_b.join_remote_project(project_id, cx_b).await; executor.run_until_parked(); project_a.read_with(cx_a, |project, _| { @@ -3927,7 +3927,7 @@ async fn test_collaborating_with_diagnostics( ); // Join the worktree as client B. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Wait for server to see the diagnostics update. executor.run_until_parked(); @@ -3952,7 +3952,7 @@ async fn test_collaborating_with_diagnostics( }); // Join project as client C and observe the diagnostics. - let project_c = client_c.build_dev_server_project(project_id, cx_c).await; + let project_c = client_c.join_remote_project(project_id, cx_c).await; executor.run_until_parked(); let project_c_diagnostic_summaries = Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { @@ -4160,7 +4160,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( .unwrap(); // Join the project as client B and open all three files. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| { project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx)) })) @@ -4266,7 +4266,7 @@ async fn test_reloading_buffer_manually( .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); @@ -4364,7 +4364,7 @@ async fn test_formatting_buffer( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); @@ -4486,7 +4486,7 @@ async fn test_prettier_formatting_buffer( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx)); let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap(); @@ -4599,7 +4599,7 @@ async fn test_definition( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open the file on client B. let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); @@ -4744,7 +4744,7 @@ async fn test_references( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open the file on client B. let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)); @@ -4901,7 +4901,7 @@ async fn test_project_search( .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Perform a search as the guest. let mut results = HashMap::default(); @@ -4991,7 +4991,7 @@ async fn test_document_highlights( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open the file on client B. let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); @@ -5109,7 +5109,7 @@ async fn test_lsp_hover( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Open the file as the guest let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)); @@ -5286,7 +5286,7 @@ async fn test_project_symbols( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Cause the language server to start. let open_buffer_task = @@ -5381,7 +5381,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)); let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap(); @@ -6470,7 +6470,7 @@ async fn test_context_collaboration_with_reconnect( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; // Client A sees that a guest has joined. executor.run_until_parked(); diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index bad5ef9053..a9cc32c1dd 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -9,7 +9,7 @@ use remote_server::HeadlessProject; use serde_json::json; use std::{path::Path, sync::Arc}; -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_sharing_an_ssh_remote_project( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -54,9 +54,8 @@ async fn test_sharing_an_ssh_remote_project( let (project_a, worktree_id) = client_a .build_ssh_project("/code/project1", client_ssh, cx_a) .await; - executor.run_until_parked(); - // User A shares the remote project. + // While the SSH worktree is being scanned, user A shares the remote project. let active_call_a = cx_a.read(ActiveCall::global); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) @@ -64,12 +63,30 @@ async fn test_sharing_an_ssh_remote_project( .unwrap(); // User B joins the project. - let project_b = client_b.build_dev_server_project(project_id, cx_b).await; + let project_b = client_b.join_remote_project(project_id, cx_b).await; let worktree_b = project_b .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx)) .unwrap(); + let worktree_a = project_a + .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx)) + .unwrap(); + executor.run_until_parked(); + + worktree_a.update(cx_a, |worktree, _cx| { + assert_eq!( + worktree.paths().map(Arc::as_ref).collect::>(), + vec![ + Path::new(".zed"), + Path::new(".zed/settings.json"), + Path::new("README.md"), + Path::new("src"), + Path::new("src/lib.rs"), + ] + ); + }); + worktree_b.update(cx_b, |worktree, _cx| { assert_eq!( worktree.paths().map(Arc::as_ref).collect::>(), diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 6f07d76b0b..94c7d3907f 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -921,7 +921,7 @@ impl TestClient { }) } - pub async fn build_dev_server_project( + pub async fn join_remote_project( &self, host_project_id: u64, guest_cx: &mut TestAppContext, diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index e445eab2dd..1fc04a0d0b 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -204,8 +204,11 @@ impl WorktreeStore { self.loading_worktrees.insert(path.clone(), task.shared()); } let task = self.loading_worktrees.get(&path).unwrap().clone(); - cx.background_executor().spawn(async move { - match task.await { + cx.spawn(|this, mut cx| async move { + let result = task.await; + this.update(&mut cx, |this, _| this.loading_worktrees.remove(&path)) + .ok(); + match result { Ok(worktree) => Ok(worktree), Err(err) => Err((*err).cloned()), } @@ -219,7 +222,8 @@ impl WorktreeStore { visible: bool, cx: &mut ModelContext, ) -> Task, Arc>> { - let mut abs_path = abs_path.as_ref().to_string_lossy().to_string(); + let path_key: Arc = abs_path.as_ref().into(); + let mut abs_path = path_key.clone().to_string_lossy().to_string(); // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/` // in which case want to strip the leading the `/`. // On the host-side, the `~` will get expanded. @@ -261,8 +265,9 @@ impl WorktreeStore { ) })?; - this.update(&mut cx, |this, cx| this.add(&worktree, cx))?; - + this.update(&mut cx, |this, cx| { + this.add(&worktree, cx); + })?; Ok(worktree) }) } @@ -280,10 +285,6 @@ impl WorktreeStore { cx.spawn(move |this, mut cx| async move { let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await; - this.update(&mut cx, |project, _| { - project.loading_worktrees.remove(&path); - })?; - let worktree = worktree?; this.update(&mut cx, |this, cx| this.add(&worktree, cx))?; @@ -317,7 +318,7 @@ impl WorktreeStore { }); let abs_path = abs_path.as_ref().to_path_buf(); - cx.spawn(move |project, mut cx| async move { + cx.spawn(move |project, cx| async move { let (tx, rx) = futures::channel::oneshot::channel(); let tx = RefCell::new(Some(tx)); let Some(project) = project.upgrade() else { @@ -339,14 +340,10 @@ impl WorktreeStore { request.await?; let worktree = rx.await.map_err(|e| anyhow!(e))?; drop(observer); - project.update(&mut cx, |project, _| { - project.loading_worktrees.remove(&path); - })?; Ok(worktree) }) } - #[track_caller] pub fn add(&mut self, worktree: &Model, cx: &mut ModelContext) { let worktree_id = worktree.read(cx).id(); debug_assert!(self.worktrees().all(|w| w.read(cx).id() != worktree_id)); @@ -553,9 +550,12 @@ impl WorktreeStore { let client = client.clone(); async move { if client.is_via_collab() { - client.request(update).map(|result| result.is_ok()).await + client + .request(update) + .map(|result| result.log_err().is_some()) + .await } else { - client.send(update).is_ok() + client.send(update).log_err().is_some() } } } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 084fcf9929..8920639427 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -564,6 +564,48 @@ async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut }); } +#[gpui::test] +async fn test_adding_then_removing_then_adding_worktrees( + cx: &mut TestAppContext, + server_cx: &mut TestAppContext, +) { + let (project, _headless, _fs) = init_test(cx, server_cx).await; + let (_worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_worktree("/code/project1", true, cx) + }) + .await + .unwrap(); + + let (worktree_2, _) = project + .update(cx, |project, cx| { + project.find_or_create_worktree("/code/project2", true, cx) + }) + .await + .unwrap(); + let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id()); + + project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx)); + + let (worktree_2, _) = project + .update(cx, |project, cx| { + project.find_or_create_worktree("/code/project2", true, cx) + }) + .await + .unwrap(); + + cx.run_until_parked(); + worktree_2.update(cx, |worktree, _cx| { + assert!(worktree.is_visible()); + let entries = worktree.entries(true, 0).collect::>(); + assert_eq!(entries.len(), 2); + assert_eq!( + entries[1].path.to_string_lossy().to_string(), + "README.md".to_string() + ) + }) +} + fn init_logger() { if std::env::var("RUST_LOG").is_ok() { env_logger::try_init().ok(); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f91a832b80..d81c91132b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1826,10 +1826,13 @@ impl RemoteWorktree { let initial_update = self .snapshot .build_initial_update(project_id, self.id().to_proto()); - self.updates_tx = Some(tx); + self.update_observer = Some(tx); cx.spawn(|this, mut cx| async move { let mut update = initial_update; loop { + // SSH projects use a special project ID of 0, and we need to + // remap it to the correct one here. + update.project_id = project_id; if !callback(update).await { break; } @@ -1841,7 +1844,7 @@ impl RemoteWorktree { } this.update(&mut cx, |this, _| { let this = this.as_remote_mut().unwrap(); - this.updates_tx.take(); + this.update_observer.take(); }) }) .detach(); diff --git a/script/zed-local b/script/zed-local index c3dfb2879d..9ec9b24af7 100755 --- a/script/zed-local +++ b/script/zed-local @@ -9,12 +9,18 @@ SUMMARY Each instance of Zed will be signed in as a different user specified in either \`.admins.json\` or \`.admins.default.json\`. + All arguments after the initial options will be passed through to the first + instance of Zed. This can be used to test SSH remoting along with collab, like + so: + + $ script/zed-local -2 ssh://your-ssh-uri-here + OPTIONS - --help Print this help message - --release Build Zed in release mode - -2, -3, -4, ... Spawn multiple Zed instances, with their windows tiled. - --top Arrange the Zed windows so they take up the top half of the screen. - --stable Use stable Zed release installed on local machine for all instances (except for the first one). + --help Print this help message + --release Build Zed in release mode + -2, -3, -4, ... Spawn multiple Zed instances, with their windows tiled. + --top Arrange the Zed windows so they take up the top half of the screen. + --stable Use stable Zed release installed on local machine for all instances (except for the first one). `.trim(); const { spawn, execSync, execFileSync } = require("child_process");