Fix race conditions in updating buffer diffs on git changes (#26409)

Max Brunsfeld and Cole Miller created

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>

Change summary

crates/buffer_diff/src/buffer_diff.rs |  43 +++++-
crates/project/src/buffer_store.rs    | 173 +++++++++++++---------------
2 files changed, 116 insertions(+), 100 deletions(-)

Detailed changes

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -837,8 +837,8 @@ impl BufferDiff {
         language: Option<Arc<Language>>,
         language_registry: Option<Arc<LanguageRegistry>>,
         cx: &mut AsyncApp,
-    ) -> anyhow::Result<Option<Range<Anchor>>> {
-        let snapshot = if base_text_changed || language_changed {
+    ) -> anyhow::Result<BufferDiffSnapshot> {
+        let inner = if base_text_changed || language_changed {
             cx.update(|cx| {
                 Self::build(
                     buffer.clone(),
@@ -860,18 +860,45 @@ impl BufferDiff {
             })?
             .await
         };
-
-        this.update(cx, |this, _| this.set_state(snapshot, &buffer))
+        Ok(BufferDiffSnapshot {
+            inner,
+            secondary_diff: None,
+        })
     }
 
-    pub fn update_diff_from(
+    pub fn set_snapshot(
         &mut self,
         buffer: &text::BufferSnapshot,
-        other: &Entity<Self>,
+        new_snapshot: BufferDiffSnapshot,
+        language_changed: bool,
+        secondary_changed_range: Option<Range<Anchor>>,
         cx: &mut Context<Self>,
     ) -> Option<Range<Anchor>> {
-        let other = other.read(cx).inner.clone();
-        self.set_state(other, buffer)
+        let changed_range = self.set_state(new_snapshot.inner, buffer);
+        if language_changed {
+            cx.emit(BufferDiffEvent::LanguageChanged);
+        }
+
+        let changed_range = match (secondary_changed_range, changed_range) {
+            (None, None) => None,
+            (Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
+            (None, Some(uncommitted_range)) => Some(uncommitted_range),
+            (Some(unstaged_range), Some(uncommitted_range)) => {
+                let mut start = uncommitted_range.start;
+                let mut end = uncommitted_range.end;
+                if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
+                {
+                    start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
+                    end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
+                }
+                Some(start..end)
+            }
+        };
+
+        cx.emit(BufferDiffEvent::DiffChanged {
+            changed_range: changed_range.clone(),
+        });
+        changed_range
     }
 
     fn set_state(

crates/project/src/buffer_store.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
 };
 use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
 use anyhow::{anyhow, bail, Context as _, Result};
-use buffer_diff::{BufferDiff, BufferDiffEvent};
+use buffer_diff::BufferDiff;
 use client::Client;
 use collections::{hash_map, HashMap, HashSet};
 use fs::Fs;
@@ -217,39 +217,29 @@ impl BufferDiffState {
             _ => false,
         };
         self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
-            let mut unstaged_changed_range = None;
+            let mut new_unstaged_diff = None;
             if let Some(unstaged_diff) = &unstaged_diff {
-                unstaged_changed_range = BufferDiff::update_diff(
-                    unstaged_diff.clone(),
-                    buffer.clone(),
-                    index,
-                    index_changed,
-                    language_changed,
-                    language.clone(),
-                    language_registry.clone(),
-                    &mut cx,
-                )
-                .await?;
-
-                unstaged_diff.update(&mut cx, |_, cx| {
-                    if language_changed {
-                        cx.emit(BufferDiffEvent::LanguageChanged);
-                    }
-                    if let Some(changed_range) = unstaged_changed_range.clone() {
-                        cx.emit(BufferDiffEvent::DiffChanged {
-                            changed_range: Some(changed_range),
-                        })
-                    }
-                })?;
+                new_unstaged_diff = Some(
+                    BufferDiff::update_diff(
+                        unstaged_diff.clone(),
+                        buffer.clone(),
+                        index,
+                        index_changed,
+                        language_changed,
+                        language.clone(),
+                        language_registry.clone(),
+                        &mut cx,
+                    )
+                    .await?,
+                );
             }
 
+            let mut new_uncommitted_diff = None;
             if let Some(uncommitted_diff) = &uncommitted_diff {
-                let uncommitted_changed_range =
-                    if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) {
-                        uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
-                            uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx)
-                        })?
-                    } else {
+                new_uncommitted_diff = if index_matches_head {
+                    new_unstaged_diff.clone()
+                } else {
+                    Some(
                         BufferDiff::update_diff(
                             uncommitted_diff.clone(),
                             buffer.clone(),
@@ -260,32 +250,32 @@ impl BufferDiffState {
                             language_registry.clone(),
                             &mut cx,
                         )
-                        .await?
-                    };
+                        .await?,
+                    )
+                }
+            }
+
+            let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
+                unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
+            {
+                unstaged_diff.update(&mut cx, |diff, cx| {
+                    diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
+                })?
+            } else {
+                None
+            };
 
+            if let Some((uncommitted_diff, new_uncommitted_diff)) =
+                uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
+            {
                 uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
-                    if language_changed {
-                        cx.emit(BufferDiffEvent::LanguageChanged);
-                    }
-                    let changed_range = match (unstaged_changed_range, uncommitted_changed_range) {
-                        (None, None) => None,
-                        (Some(unstaged_range), None) => {
-                            uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
-                        }
-                        (None, Some(uncommitted_range)) => Some(uncommitted_range),
-                        (Some(unstaged_range), Some(uncommitted_range)) => {
-                            let mut start = uncommitted_range.start;
-                            let mut end = uncommitted_range.end;
-                            if let Some(unstaged_range) =
-                                uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
-                            {
-                                start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
-                                end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
-                            }
-                            Some(start..end)
-                        }
-                    };
-                    cx.emit(BufferDiffEvent::DiffChanged { changed_range });
+                    uncommitted_diff.set_snapshot(
+                        &buffer,
+                        new_uncommitted_diff,
+                        language_changed,
+                        unstaged_changed_range,
+                        cx,
+                    );
                 })?;
             }
 
@@ -813,8 +803,7 @@ impl LocalBufferStore {
             let Some(buffer) = buffer.upgrade() else {
                 continue;
             };
-            let buffer = buffer.read(cx);
-            let Some(file) = File::from_dyn(buffer.file()) else {
+            let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
                 continue;
             };
             if file.worktree != worktree_handle {
@@ -825,7 +814,6 @@ impl LocalBufferStore {
                 .iter()
                 .any(|(work_dir, _)| file.path.starts_with(work_dir))
             {
-                let snapshot = buffer.text_snapshot();
                 let has_unstaged_diff = diff_state
                     .unstaged_diff
                     .as_ref()
@@ -835,7 +823,7 @@ impl LocalBufferStore {
                     .as_ref()
                     .is_some_and(|set| set.is_upgradable());
                 diff_state_updates.push((
-                    snapshot.clone(),
+                    buffer,
                     file.path.clone(),
                     has_unstaged_diff.then(|| diff_state.index_text.clone()),
                     has_uncommitted_diff.then(|| diff_state.head_text.clone()),
@@ -854,36 +842,33 @@ impl LocalBufferStore {
                 .background_spawn(async move {
                     diff_state_updates
                         .into_iter()
-                        .filter_map(
-                            |(buffer_snapshot, path, current_index_text, current_head_text)| {
-                                let local_repo = snapshot.local_repo_for_path(&path)?;
-                                let relative_path = local_repo.relativize(&path).ok()?;
-                                let index_text = if current_index_text.is_some() {
-                                    local_repo.repo().load_index_text(&relative_path)
-                                } else {
-                                    None
-                                };
-                                let head_text = if current_head_text.is_some() {
-                                    local_repo.repo().load_committed_text(&relative_path)
-                                } else {
-                                    None
-                                };
+                        .filter_map(|(buffer, path, current_index_text, current_head_text)| {
+                            let local_repo = snapshot.local_repo_for_path(&path)?;
+                            let relative_path = local_repo.relativize(&path).ok()?;
+                            let index_text = if current_index_text.is_some() {
+                                local_repo.repo().load_index_text(&relative_path)
+                            } else {
+                                None
+                            };
+                            let head_text = if current_head_text.is_some() {
+                                local_repo.repo().load_committed_text(&relative_path)
+                            } else {
+                                None
+                            };
 
-                                // Avoid triggering a diff update if the base text has not changed.
-                                if let Some((current_index, current_head)) =
-                                    current_index_text.as_ref().zip(current_head_text.as_ref())
+                            // Avoid triggering a diff update if the base text has not changed.
+                            if let Some((current_index, current_head)) =
+                                current_index_text.as_ref().zip(current_head_text.as_ref())
+                            {
+                                if current_index.as_deref() == index_text.as_ref()
+                                    && current_head.as_deref() == head_text.as_ref()
                                 {
-                                    if current_index.as_deref() == index_text.as_ref()
-                                        && current_head.as_deref() == head_text.as_ref()
-                                    {
-                                        return None;
-                                    }
+                                    return None;
                                 }
+                            }
 
-                                let diff_bases_change = match (
-                                    current_index_text.is_some(),
-                                    current_head_text.is_some(),
-                                ) {
+                            let diff_bases_change =
+                                match (current_index_text.is_some(), current_head_text.is_some()) {
                                     (true, true) => Some(if index_text == head_text {
                                         DiffBasesChange::SetBoth(head_text)
                                     } else {
@@ -896,17 +881,17 @@ impl LocalBufferStore {
                                     (false, true) => Some(DiffBasesChange::SetHead(head_text)),
                                     (false, false) => None,
                                 };
-                                Some((buffer_snapshot, diff_bases_change))
-                            },
-                        )
+
+                            Some((buffer, diff_bases_change))
+                        })
                         .collect::<Vec<_>>()
                 })
                 .await;
 
             this.update(&mut cx, |this, cx| {
-                for (buffer_snapshot, diff_bases_change) in diff_bases_changes_by_buffer {
+                for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
                     let Some(OpenBuffer::Complete { diff_state, .. }) =
-                        this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
+                        this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
                     else {
                         continue;
                     };
@@ -917,8 +902,9 @@ impl LocalBufferStore {
                     diff_state.update(cx, |diff_state, cx| {
                         use proto::update_diff_bases::Mode;
 
+                        let buffer = buffer.read(cx);
                         if let Some((client, project_id)) = this.downstream_client.as_ref() {
-                            let buffer_id = buffer_snapshot.remote_id().to_proto();
+                            let buffer_id = buffer.remote_id().to_proto();
                             let (staged_text, committed_text, mode) = match diff_bases_change
                                 .clone()
                             {
@@ -942,8 +928,11 @@ impl LocalBufferStore {
                             client.send(message).log_err();
                         }
 
-                        let _ =
-                            diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
+                        let _ = diff_state.diff_bases_changed(
+                            buffer.text_snapshot(),
+                            diff_bases_change,
+                            cx,
+                        );
                     });
                 }
             })