Fix diff indicators not restored when reopening remote project (#31384)

feeiyu and Cole Miller created

Closes #30917

Release Notes:

- Fix diff indicators not restored when reopening remote project

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

Cargo.lock                                       |   2 
crates/editor/src/editor.rs                      |  26 +++
crates/git_ui/src/git_panel.rs                   |   2 
crates/project/src/git_store.rs                  |  25 ++
crates/remote_server/Cargo.toml                  |   2 
crates/remote_server/src/remote_editing_tests.rs | 142 ++++++++++++++++++
6 files changed, 192 insertions(+), 7 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -13222,6 +13222,7 @@ dependencies = [
  "dap",
  "dap_adapters",
  "debug_adapter_extension",
+ "editor",
  "env_logger 0.11.8",
  "extension",
  "extension_host",
@@ -13260,6 +13261,7 @@ dependencies = [
  "unindent",
  "util",
  "watch",
+ "workspace",
  "worktree",
  "zlog",
 ]

crates/editor/src/editor.rs 🔗

@@ -134,6 +134,7 @@ use project::{
         },
         session::{Session, SessionEvent},
     },
+    git_store::{GitStoreEvent, RepositoryEvent},
     project_settings::DiagnosticSeverity,
 };
 
@@ -1866,6 +1867,31 @@ impl Editor {
                         _ => {}
                     },
                 ));
+                let git_store = project.read(cx).git_store().clone();
+                let project = project.clone();
+                project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| {
+                    match event {
+                        GitStoreEvent::RepositoryUpdated(
+                            _,
+                            RepositoryEvent::Updated {
+                                new_instance: true, ..
+                            },
+                            _,
+                        ) => {
+                            this.load_diff_task = Some(
+                                update_uncommitted_diff_for_buffer(
+                                    cx.entity(),
+                                    &project,
+                                    this.buffer.read(cx).all_buffers(),
+                                    this.buffer.clone(),
+                                    cx,
+                                )
+                                .shared(),
+                            );
+                        }
+                        _ => {}
+                    }
+                }));
             }
         }
 

crates/git_ui/src/git_panel.rs 🔗

@@ -475,7 +475,7 @@ impl GitPanel {
                     }
                     GitStoreEvent::RepositoryUpdated(
                         _,
-                        RepositoryEvent::Updated { full_scan },
+                        RepositoryEvent::Updated { full_scan, .. },
                         true,
                     ) => {
                         this.schedule_update(*full_scan, window, cx);

crates/project/src/git_store.rs 🔗

@@ -292,7 +292,7 @@ pub enum RepositoryState {
 
 #[derive(Clone, Debug)]
 pub enum RepositoryEvent {
-    Updated { full_scan: bool },
+    Updated { full_scan: bool, new_instance: bool },
     MergeHeadsChanged,
 }
 
@@ -1496,7 +1496,7 @@ impl GitStore {
 
             repo.update(cx, {
                 let update = update.clone();
-                |repo, cx| repo.apply_remote_update(update, cx)
+                |repo, cx| repo.apply_remote_update(update, is_new, cx)
             })?;
 
             this.active_repo_id.get_or_insert_with(|| {
@@ -3597,7 +3597,10 @@ impl Repository {
                             let snapshot = this.update(&mut cx, |this, cx| {
                                 this.snapshot.branch = branch;
                                 let snapshot = this.snapshot.clone();
-                                cx.emit(RepositoryEvent::Updated { full_scan: false });
+                                cx.emit(RepositoryEvent::Updated {
+                                    full_scan: false,
+                                    new_instance: false,
+                                });
                                 snapshot
                             })?;
                             if let Some(updates_tx) = updates_tx {
@@ -3942,6 +3945,7 @@ impl Repository {
     pub(crate) fn apply_remote_update(
         &mut self,
         update: proto::UpdateRepository,
+        is_new: bool,
         cx: &mut Context<Self>,
     ) -> Result<()> {
         let conflicted_paths = TreeSet::from_ordered_entries(
@@ -3975,7 +3979,10 @@ impl Repository {
         if update.is_last_update {
             self.snapshot.scan_id = update.scan_id;
         }
-        cx.emit(RepositoryEvent::Updated { full_scan: true });
+        cx.emit(RepositoryEvent::Updated {
+            full_scan: true,
+            new_instance: is_new,
+        });
         Ok(())
     }
 
@@ -4305,7 +4312,10 @@ impl Repository {
                                 .ok();
                         }
                     }
-                    cx.emit(RepositoryEvent::Updated { full_scan: false });
+                    cx.emit(RepositoryEvent::Updated {
+                        full_scan: false,
+                        new_instance: false,
+                    });
                 })
             },
         );
@@ -4565,7 +4575,10 @@ async fn compute_snapshot(
         || branch != prev_snapshot.branch
         || statuses_by_path != prev_snapshot.statuses_by_path
     {
-        events.push(RepositoryEvent::Updated { full_scan: true });
+        events.push(RepositoryEvent::Updated {
+            full_scan: true,
+            new_instance: false,
+        });
     }
 
     // Cache merge conflict paths so they don't change from staging/unstaging,

crates/remote_server/Cargo.toml 🔗

@@ -75,6 +75,8 @@ assistant_tools.workspace = true
 client = { workspace = true, features = ["test-support"] }
 clock = { workspace = true, features = ["test-support"] }
 dap = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }
 fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 http_client = { workspace = true, features = ["test-support"] }

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -1434,6 +1434,148 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
     });
 }
 
+// TODO: this test fails on Windows.
+#[cfg(not(windows))]
+#[gpui::test]
+async fn test_remote_git_diffs_when_recv_update_repository_delay(
+    cx: &mut TestAppContext,
+    server_cx: &mut TestAppContext,
+) {
+    use editor::Editor;
+    use gpui::VisualContext;
+    let text_2 = "
+        fn one() -> usize {
+            1
+        }
+    "
+    .unindent();
+    let text_1 = "
+        fn one() -> usize {
+            0
+        }
+    "
+    .unindent();
+
+    let fs = FakeFs::new(server_cx.executor());
+    fs.insert_tree(
+        "/code",
+        json!({
+            "project1": {
+                "src": {
+                    "lib.rs": text_2
+                },
+                "README.md": "# project 1",
+            },
+        }),
+    )
+    .await;
+
+    let (project, _headless) = init_test(&fs, cx, server_cx).await;
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree("/code/project1", true, cx)
+        })
+        .await
+        .unwrap();
+    let worktree_id = cx.update(|cx| worktree.read(cx).id());
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+        })
+        .await
+        .unwrap();
+    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
+    cx.update(|cx| {
+        workspace::init_settings(cx);
+        editor::init_settings(cx);
+    });
+    let cx = cx.add_empty_window();
+    let editor = cx.new_window_entity(|window, cx| {
+        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
+    });
+
+    // Remote server will send proto::UpdateRepository after the instance of Editor create.
+    fs.insert_tree(
+        "/code",
+        json!({
+            "project1": {
+                ".git": {},
+            },
+        }),
+    )
+    .await;
+
+    fs.set_index_for_repo(
+        Path::new("/code/project1/.git"),
+        &[("src/lib.rs".into(), text_1.clone())],
+    );
+    fs.set_head_for_repo(
+        Path::new("/code/project1/.git"),
+        &[("src/lib.rs".into(), text_1.clone())],
+        "sha",
+    );
+
+    cx.executor().run_until_parked();
+    let diff = editor
+        .read_with(cx, |editor, cx| {
+            editor
+                .buffer()
+                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
+        })
+        .unwrap();
+
+    diff.read_with(cx, |diff, cx| {
+        assert_eq!(diff.base_text_string().unwrap(), text_1);
+        assert_eq!(
+            diff.secondary_diff()
+                .unwrap()
+                .read(cx)
+                .base_text_string()
+                .unwrap(),
+            text_1
+        );
+    });
+
+    // stage the current buffer's contents
+    fs.set_index_for_repo(
+        Path::new("/code/project1/.git"),
+        &[("src/lib.rs".into(), text_2.clone())],
+    );
+
+    cx.executor().run_until_parked();
+    diff.read_with(cx, |diff, cx| {
+        assert_eq!(diff.base_text_string().unwrap(), text_1);
+        assert_eq!(
+            diff.secondary_diff()
+                .unwrap()
+                .read(cx)
+                .base_text_string()
+                .unwrap(),
+            text_2
+        );
+    });
+
+    // commit the current buffer's contents
+    fs.set_head_for_repo(
+        Path::new("/code/project1/.git"),
+        &[("src/lib.rs".into(), text_2.clone())],
+        "sha",
+    );
+
+    cx.executor().run_until_parked();
+    diff.read_with(cx, |diff, cx| {
+        assert_eq!(diff.base_text_string().unwrap(), text_2);
+        assert_eq!(
+            diff.secondary_diff()
+                .unwrap()
+                .read(cx)
+                .base_text_string()
+                .unwrap(),
+            text_2
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
     let fs = FakeFs::new(server_cx.executor());