Don't prompt guest to save when closing window after disconnection

Antonio Scandurra created

Change summary

crates/collab/src/integration_tests.rs | 43 ++++++++++++++++++++++++---
crates/project/src/project.rs          |  8 +++-
crates/workspace/src/workspace.rs      | 34 ++++++++++-----------
3 files changed, 58 insertions(+), 27 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -267,7 +267,8 @@ async fn test_host_disconnect(
     cx_b: &mut TestAppContext,
     cx_c: &mut TestAppContext,
 ) {
-    cx_a.foreground().forbid_parking();
+    cx_b.update(editor::init);
+    deterministic.forbid_parking();
     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;
@@ -298,10 +299,23 @@ async fn test_host_disconnect(
     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()));
 
-    project_b
-        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+    let (_, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "b.txt"), true, cx)
+        })
         .await
+        .unwrap()
+        .downcast::<Editor>()
         .unwrap();
+    cx_b.read(|cx| {
+        assert_eq!(
+            cx.focused_view_id(workspace_b.window_id()),
+            Some(editor_b.id())
+        );
+    });
+    editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
+    assert!(cx_b.is_window_edited(workspace_b.window_id()));
 
     // Request to join that project as client C
     let project_c = cx_c.spawn(|cx| {
@@ -328,14 +342,31 @@ async fn test_host_disconnect(
         .condition(cx_b, |project, _| project.is_read_only())
         .await;
     assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
-    cx_b.update(|_| {
-        drop(project_b);
-    });
     assert!(matches!(
         project_c.await.unwrap_err(),
         project::JoinProjectError::HostWentOffline
     ));
 
+    // Ensure client B's edited state is reset and that the whole window is blurred.
+    cx_b.read(|cx| {
+        assert_eq!(cx.focused_view_id(workspace_b.window_id()), None);
+    });
+    assert!(!cx_b.is_window_edited(workspace_b.window_id()));
+
+    // Ensure client B is not prompted to save edits when closing window after disconnecting.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.close(&Default::default(), cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+    assert_eq!(cx_b.window_ids().len(), 0);
+    cx_b.update(|_| {
+        drop(workspace_b);
+        drop(project_b);
+    });
+
     // Ensure guests can still join.
     let project_b2 = 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()));

crates/project/src/project.rs 🔗

@@ -190,6 +190,7 @@ pub enum Event {
         language_server_id: usize,
     },
     RemoteIdChanged(Option<u64>),
+    DisconnectedFromHost,
     CollaboratorLeft(PeerId),
     ContactRequestedJoin(Arc<User>),
     ContactCancelledJoinRequest(Arc<User>),
@@ -569,7 +570,7 @@ impl Project {
                             // Even if we're initially connected, any future change of the status means we momentarily disconnected.
                             if !is_connected || status.next().await.is_some() {
                                 if let Some(this) = this.upgrade(&cx) {
-                                    this.update(&mut cx, |this, cx| this.removed_from_project(cx))
+                                    this.update(&mut cx, |this, cx| this.disconnected_from_host(cx))
                                 }
                             }
                             Ok(())
@@ -1434,7 +1435,7 @@ impl Project {
         }
     }
 
-    fn removed_from_project(&mut self, cx: &mut ModelContext<Self>) {
+    fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
         if let ProjectClientState::Remote {
             sharing_has_stopped,
             ..
@@ -1451,6 +1452,7 @@ impl Project {
                     });
                 }
             }
+            cx.emit(Event::DisconnectedFromHost);
             cx.notify();
         }
     }
@@ -4628,7 +4630,7 @@ impl Project {
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
-        this.update(&mut cx, |this, cx| this.removed_from_project(cx));
+        this.update(&mut cx, |this, cx| this.disconnected_from_host(cx));
         Ok(())
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -805,17 +805,10 @@ enum FollowerItem {
 
 impl Workspace {
     pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-        cx.observe(&project, |_, project, cx| {
-            if project.read(cx).is_read_only() {
-                cx.blur();
-            }
-            cx.notify()
-        })
-        .detach();
         cx.observe_window_activation(Self::on_window_activation_changed)
             .detach();
-
-        cx.subscribe(&project, move |this, project, event, cx| {
+        cx.observe(&project, |_, _, cx| cx.notify()).detach();
+        cx.subscribe(&project, move |this, _, event, cx| {
             match event {
                 project::Event::RemoteIdChanged(remote_id) => {
                     this.project_remote_id_changed(*remote_id, cx);
@@ -826,11 +819,12 @@ impl Workspace {
                 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
                     this.update_window_title(cx);
                 }
+                project::Event::DisconnectedFromHost => {
+                    this.update_window_edited(cx);
+                    cx.blur();
+                }
                 _ => {}
             }
-            if project.read(cx).is_read_only() {
-                cx.blur();
-            }
             cx.notify()
         })
         .detach();
@@ -1029,6 +1023,10 @@ impl Workspace {
         should_prompt_to_save: bool,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
+        if self.project.read(cx).is_read_only() {
+            return Task::ready(Ok(true));
+        }
+
         let dirty_items = self
             .panes
             .iter()
@@ -1045,11 +1043,10 @@ impl Workspace {
 
         let project = self.project.clone();
         cx.spawn_weak(|_, mut cx| async move {
-            // let mut saved_project_entry_ids = HashSet::default();
             for (pane, item) in dirty_items {
-                let (is_singl, project_entry_ids) =
+                let (singleton, project_entry_ids) =
                     cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
-                if is_singl || !project_entry_ids.is_empty() {
+                if singleton || !project_entry_ids.is_empty() {
                     if let Some(ix) =
                         pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
                     {
@@ -1910,9 +1907,10 @@ impl Workspace {
     }
 
     fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
-        let is_edited = self
-            .items(cx)
-            .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
+        let is_edited = !self.project.read(cx).is_read_only()
+            && self
+                .items(cx)
+                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
         if is_edited != self.window_edited {
             self.window_edited = is_edited;
             cx.set_window_edited(self.window_edited)