WIP Notifying buffers of head text change

Julia and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/editor/src/items.rs           |  4 +-
crates/editor/src/multi_buffer.rs    |  6 +-
crates/language/src/buffer.rs        | 14 +++++-
crates/project/src/fs.rs             | 46 ------------------------
crates/project/src/git_repository.rs | 44 +++++++++++++++++++++--
crates/project/src/project.rs        | 29 ++++++++++++++
crates/project/src/worktree.rs       | 57 +++++++++++++++++++++++++++--
crates/workspace/src/workspace.rs    | 12 +++---
8 files changed, 142 insertions(+), 70 deletions(-)

Detailed changes

crates/editor/src/items.rs 🔗

@@ -478,13 +478,13 @@ impl Item for Editor {
         })
     }
 
-    fn update_git(
+    fn git_diff_recalc(
         &mut self,
         _project: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         self.buffer().update(cx, |multibuffer, cx| {
-            multibuffer.update_git(cx);
+            multibuffer.git_diff_recalc(cx);
         });
         Task::ready(Ok(()))
     }

crates/editor/src/multi_buffer.rs 🔗

@@ -312,13 +312,13 @@ impl MultiBuffer {
         self.read(cx).symbols_containing(offset, theme)
     }
 
-    pub fn update_git(&mut self, cx: &mut ModelContext<Self>) {
+    pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
         let buffers = self.buffers.borrow();
         for buffer_state in buffers.values() {
-            if buffer_state.buffer.read(cx).needs_git_update() {
+            if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
                 buffer_state
                     .buffer
-                    .update(cx, |buffer, cx| buffer.update_git(cx))
+                    .update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
             }
         }
     }

crates/language/src/buffer.rs 🔗

@@ -613,6 +613,7 @@ impl Buffer {
                 cx,
             );
         }
+        self.update_git(cx);
         cx.emit(Event::Reloaded);
         cx.notify();
     }
@@ -661,12 +662,19 @@ impl Buffer {
         self.file = Some(new_file);
         task
     }
+    
+    pub fn update_git(&mut self, cx: &mut ModelContext<Self>) {
+        //Grab head text
+        
+
+        self.git_diff_recalc(cx);
+    }
 
-    pub fn needs_git_update(&self) -> bool {
+    pub fn needs_git_diff_recalc(&self) -> bool {
         self.git_diff_status.diff.needs_update(self)
     }
 
-    pub fn update_git(&mut self, cx: &mut ModelContext<Self>) {
+    pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
         if self.git_diff_status.update_in_progress {
             self.git_diff_status.update_requested = true;
             return;
@@ -692,7 +700,7 @@ impl Buffer {
 
                         this.git_diff_status.update_in_progress = false;
                         if this.git_diff_status.update_requested {
-                            this.update_git(cx);
+                            this.git_diff_recalc(cx);
                         }
                     })
                 }

crates/project/src/fs.rs 🔗

@@ -34,7 +34,6 @@ pub trait Fs: Send + Sync {
     async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
     async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
     async fn load(&self, path: &Path) -> Result<String>;
-    async fn load_head_text(&self, path: &Path) -> Option<String>;
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
     async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
     async fn is_file(&self, path: &Path) -> bool;
@@ -48,7 +47,6 @@ pub trait Fs: Send + Sync {
         path: &Path,
         latency: Duration,
     ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
-    fn open_git_repository(&self, abs_dotgit_path: &Path) -> Option<GitRepository>;
     fn is_fake(&self) -> bool;
     #[cfg(any(test, feature = "test-support"))]
     fn as_fake(&self) -> &FakeFs;
@@ -168,38 +166,6 @@ impl Fs for RealFs {
         Ok(text)
     }
 
-    async fn load_head_text(&self, path: &Path) -> Option<String> {
-        fn logic(path: &Path) -> Result<Option<String>> {
-            let repo = Repository::open_ext(path, RepositoryOpenFlags::empty(), &[OsStr::new("")])?;
-            assert!(repo.path().ends_with(".git"));
-            let repo_root_path = match repo.path().parent() {
-                Some(root) => root,
-                None => return Ok(None),
-            };
-
-            let relative_path = path.strip_prefix(repo_root_path)?;
-            let object = repo
-                .head()?
-                .peel_to_tree()?
-                .get_path(relative_path)?
-                .to_object(&repo)?;
-
-            let content = match object.as_blob() {
-                Some(blob) => blob.content().to_owned(),
-                None => return Ok(None),
-            };
-
-            let head_text = String::from_utf8(content.to_owned())?;
-            Ok(Some(head_text))
-        }
-
-        match logic(path) {
-            Ok(value) => return value,
-            Err(err) => log::error!("Error loading head text: {:?}", err),
-        }
-        None
-    }
-
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
         let buffer_size = text.summary().len.min(10 * 1024);
         let file = smol::fs::File::create(path).await?;
@@ -274,10 +240,6 @@ impl Fs for RealFs {
         })))
     }
 
-    fn open_git_repository(&self, abs_dotgit_path: &Path) -> Option<GitRepository> {
-        GitRepository::open(abs_dotgit_path)
-    }
-
     fn is_fake(&self) -> bool {
         false
     }
@@ -791,10 +753,6 @@ impl Fs for FakeFs {
         entry.file_content(&path).cloned()
     }
 
-    async fn load_head_text(&self, _: &Path) -> Option<String> {
-        None
-    }
-
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
@@ -893,10 +851,6 @@ impl Fs for FakeFs {
         }))
     }
 
-    fn open_git_repository(&self, _: &Path) -> Option<GitRepository> {
-        None
-    }
-
     fn is_fake(&self) -> bool {
         true
     }

crates/project/src/git_repository.rs 🔗

@@ -1,6 +1,7 @@
-use git2::Repository;
+use anyhow::Result;
+use git2::{Repository as LibGitRepository, RepositoryOpenFlags as LibGitRepositoryOpenFlags};
 use parking_lot::Mutex;
-use std::{path::Path, sync::Arc};
+use std::{path::Path, sync::Arc, ffi::OsStr};
 use util::ResultExt;
 
 #[derive(Clone)]
@@ -11,12 +12,12 @@ pub struct GitRepository {
     // Note: if .git is a file, this points to the folder indicated by the .git file
     git_dir_path: Arc<Path>,
     scan_id: usize,
-    libgit_repository: Arc<Mutex<git2::Repository>>,
+    libgit_repository: Arc<Mutex<LibGitRepository>>,
 }
 
 impl GitRepository {
     pub fn open(dotgit_path: &Path) -> Option<GitRepository> {
-        Repository::open(&dotgit_path)
+        LibGitRepository::open(&dotgit_path)
             .log_err()
             .and_then(|libgit_repository| {
                 Some(Self {
@@ -60,4 +61,39 @@ impl GitRepository {
         let mut git2 = self.libgit_repository.lock();
         f(&mut git2)
     }
+
+    pub async fn load_head_text(&self, file_path: &Path) -> Option<String> {
+        fn logic(repo: &LibGitRepository, file_path: &Path) -> Result<Option<String>> {
+            let object = repo
+                .head()?
+                .peel_to_tree()?
+                .get_path(file_path)?
+                .to_object(&repo)?;
+
+            let content = match object.as_blob() {
+                Some(blob) => blob.content().to_owned(),
+                None => return Ok(None),
+            };
+
+            let head_text = String::from_utf8(content.to_owned())?;
+            Ok(Some(head_text))
+        }
+
+        match logic(&self.libgit_repository.lock(), file_path) {
+            Ok(value) => return value,
+            Err(err) => log::error!("Error loading head text: {:?}", err),
+        }
+        None
+    }
+}
+
+impl std::fmt::Debug for GitRepository {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("GitRepository")
+            .field("content_path", &self.content_path)
+            .field("git_dir_path", &self.git_dir_path)
+            .field("scan_id", &self.scan_id)
+            .field("libgit_repository", &"LibGitRepository")
+            .finish()
+    }
 }

crates/project/src/project.rs 🔗

@@ -13,6 +13,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
+use git_repository::GitRepository;
 use gpui::{
     AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
@@ -4536,7 +4537,9 @@ impl Project {
         if worktree.read(cx).is_local() {
             cx.subscribe(worktree, |this, worktree, event, cx| match event {
                 worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
-                worktree::Event::UpdatedGitRepositories(_) => todo!(),
+                worktree::Event::UpdatedGitRepositories(updated_repos) => {
+                    this.update_local_worktree_buffers_git_repos(updated_repos, cx)
+                }
             })
             .detach();
         }
@@ -4644,6 +4647,30 @@ impl Project {
         }
     }
 
+    fn update_local_worktree_buffers_git_repos(
+        &mut self,
+        updated_repos: &[GitRepository],
+        cx: &mut ModelContext<Self>,
+    ) {
+        for (buffer_id, buffer) in &self.opened_buffers {
+            if let Some(buffer) = buffer.upgrade(cx) {
+                buffer.update(cx, |buffer, cx| {
+                    let updated = updated_repos.iter().any(|repo| {
+                        buffer
+                            .file()
+                            .and_then(|file| file.as_local())
+                            .map(|file| repo.manages(&file.abs_path(cx)))
+                            .unwrap_or(false)
+                    });
+
+                    if updated {
+                        buffer.update_git(cx);
+                    }
+                });
+            }
+        }
+    }
+
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;

crates/project/src/worktree.rs 🔗

@@ -467,7 +467,7 @@ impl LocalWorktree {
                 .await?;
             Ok(cx.add_model(|cx| {
                 let mut buffer = Buffer::from_file(0, contents, head_text, Arc::new(file), cx);
-                buffer.update_git(cx);
+                buffer.git_diff_recalc(cx);
                 buffer
             }))
         })
@@ -522,16 +522,28 @@ impl LocalWorktree {
 
         match self.scan_state() {
             ScanState::Idle => {
-                self.snapshot = self.background_snapshot.lock().clone();
+                let new_snapshot = self.background_snapshot.lock().clone();
+                let updated_repos = self.list_updated_repos(&new_snapshot);
+                self.snapshot = new_snapshot;
+
                 if let Some(share) = self.share.as_mut() {
                     *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
                 }
+
                 cx.emit(Event::UpdatedEntries);
+
+                if !updated_repos.is_empty() {
+                    cx.emit(Event::UpdatedGitRepositories(updated_repos));
+                }
             }
 
             ScanState::Initializing => {
                 let is_fake_fs = self.fs.is_fake();
-                self.snapshot = self.background_snapshot.lock().clone();
+
+                let new_snapshot = self.background_snapshot.lock().clone();
+                let updated_repos = self.list_updated_repos(&new_snapshot);
+                self.snapshot = new_snapshot;
+
                 self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
                     if is_fake_fs {
                         #[cfg(any(test, feature = "test-support"))]
@@ -543,7 +555,12 @@ impl LocalWorktree {
                         this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
                     }
                 }));
+
                 cx.emit(Event::UpdatedEntries);
+
+                if !updated_repos.is_empty() {
+                    cx.emit(Event::UpdatedGitRepositories(updated_repos));
+                }
             }
 
             _ => {
@@ -556,6 +573,34 @@ impl LocalWorktree {
         cx.notify();
     }
 
+    fn list_updated_repos(&self, new_snapshot: &LocalSnapshot) -> Vec<GitRepository> {
+        let old_snapshot = &self.snapshot;
+
+        fn diff<'a>(
+            a: &'a LocalSnapshot,
+            b: &'a LocalSnapshot,
+            updated: &mut HashMap<&'a Path, GitRepository>,
+        ) {
+            for a_repo in &a.git_repositories {
+                let matched = b.git_repositories.iter().find(|b_repo| {
+                    a_repo.git_dir_path() == b_repo.git_dir_path()
+                        && a_repo.scan_id() == b_repo.scan_id()
+                });
+
+                if matched.is_some() {
+                    updated.insert(a_repo.git_dir_path(), a_repo.clone());
+                }
+            }
+        }
+
+        let mut updated = HashMap::<&Path, GitRepository>::default();
+
+        diff(old_snapshot, new_snapshot, &mut updated);
+        diff(new_snapshot, old_snapshot, &mut updated);
+
+        updated.into_values().collect()
+    }
+
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
         let mut scan_state_rx = self.last_scan_state_rx.clone();
         async move {
@@ -606,9 +651,11 @@ impl LocalWorktree {
                 files_included,
                 settings::GitFilesIncluded::All | settings::GitFilesIncluded::OnlyTracked
             ) {
+                
+                
                 let fs = fs.clone();
                 let abs_path = abs_path.clone();
-                let task = async move { fs.load_head_text(&abs_path).await };
+                let opt_future = async move { fs.load_head_text(&abs_path).await };
                 let results = cx.background().spawn(task).await;
 
                 if files_included == settings::GitFilesIncluded::All {
@@ -1495,7 +1542,7 @@ impl LocalSnapshot {
                 .git_repositories
                 .binary_search_by_key(&abs_path.as_path(), |repo| repo.git_dir_path())
             {
-                if let Some(repository) = fs.open_git_repository(&abs_path) {
+                if let Some(repository) = GitRepository::open(&abs_path) {
                     self.git_repositories.insert(ix, repository);
                 }
             }

crates/workspace/src/workspace.rs 🔗

@@ -317,7 +317,7 @@ pub trait Item: View {
         project: ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
-    fn update_git(
+    fn git_diff_recalc(
         &mut self,
         _project: ModelHandle<Project>,
         _cx: &mut ViewContext<Self>,
@@ -539,7 +539,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
     ) -> Task<Result<()>>;
     fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
         -> Task<Result<()>>;
-    fn update_git(
+    fn git_diff_recalc(
         &self,
         project: ModelHandle<Project>,
         cx: &mut MutableAppContext,
@@ -753,7 +753,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                         workspace,
                                         cx,
                                         |project, mut cx| async move {
-                                            cx.update(|cx| item.update_git(project, cx))
+                                            cx.update(|cx| item.git_diff_recalc(project, cx))
                                                 .await
                                                 .log_err();
                                         },
@@ -762,7 +762,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                     let project = workspace.project().downgrade();
                                     cx.spawn_weak(|_, mut cx| async move {
                                         if let Some(project) = project.upgrade(&cx) {
-                                            cx.update(|cx| item.update_git(project, cx))
+                                            cx.update(|cx| item.git_diff_recalc(project, cx))
                                                 .await
                                                 .log_err();
                                         }
@@ -850,12 +850,12 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.update(cx, |item, cx| item.reload(project, cx))
     }
 
-    fn update_git(
+    fn git_diff_recalc(
         &self,
         project: ModelHandle<Project>,
         cx: &mut MutableAppContext,
     ) -> Task<Result<()>> {
-        self.update(cx, |item, cx| item.update_git(project, cx))
+        self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
     }
 
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {