Introduce diff crate to unite BufferDiff and BufferChangeSet (#24392)

Cole Miller and maxbrunsfeld created

This is a refactoring PR that does three things:

- First, it introduces a new `diff` crate that holds the previous
contents of the `git::diff` module, plus the `BufferChangeSet` type
formerly of `project::buffer_store`. The new crate is necessary since
simply moving `BufferChangeSet` into `git::diff` results in a dependency
cycle due to the use of `language::Buffer` to represent the diff base in
`BufferChangeSet`.
- Second, it renames the two main types in the new diff crate:
`BufferDiff` becomes `BufferDiffSnapshot`, and `BufferChangeSet` becomes
`BufferDiff`. This reflects that the relationship between these two
types (immutable cheaply-cloneable "value" type + stateful "resource
type" with subscriptions) mirrors existing pairs like
`Buffer`/`BufferSnapshot`. References to "change sets" throughout the
codebase are updated to refer to "diffs" instead.
- Finally, it moves the base_text field of the new BufferDiff type to
BufferDiffSnapshot.

Release Notes:

- N/A

---------

Co-authored-by: maxbrunsfeld <max@zed.dev>

Change summary

Cargo.lock                                                    |  26 
Cargo.toml                                                    |   2 
crates/collab/Cargo.toml                                      |   1 
crates/collab/src/rpc.rs                                      |   4 
crates/collab/src/tests/integration_tests.rs                  | 114 
crates/collab/src/tests/random_project_collaboration_tests.rs |   4 
crates/diff/Cargo.toml                                        |  32 
crates/diff/LICENSE-GPL                                       |   1 
crates/diff/src/diff.rs                                       | 366 ++
crates/editor/Cargo.toml                                      |   1 
crates/editor/src/editor.rs                                   |  23 
crates/editor/src/editor_tests.rs                             |  22 
crates/editor/src/element.rs                                  |   3 
crates/editor/src/hunk_diff.rs                                |  51 
crates/editor/src/proposed_changes_editor.rs                  |  27 
crates/editor/src/test/editor_test_context.rs                 |   3 
crates/git/src/git.rs                                         |   1 
crates/git_ui/Cargo.toml                                      |   1 
crates/git_ui/src/project_diff.rs                             |  18 
crates/multi_buffer/Cargo.toml                                |  13 
crates/multi_buffer/src/anchor.rs                             |  36 
crates/multi_buffer/src/multi_buffer.rs                       | 221 -
crates/multi_buffer/src/multi_buffer_tests.rs                 | 100 
crates/project/Cargo.toml                                     |   2 
crates/project/src/buffer_store.rs                            | 586 +---
crates/project/src/project.rs                                 |  15 
crates/project/src/project_tests.rs                           |  48 
crates/proto/proto/zed.proto                                  |  16 
crates/proto/src/proto.rs                                     |  16 
crates/remote_server/src/remote_editing_tests.rs              |  40 
crates/worktree/src/worktree.rs                               |   4 
31 files changed, 922 insertions(+), 875 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2753,6 +2753,7 @@ dependencies = [
  "ctor",
  "dashmap 6.1.0",
  "derive_more",
+ "diff 0.1.0",
  "editor",
  "env_logger 0.11.6",
  "envy",
@@ -3837,6 +3838,24 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "diff"
+version = "0.1.0"
+dependencies = [
+ "futures 0.3.31",
+ "git2",
+ "gpui",
+ "language",
+ "log",
+ "pretty_assertions",
+ "rope",
+ "serde_json",
+ "sum_tree",
+ "text",
+ "unindent",
+ "util",
+]
+
 [[package]]
 name = "diff"
 version = "0.1.13"
@@ -4007,6 +4026,7 @@ dependencies = [
  "convert_case 0.7.1",
  "ctor",
  "db",
+ "diff 0.1.0",
  "emojis",
  "env_logger 0.11.6",
  "file_icons",
@@ -5306,6 +5326,7 @@ dependencies = [
  "anyhow",
  "collections",
  "db",
+ "diff 0.1.0",
  "editor",
  "feature_flags",
  "futures 0.3.31",
@@ -7910,9 +7931,9 @@ dependencies = [
  "clock",
  "collections",
  "ctor",
+ "diff 0.1.0",
  "env_logger 0.11.6",
  "futures 0.3.31",
- "git",
  "gpui",
  "indoc",
  "itertools 0.14.0",
@@ -9919,7 +9940,7 @@ version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
 dependencies = [
- "diff",
+ "diff 0.1.13",
  "yansi",
 ]
 
@@ -10015,6 +10036,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "diff 0.1.0",
  "env_logger 0.11.6",
  "fancy-regex 0.14.0",
  "fs",

Cargo.toml 🔗

@@ -32,6 +32,7 @@ members = [
     "crates/db",
     "crates/deepseek",
     "crates/diagnostics",
+    "crates/diff",
     "crates/docs_preprocessor",
     "crates/editor",
     "crates/evals",
@@ -231,6 +232,7 @@ copilot = { path = "crates/copilot" }
 db = { path = "crates/db" }
 deepseek = { path = "crates/deepseek" }
 diagnostics = { path = "crates/diagnostics" }
+diff = { path = "crates/diff" }
 editor = { path = "crates/editor" }
 extension = { path = "crates/extension" }
 extension_host = { path = "crates/extension_host" }

crates/collab/Cargo.toml 🔗

@@ -33,6 +33,7 @@ clock.workspace = true
 collections.workspace = true
 dashmap.workspace = true
 derive_more.workspace = true
+diff.workspace = true
 envy = "0.4.2"
 futures.workspace = true
 google_ai.workspace = true

crates/collab/src/rpc.rs 🔗

@@ -309,8 +309,8 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
             .add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
-            .add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedChanges>)
-            .add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedChanges>)
+            .add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
+            .add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
             .add_request_handler(
                 forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
             )

crates/collab/src/tests/integration_tests.rs 🔗

@@ -2598,25 +2598,25 @@ async fn test_git_diff_base_change(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let local_unstaged_changes_a = project_local
+    let local_unstaged_diff_a = project_local
         .update(cx_a, |p, cx| {
-            p.open_unstaged_changes(buffer_local_a.clone(), cx)
+            p.open_unstaged_diff(buffer_local_a.clone(), cx)
         })
         .await
         .unwrap();
 
     // Wait for it to catch up to the new diff
     executor.run_until_parked();
-    local_unstaged_changes_a.read_with(cx_a, |change_set, cx| {
+    local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_a.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(1..2, "", "two\n")],
         );
     });
@@ -2626,47 +2626,47 @@ async fn test_git_diff_base_change(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let remote_unstaged_changes_a = project_remote
+    let remote_unstaged_diff_a = project_remote
         .update(cx_b, |p, cx| {
-            p.open_unstaged_changes(buffer_remote_a.clone(), cx)
+            p.open_unstaged_diff(buffer_remote_a.clone(), cx)
         })
         .await
         .unwrap();
 
     // Wait remote buffer to catch up to the new diff
     executor.run_until_parked();
-    remote_unstaged_changes_a.read_with(cx_b, |change_set, cx| {
+    remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = buffer_remote_a.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(1..2, "", "two\n")],
         );
     });
 
     // Open uncommitted changes on the guest, without opening them on the host first
-    let remote_uncommitted_changes_a = project_remote
+    let remote_uncommitted_diff_a = project_remote
         .update(cx_b, |p, cx| {
-            p.open_uncommitted_changes(buffer_remote_a.clone(), cx)
+            p.open_uncommitted_diff(buffer_remote_a.clone(), cx)
         })
         .await
         .unwrap();
     executor.run_until_parked();
-    remote_uncommitted_changes_a.read_with(cx_b, |change_set, cx| {
+    remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = buffer_remote_a.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(committed_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(1..2, "TWO\n", "two\n")],
         );
     });
@@ -2683,44 +2683,44 @@ async fn test_git_diff_base_change(
 
     // Wait for buffer_local_a to receive it
     executor.run_until_parked();
-    local_unstaged_changes_a.read_with(cx_a, |change_set, cx| {
+    local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_a.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(new_staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(2..3, "", "three\n")],
         );
     });
 
-    remote_unstaged_changes_a.read_with(cx_b, |change_set, cx| {
+    remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = buffer_remote_a.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(new_staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(2..3, "", "three\n")],
         );
     });
 
-    remote_uncommitted_changes_a.read_with(cx_b, |change_set, cx| {
+    remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
         let buffer = buffer_remote_a.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(new_committed_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(1..2, "TWO_HUNDRED\n", "two\n")],
         );
     });
@@ -2748,25 +2748,25 @@ async fn test_git_diff_base_change(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
         .await
         .unwrap();
-    let local_unstaged_changes_b = project_local
+    let local_unstaged_diff_b = project_local
         .update(cx_a, |p, cx| {
-            p.open_unstaged_changes(buffer_local_b.clone(), cx)
+            p.open_unstaged_diff(buffer_local_b.clone(), cx)
         })
         .await
         .unwrap();
 
     // Wait for it to catch up to the new diff
     executor.run_until_parked();
-    local_unstaged_changes_b.read_with(cx_a, |change_set, cx| {
+    local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_b.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
-            &change_set.base_text_string().unwrap(),
+            &diff.base_text_string().unwrap(),
             &[(1..2, "", "two\n")],
         );
     });
@@ -2776,22 +2776,22 @@ async fn test_git_diff_base_change(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
         .await
         .unwrap();
-    let remote_unstaged_changes_b = project_remote
+    let remote_unstaged_diff_b = project_remote
         .update(cx_b, |p, cx| {
-            p.open_unstaged_changes(buffer_remote_b.clone(), cx)
+            p.open_unstaged_diff(buffer_remote_b.clone(), cx)
         })
         .await
         .unwrap();
 
     executor.run_until_parked();
-    remote_unstaged_changes_b.read_with(cx_b, |change_set, cx| {
+    remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
         let buffer = buffer_remote_b.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
             &staged_text,
             &[(1..2, "", "two\n")],
@@ -2806,28 +2806,28 @@ async fn test_git_diff_base_change(
 
     // Wait for buffer_local_b to receive it
     executor.run_until_parked();
-    local_unstaged_changes_b.read_with(cx_a, |change_set, cx| {
+    local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
         let buffer = buffer_local_b.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(new_staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
             &new_staged_text,
             &[(2..3, "", "three\n")],
         );
     });
 
-    remote_unstaged_changes_b.read_with(cx_b, |change_set, cx| {
+    remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
         let buffer = buffer_remote_b.read(cx);
         assert_eq!(
-            change_set.base_text_string().as_deref(),
+            diff.base_text_string().as_deref(),
             Some(new_staged_text.as_str())
         );
-        git::diff::assert_hunks(
-            change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer),
+        diff::assert_hunks(
+            diff.snapshot.hunks_in_row_range(0..4, buffer),
             buffer,
             &new_staged_text,
             &[(2..3, "", "three\n")],

crates/collab/src/tests/random_project_collaboration_tests.rs 🔗

@@ -1339,7 +1339,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                         project
                             .buffer_store()
                             .read(cx)
-                            .get_unstaged_changes(host_buffer.read(cx).remote_id(), cx)
+                            .get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
                             .unwrap()
                             .read(cx)
                             .base_text_string()
@@ -1348,7 +1348,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                         project
                             .buffer_store()
                             .read(cx)
-                            .get_unstaged_changes(guest_buffer.read(cx).remote_id(), cx)
+                            .get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
                             .unwrap()
                             .read(cx)
                             .base_text_string()

crates/diff/Cargo.toml 🔗

@@ -0,0 +1,32 @@
+[package]
+name = "diff"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/diff.rs"
+
+[dependencies]
+futures.workspace = true
+git2.workspace = true
+gpui.workspace = true
+language.workspace = true
+log.workspace = true
+rope.workspace = true
+sum_tree.workspace = true
+text.workspace = true
+util.workspace = true
+
+[dev-dependencies]
+unindent.workspace = true
+serde_json.workspace = true
+pretty_assertions.workspace = true
+text = {workspace = true, features = ["test-support"]}
+
+[features]
+test-support = []

crates/git/src/diff.rs → crates/diff/src/diff.rs 🔗

@@ -1,10 +1,12 @@
+use futures::{channel::oneshot, future::OptionFuture};
+use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
+use gpui::{App, Context, Entity, EventEmitter};
+use language::{Language, LanguageRegistry};
 use rope::Rope;
-use std::{cmp, iter, ops::Range};
+use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
 use sum_tree::SumTree;
-use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
-
-pub use git2 as libgit;
-use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
+use text::{Anchor, BufferId, OffsetRangeExt, Point};
+use util::ResultExt;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub enum DiffHunkStatus {
@@ -62,36 +64,110 @@ impl sum_tree::Summary for DiffHunkSummary {
     }
 }
 
-#[derive(Debug, Clone)]
-pub struct BufferDiff {
-    tree: SumTree<InternalDiffHunk>,
+#[derive(Clone)]
+pub struct BufferDiffSnapshot {
+    hunks: SumTree<InternalDiffHunk>,
+    pub base_text: Option<language::BufferSnapshot>,
 }
 
-impl BufferDiff {
-    pub fn new(buffer: &BufferSnapshot) -> BufferDiff {
-        BufferDiff {
-            tree: SumTree::new(buffer),
+impl std::fmt::Debug for BufferDiffSnapshot {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("BufferDiffSnapshot")
+            .field("hunks", &self.hunks)
+            .finish()
+    }
+}
+
+impl BufferDiffSnapshot {
+    pub fn new(buffer: &text::BufferSnapshot) -> BufferDiffSnapshot {
+        BufferDiffSnapshot {
+            hunks: SumTree::new(buffer),
+            base_text: None,
         }
     }
 
-    pub fn new_with_single_insertion(buffer: &BufferSnapshot) -> Self {
+    pub fn new_with_single_insertion(cx: &mut App) -> Self {
+        let base_text = language::Buffer::build_empty_snapshot(cx);
         Self {
-            tree: SumTree::from_item(
+            hunks: SumTree::from_item(
                 InternalDiffHunk {
                     buffer_range: Anchor::MIN..Anchor::MAX,
                     diff_base_byte_range: 0..0,
                 },
-                buffer,
+                &base_text,
             ),
+            base_text: Some(base_text),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn build_sync(
+        buffer: text::BufferSnapshot,
+        diff_base: String,
+        cx: &mut gpui::TestAppContext,
+    ) -> Self {
+        let snapshot =
+            cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
+        cx.executor().block(snapshot)
+    }
+
+    pub fn build(
+        buffer: text::BufferSnapshot,
+        diff_base: Option<Arc<String>>,
+        language: Option<Arc<Language>>,
+        language_registry: Option<Arc<LanguageRegistry>>,
+        cx: &mut App,
+    ) -> impl Future<Output = Self> {
+        let base_text_snapshot = diff_base.as_ref().map(|base_text| {
+            language::Buffer::build_snapshot(
+                Rope::from(base_text.as_str()),
+                language.clone(),
+                language_registry.clone(),
+                cx,
+            )
+        });
+        let base_text_snapshot = cx
+            .background_executor()
+            .spawn(OptionFuture::from(base_text_snapshot));
+
+        let hunks = cx.background_executor().spawn({
+            let buffer = buffer.clone();
+            async move { Self::recalculate_hunks(diff_base, buffer) }
+        });
+
+        async move {
+            let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
+            Self { base_text, hunks }
         }
     }
 
-    pub fn build(diff_base: Option<&str>, buffer: &text::BufferSnapshot) -> Self {
-        let mut tree = SumTree::new(buffer);
+    pub fn build_with_base_buffer(
+        buffer: text::BufferSnapshot,
+        diff_base: Option<Arc<String>>,
+        diff_base_buffer: Option<language::BufferSnapshot>,
+        cx: &App,
+    ) -> impl Future<Output = Self> {
+        cx.background_executor().spawn({
+            let buffer = buffer.clone();
+            async move {
+                let hunks = Self::recalculate_hunks(diff_base, buffer);
+                Self {
+                    hunks,
+                    base_text: diff_base_buffer,
+                }
+            }
+        })
+    }
+
+    fn recalculate_hunks(
+        diff_base: Option<Arc<String>>,
+        buffer: text::BufferSnapshot,
+    ) -> SumTree<InternalDiffHunk> {
+        let mut tree = SumTree::new(&buffer);
 
         if let Some(diff_base) = diff_base {
             let buffer_text = buffer.as_rope().to_string();
-            let patch = Self::diff(diff_base, &buffer_text);
+            let patch = Self::diff(&diff_base, &buffer_text);
 
             // A common case in Zed is that the empty buffer is represented as just a newline,
             // but if we just compute a naive diff you get a "preserved" line in the middle,
@@ -102,32 +178,32 @@ impl BufferDiff {
                         buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
                         diff_base_byte_range: 0..diff_base.len() - 1,
                     },
-                    buffer,
+                    &buffer,
                 );
-                return Self { tree };
+                return tree;
             }
 
             if let Some(patch) = patch {
                 let mut divergence = 0;
                 for hunk_index in 0..patch.num_hunks() {
                     let hunk =
-                        Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
-                    tree.push(hunk, buffer);
+                        Self::process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence);
+                    tree.push(hunk, &buffer);
                 }
             }
         }
 
-        Self { tree }
+        tree
     }
 
     pub fn is_empty(&self) -> bool {
-        self.tree.is_empty()
+        self.hunks.is_empty()
     }
 
     pub fn hunks_in_row_range<'a>(
         &'a self,
         range: Range<u32>,
-        buffer: &'a BufferSnapshot,
+        buffer: &'a text::BufferSnapshot,
     ) -> impl 'a + Iterator<Item = DiffHunk> {
         let start = buffer.anchor_before(Point::new(range.start, 0));
         let end = buffer.anchor_after(Point::new(range.end, 0));
@@ -138,12 +214,12 @@ impl BufferDiff {
     pub fn hunks_intersecting_range<'a>(
         &'a self,
         range: Range<Anchor>,
-        buffer: &'a BufferSnapshot,
+        buffer: &'a text::BufferSnapshot,
     ) -> impl 'a + Iterator<Item = DiffHunk> {
         let range = range.to_offset(buffer);
 
         let mut cursor = self
-            .tree
+            .hunks
             .filter::<_, DiffHunkSummary>(buffer, move |summary| {
                 let summary_range = summary.buffer_range.to_offset(buffer);
                 let before_start = summary_range.end < range.start;
@@ -194,10 +270,10 @@ impl BufferDiff {
     pub fn hunks_intersecting_range_rev<'a>(
         &'a self,
         range: Range<Anchor>,
-        buffer: &'a BufferSnapshot,
+        buffer: &'a text::BufferSnapshot,
     ) -> impl 'a + Iterator<Item = DiffHunk> {
         let mut cursor = self
-            .tree
+            .hunks
             .filter::<_, DiffHunkSummary>(buffer, move |summary| {
                 let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
                 let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
@@ -223,9 +299,13 @@ impl BufferDiff {
         })
     }
 
-    pub fn compare(&self, old: &Self, new_snapshot: &BufferSnapshot) -> Option<Range<Anchor>> {
-        let mut new_cursor = self.tree.cursor::<()>(new_snapshot);
-        let mut old_cursor = old.tree.cursor::<()>(new_snapshot);
+    pub fn compare(
+        &self,
+        old: &Self,
+        new_snapshot: &text::BufferSnapshot,
+    ) -> Option<Range<Anchor>> {
+        let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
+        let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
         old_cursor.next(new_snapshot);
         new_cursor.next(new_snapshot);
         let mut start = None;
@@ -288,15 +368,11 @@ impl BufferDiff {
 
     #[cfg(test)]
     fn clear(&mut self, buffer: &text::BufferSnapshot) {
-        self.tree = SumTree::new(buffer);
-    }
-
-    pub fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
-        *self = Self::build(Some(&diff_base.to_string()), buffer);
+        self.hunks = SumTree::new(buffer);
     }
 
     #[cfg(test)]
-    fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
+    fn hunks<'a>(&'a self, text: &'a text::BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
         let start = text.anchor_before(Point::new(0, 0));
         let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
         self.hunks_intersecting_range(start..end, text)
@@ -391,12 +467,171 @@ impl BufferDiff {
     }
 }
 
+pub struct BufferDiff {
+    pub buffer_id: BufferId,
+    pub snapshot: BufferDiffSnapshot,
+    pub unstaged_diff: Option<Entity<BufferDiff>>,
+}
+
+impl std::fmt::Debug for BufferDiff {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("BufferChangeSet")
+            .field("buffer_id", &self.buffer_id)
+            .field("snapshot", &self.snapshot)
+            .finish()
+    }
+}
+
+pub enum BufferDiffEvent {
+    DiffChanged { changed_range: Range<text::Anchor> },
+    LanguageChanged,
+}
+
+impl EventEmitter<BufferDiffEvent> for BufferDiff {}
+
+impl BufferDiff {
+    pub fn set_state(
+        &mut self,
+        snapshot: BufferDiffSnapshot,
+        buffer: &text::BufferSnapshot,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(base_text) = snapshot.base_text.as_ref() {
+            let changed_range = if Some(base_text.remote_id())
+                != self
+                    .snapshot
+                    .base_text
+                    .as_ref()
+                    .map(|buffer| buffer.remote_id())
+            {
+                Some(text::Anchor::MIN..text::Anchor::MAX)
+            } else {
+                snapshot.compare(&self.snapshot, buffer)
+            };
+            if let Some(changed_range) = changed_range {
+                cx.emit(BufferDiffEvent::DiffChanged { changed_range });
+            }
+        }
+        self.snapshot = snapshot;
+    }
+
+    pub fn diff_hunks_intersecting_range<'a>(
+        &'a self,
+        range: Range<text::Anchor>,
+        buffer_snapshot: &'a text::BufferSnapshot,
+    ) -> impl 'a + Iterator<Item = DiffHunk> {
+        self.snapshot
+            .hunks_intersecting_range(range, buffer_snapshot)
+    }
+
+    pub fn diff_hunks_intersecting_range_rev<'a>(
+        &'a self,
+        range: Range<text::Anchor>,
+        buffer_snapshot: &'a text::BufferSnapshot,
+    ) -> impl 'a + Iterator<Item = DiffHunk> {
+        self.snapshot
+            .hunks_intersecting_range_rev(range, buffer_snapshot)
+    }
+
+    /// Used in cases where the change set isn't derived from git.
+    pub fn set_base_text(
+        &mut self,
+        base_buffer: Entity<language::Buffer>,
+        buffer: text::BufferSnapshot,
+        cx: &mut Context<Self>,
+    ) -> oneshot::Receiver<()> {
+        let (tx, rx) = oneshot::channel();
+        let this = cx.weak_entity();
+        let base_buffer = base_buffer.read(cx);
+        let language_registry = base_buffer.language_registry();
+        let base_buffer = base_buffer.snapshot();
+        let base_text = Arc::new(base_buffer.text());
+
+        let snapshot = BufferDiffSnapshot::build(
+            buffer.clone(),
+            Some(base_text),
+            base_buffer.language().cloned(),
+            language_registry,
+            cx,
+        );
+        let complete_on_drop = util::defer(|| {
+            tx.send(()).ok();
+        });
+        cx.spawn(|_, mut cx| async move {
+            let snapshot = snapshot.await;
+            let Some(this) = this.upgrade() else {
+                return;
+            };
+            this.update(&mut cx, |this, cx| {
+                this.set_state(snapshot, &buffer, cx);
+            })
+            .log_err();
+            drop(complete_on_drop)
+        })
+        .detach();
+        rx
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn base_text_string(&self) -> Option<String> {
+        self.snapshot.base_text.as_ref().map(|buffer| buffer.text())
+    }
+
+    pub fn new(buffer: &Entity<language::Buffer>, cx: &mut App) -> Self {
+        BufferDiff {
+            buffer_id: buffer.read(cx).remote_id(),
+            snapshot: BufferDiffSnapshot::new(&buffer.read(cx)),
+            unstaged_diff: None,
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn new_with_base_text(
+        base_text: &str,
+        buffer: &Entity<language::Buffer>,
+        cx: &mut App,
+    ) -> Self {
+        let mut base_text = base_text.to_owned();
+        text::LineEnding::normalize(&mut base_text);
+        let snapshot = BufferDiffSnapshot::build(
+            buffer.read(cx).text_snapshot(),
+            Some(base_text.into()),
+            None,
+            None,
+            cx,
+        );
+        let snapshot = cx.background_executor().block(snapshot);
+        BufferDiff {
+            buffer_id: buffer.read(cx).remote_id(),
+            snapshot,
+            unstaged_diff: None,
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
+        let base_text = self
+            .snapshot
+            .base_text
+            .as_ref()
+            .map(|base_text| base_text.text());
+        let snapshot = BufferDiffSnapshot::build_with_base_buffer(
+            buffer.clone(),
+            base_text.clone().map(Arc::new),
+            self.snapshot.base_text.clone(),
+            cx,
+        );
+        let snapshot = cx.background_executor().block(snapshot);
+        self.set_state(snapshot, &buffer, cx);
+    }
+}
+
 /// Range (crossing new lines), old, new
 #[cfg(any(test, feature = "test-support"))]
 #[track_caller]
 pub fn assert_hunks<Iter>(
     diff_hunks: Iter,
-    buffer: &BufferSnapshot,
+    buffer: &text::BufferSnapshot,
     diff_base: &str,
     expected_hunks: &[(Range<u32>, &str, &str)],
 ) where
@@ -429,18 +664,18 @@ mod tests {
     use std::assert_eq;
 
     use super::*;
+    use gpui::TestAppContext;
     use text::{Buffer, BufferId};
     use unindent::Unindent as _;
 
-    #[test]
-    fn test_buffer_diff_simple() {
+    #[gpui::test]
+    async fn test_buffer_diff_simple(cx: &mut gpui::TestAppContext) {
         let diff_base = "
             one
             two
             three
         "
         .unindent();
-        let diff_base_rope = Rope::from(diff_base.clone());
 
         let buffer_text = "
             one
@@ -450,8 +685,7 @@ mod tests {
         .unindent();
 
         let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
-        let mut diff = BufferDiff::new(&buffer);
-        diff.update(&diff_base_rope, &buffer);
+        let mut diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx);
         assert_hunks(
             diff.hunks(&buffer),
             &buffer,
@@ -460,7 +694,7 @@ mod tests {
         );
 
         buffer.edit([(0..0, "point five\n")]);
-        diff.update(&diff_base_rope, &buffer);
+        diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx);
         assert_hunks(
             diff.hunks(&buffer),
             &buffer,
@@ -472,9 +706,10 @@ mod tests {
         assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]);
     }
 
-    #[test]
-    fn test_buffer_diff_range() {
-        let diff_base = "
+    #[gpui::test]
+    async fn test_buffer_diff_range(cx: &mut TestAppContext) {
+        let diff_base = Arc::new(
+            "
             one
             two
             three
@@ -486,8 +721,8 @@ mod tests {
             nine
             ten
         "
-        .unindent();
-        let diff_base_rope = Rope::from(diff_base.clone());
+            .unindent(),
+        );
 
         let buffer_text = "
             A
@@ -511,8 +746,17 @@ mod tests {
         .unindent();
 
         let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
-        let mut diff = BufferDiff::new(&buffer);
-        diff.update(&diff_base_rope, &buffer);
+        let diff = cx
+            .update(|cx| {
+                BufferDiffSnapshot::build(
+                    buffer.snapshot(),
+                    Some(diff_base.clone()),
+                    None,
+                    None,
+                    cx,
+                )
+            })
+            .await;
         assert_eq!(diff.hunks(&buffer).count(), 8);
 
         assert_hunks(
@@ -527,8 +771,8 @@ mod tests {
         );
     }
 
-    #[test]
-    fn test_buffer_diff_compare() {
+    #[gpui::test]
+    async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
         let base_text = "
             zero
             one
@@ -557,8 +801,8 @@ mod tests {
 
         let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
 
-        let empty_diff = BufferDiff::new(&buffer);
-        let diff_1 = BufferDiff::build(Some(&base_text), &buffer);
+        let empty_diff = BufferDiffSnapshot::new(&buffer);
+        let diff_1 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
         let range = diff_1.compare(&empty_diff, &buffer).unwrap();
         assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
 
@@ -576,7 +820,7 @@ mod tests {
             "
             .unindent(),
         );
-        let diff_2 = BufferDiff::build(Some(&base_text), &buffer);
+        let diff_2 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
         assert_eq!(None, diff_2.compare(&diff_1, &buffer));
 
         // Edit turns a deletion hunk into a modification.
@@ -593,7 +837,7 @@ mod tests {
             "
             .unindent(),
         );
-        let diff_3 = BufferDiff::build(Some(&base_text), &buffer);
+        let diff_3 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
         let range = diff_3.compare(&diff_2, &buffer).unwrap();
         assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
 
@@ -610,7 +854,7 @@ mod tests {
             "
             .unindent(),
         );
-        let diff_4 = BufferDiff::build(Some(&base_text), &buffer);
+        let diff_4 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx);
         let range = diff_4.compare(&diff_3, &buffer).unwrap();
         assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
 
@@ -628,7 +872,7 @@ mod tests {
             "
             .unindent(),
         );
-        let diff_5 = BufferDiff::build(Some(&base_text), &buffer);
+        let diff_5 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text.clone(), cx);
         let range = diff_5.compare(&diff_4, &buffer).unwrap();
         assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
 
@@ -646,7 +890,7 @@ mod tests {
             "
             .unindent(),
         );
-        let diff_6 = BufferDiff::build(Some(&base_text), &buffer);
+        let diff_6 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text, cx);
         let range = diff_6.compare(&diff_5, &buffer).unwrap();
         assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
     }

crates/editor/Cargo.toml 🔗

@@ -38,6 +38,7 @@ clock.workspace = true
 collections.workspace = true
 convert_case.workspace = true
 db.workspace = true
+diff.workspace = true
 emojis.workspace = true
 file_icons.workspace = true
 futures.workspace = true

crates/editor/src/editor.rs 🔗

@@ -47,7 +47,6 @@ mod signature_help;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
-use ::git::diff::DiffHunkStatus;
 pub(crate) use actions::*;
 pub use actions::{OpenExcerpts, OpenExcerptsSplit};
 use aho_corasick::AhoCorasick;
@@ -74,6 +73,7 @@ use code_context_menus::{
     AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
     CompletionsMenu, ContextMenuOrigin,
 };
+use diff::DiffHunkStatus;
 use git::blame::GitBlame;
 use gpui::{
     div, impl_actions, linear_color_stop, linear_gradient, point, prelude::*, pulsating_between,
@@ -1287,7 +1287,7 @@ impl Editor {
 
         let mut code_action_providers = Vec::new();
         if let Some(project) = project.clone() {
-            get_uncommitted_changes_for_buffer(
+            get_uncommitted_diff_for_buffer(
                 &project,
                 buffer.read(cx).all_buffers(),
                 buffer.clone(),
@@ -6773,11 +6773,12 @@ impl Editor {
         cx: &mut App,
     ) -> Option<()> {
         let buffer = self.buffer.read(cx);
-        let change_set = buffer.change_set_for(hunk.buffer_id)?;
+        let diff = buffer.diff_for(hunk.buffer_id)?;
         let buffer = buffer.buffer(hunk.buffer_id)?;
         let buffer = buffer.read(cx);
-        let original_text = change_set
+        let original_text = diff
             .read(cx)
+            .snapshot
             .base_text
             .as_ref()?
             .as_rope()
@@ -13731,9 +13732,9 @@ impl Editor {
             } => {
                 self.tasks_update_task = Some(self.refresh_runnables(window, cx));
                 let buffer_id = buffer.read(cx).remote_id();
-                if self.buffer.read(cx).change_set_for(buffer_id).is_none() {
+                if self.buffer.read(cx).diff_for(buffer_id).is_none() {
                     if let Some(project) = &self.project {
-                        get_uncommitted_changes_for_buffer(
+                        get_uncommitted_diff_for_buffer(
                             project,
                             [buffer.clone()],
                             self.buffer.clone(),
@@ -14492,7 +14493,7 @@ impl Editor {
     }
 }
 
-fn get_uncommitted_changes_for_buffer(
+fn get_uncommitted_diff_for_buffer(
     project: &Entity<Project>,
     buffers: impl IntoIterator<Item = Entity<Buffer>>,
     buffer: Entity<MultiBuffer>,
@@ -14501,15 +14502,15 @@ fn get_uncommitted_changes_for_buffer(
     let mut tasks = Vec::new();
     project.update(cx, |project, cx| {
         for buffer in buffers {
-            tasks.push(project.open_uncommitted_changes(buffer.clone(), cx))
+            tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
         }
     });
     cx.spawn(|mut cx| async move {
-        let change_sets = futures::future::join_all(tasks).await;
+        let diffs = futures::future::join_all(tasks).await;
         buffer
             .update(&mut cx, |buffer, cx| {
-                for change_set in change_sets.into_iter().flatten() {
-                    buffer.add_change_set(change_set, cx);
+                for diff in diffs.into_iter().flatten() {
+                    buffer.add_diff(diff, cx);
                 }
             })
             .ok();

crates/editor/src/editor_tests.rs 🔗

@@ -7,6 +7,7 @@ use crate::{
     },
     JoinLines,
 };
+use diff::{BufferDiff, DiffHunkStatus};
 use futures::StreamExt;
 use gpui::{
     div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
@@ -26,7 +27,7 @@ use language_settings::{Formatter, FormatterList, IndentGuideSettings};
 use multi_buffer::IndentGuide;
 use parking_lot::Mutex;
 use pretty_assertions::{assert_eq, assert_ne};
-use project::{buffer_store::BufferChangeSet, FakeFs};
+use project::FakeFs;
 use project::{
     lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
     project_settings::{LspSettings, ProjectSettings},
@@ -12440,11 +12441,10 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
             (buffer_2.clone(), base_text_2),
             (buffer_3.clone(), base_text_3),
         ] {
-            let change_set =
-                cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx));
+            let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
             editor
                 .buffer
-                .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
+                .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
         }
     });
     cx.executor().run_until_parked();
@@ -13134,11 +13134,10 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
                 (buffer_2.clone(), file_2_old),
                 (buffer_3.clone(), file_3_old),
             ] {
-                let change_set =
-                    cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx));
+                let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
                 editor
                     .buffer
-                    .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
+                    .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
             }
         })
         .unwrap();
@@ -13251,10 +13250,10 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
     });
     editor
         .update(cx, |editor, _window, cx| {
-            let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base, &buffer, cx));
+            let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
             editor
                 .buffer
-                .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
+                .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
         })
         .unwrap();
 
@@ -14420,11 +14419,10 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContex
 
         editor.buffer().update(cx, |multibuffer, cx| {
             let buffer = multibuffer.as_singleton().unwrap();
-            let change_set =
-                cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
+            let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
 
             multibuffer.set_all_diff_hunks_expanded(cx);
-            multibuffer.add_change_set(change_set, cx);
+            multibuffer.add_diff(diff, cx);
 
             buffer.read(cx).remote_id()
         })

crates/editor/src/element.rs 🔗

@@ -26,8 +26,9 @@ use crate::{
 };
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap, HashSet};
+use diff::DiffHunkStatus;
 use file_icons::FileIcons;
-use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
+use git::{blame::BlameEntry, Oid};
 use gpui::{
     anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
     relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,

crates/editor/src/hunk_diff.rs 🔗

@@ -56,7 +56,7 @@ pub(super) struct ExpandedHunk {
 pub(crate) struct DiffMapSnapshot(TreeMap<BufferId, git::diff::BufferDiff>);
 
 pub(crate) struct DiffBaseState {
-    pub(crate) change_set: Model<BufferChangeSet>,
+    pub(crate) diff: Model<BufferChangeSet>,
     pub(crate) last_version: Option<usize>,
     _subscription: Subscription,
 }
@@ -80,38 +80,29 @@ impl DiffMap {
         self.snapshot.clone()
     }
 
-    pub fn add_change_set(
+    pub fn add_diff(
         &mut self,
-        change_set: Model<BufferChangeSet>,
+        diff: Model<BufferChangeSet>,
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
-        let buffer_id = change_set.read(cx).buffer_id;
+        let buffer_id = diff.read(cx).buffer_id;
         self.snapshot
             .0
-            .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
+            .insert(buffer_id, diff.read(cx).diff_to_buffer.clone());
         self.diff_bases.insert(
             buffer_id,
             DiffBaseState {
                 last_version: None,
-                _subscription: cx.observe_in(
-                    &change_set,
-                    window,
-                    move |editor, change_set, window, cx| {
-                        editor
-                            .diff_map
-                            .snapshot
-                            .0
-                            .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
-                        Editor::sync_expanded_diff_hunks(
-                            &mut editor.diff_map,
-                            buffer_id,
-                            window,
-                            cx,
-                        );
-                    },
-                ),
-                change_set,
+                _subscription: cx.observe_in(&diff, window, move |editor, diff, window, cx| {
+                    editor
+                        .diff_map
+                        .snapshot
+                        .0
+                        .insert(buffer_id, diff.read(cx).diff_to_buffer.clone());
+                    Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, window, cx);
+                }),
+                diff,
             },
         );
         Editor::sync_expanded_diff_hunks(self, buffer_id, window, cx);
@@ -399,7 +390,7 @@ impl Editor {
             self.diff_map
                 .diff_bases
                 .get(&buffer_id)?
-                .change_set
+                .diff
                 .read(cx)
                 .base_text
                 .clone()
@@ -953,12 +944,12 @@ impl Editor {
         let mut diff_base_buffer = None;
         let mut diff_base_buffer_unchanged = true;
         if let Some(diff_base_state) = diff_base_state {
-            diff_base_state.change_set.update(cx, |change_set, _| {
-                if diff_base_state.last_version != Some(change_set.base_text_version) {
-                    diff_base_state.last_version = Some(change_set.base_text_version);
+            diff_base_state.diff.update(cx, |diff, _| {
+                if diff_base_state.last_version != Some(diff.base_text_version) {
+                    diff_base_state.last_version = Some(diff.base_text_version);
                     diff_base_buffer_unchanged = false;
                 }
-                diff_base_buffer = change_set.base_text.clone();
+                diff_base_buffer = diff.base_text.clone();
             })
         }
 
@@ -1498,14 +1489,14 @@ mod tests {
                     (buffer_1.clone(), diff_base_1),
                     (buffer_2.clone(), diff_base_2),
                 ] {
-                    let change_set = cx.new(|cx| {
+                    let diff = cx.new(|cx| {
                         BufferChangeSet::new_with_base_text(
                             diff_base.to_string(),
                             buffer.read(cx).text_snapshot(),
                             cx,
                         )
                     });
-                    editor.diff_map.add_change_set(change_set, window, cx)
+                    editor.diff_map.add_diff(diff, window, cx)
                 }
             })
             .unwrap();

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -1,10 +1,11 @@
 use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
 use collections::HashSet;
+use diff::BufferDiff;
 use futures::{channel::mpsc, future::join_all};
 use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
 use language::{Buffer, BufferEvent, Capability};
 use multi_buffer::{ExcerptRange, MultiBuffer};
-use project::{buffer_store::BufferChangeSet, Project};
+use project::Project;
 use smol::stream::StreamExt;
 use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
 use text::ToOffset;
@@ -106,12 +107,10 @@ impl ProposedChangesEditor {
                                     let buffer = buffer.read(cx);
                                     let base_buffer = buffer.base_buffer()?;
                                     let buffer = buffer.text_snapshot();
-                                    let change_set = this
-                                        .multibuffer
-                                        .read(cx)
-                                        .change_set_for(buffer.remote_id())?;
-                                    Some(change_set.update(cx, |change_set, cx| {
-                                        change_set.set_base_text(base_buffer.clone(), buffer, cx)
+                                    let diff =
+                                        this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
+                                    Some(diff.update(cx, |diff, cx| {
+                                        diff.set_base_text(base_buffer.clone(), buffer, cx)
                                     }))
                                 })
                                 .collect::<Vec<_>>()
@@ -172,7 +171,7 @@ impl ProposedChangesEditor {
         });
 
         let mut buffer_entries = Vec::new();
-        let mut new_change_sets = Vec::new();
+        let mut new_diffs = Vec::new();
         for location in locations {
             let branch_buffer;
             if let Some(ix) = self
@@ -185,14 +184,14 @@ impl ProposedChangesEditor {
                 buffer_entries.push(entry);
             } else {
                 branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
-                new_change_sets.push(cx.new(|cx| {
-                    let mut change_set = BufferChangeSet::new(&branch_buffer, cx);
-                    let _ = change_set.set_base_text(
+                new_diffs.push(cx.new(|cx| {
+                    let mut diff = BufferDiff::new(&branch_buffer, cx);
+                    let _ = diff.set_base_text(
                         location.buffer.clone(),
                         branch_buffer.read(cx).text_snapshot(),
                         cx,
                     );
-                    change_set
+                    diff
                 }));
                 buffer_entries.push(BufferEntry {
                     branch: branch_buffer.clone(),
@@ -217,8 +216,8 @@ impl ProposedChangesEditor {
         self.editor.update(cx, |editor, cx| {
             editor.change_selections(None, window, cx, |selections| selections.refresh());
             editor.buffer.update(cx, |buffer, cx| {
-                for change_set in new_change_sets {
-                    buffer.add_change_set(change_set, cx)
+                for diff in new_diffs {
+                    buffer.add_diff(diff, cx)
                 }
             })
         });

crates/editor/src/test/editor_test_context.rs 🔗

@@ -3,8 +3,9 @@ use crate::{
     RowExt,
 };
 use collections::BTreeMap;
+use diff::DiffHunkStatus;
 use futures::Future;
-use git::diff::DiffHunkStatus;
+
 use gpui::{
     prelude::*, AnyWindowHandle, App, Context, Entity, Focusable as _, Keystroke, Pixels, Point,
     VisualTestContext, Window, WindowHandle,

crates/git/src/git.rs 🔗

@@ -1,6 +1,5 @@
 pub mod blame;
 pub mod commit;
-pub mod diff;
 mod hosting_provider;
 mod remote;
 pub mod repository;

crates/git_ui/Cargo.toml 🔗

@@ -16,6 +16,7 @@ path = "src/git_ui.rs"
 anyhow.workspace = true
 collections.workspace = true
 db.workspace = true
+diff.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 futures.workspace = true

crates/git_ui/src/project_diff.rs 🔗

@@ -2,6 +2,7 @@ use std::any::{Any, TypeId};
 
 use anyhow::Result;
 use collections::HashSet;
+use diff::BufferDiff;
 use editor::{scroll::Autoscroll, Editor, EditorEvent};
 use feature_flags::FeatureFlagViewExt;
 use futures::StreamExt;
@@ -11,7 +12,7 @@ use gpui::{
 };
 use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point};
 use multi_buffer::{MultiBuffer, PathKey};
-use project::{buffer_store::BufferChangeSet, git::GitState, Project, ProjectPath};
+use project::{git::GitState, Project, ProjectPath};
 use theme::ActiveTheme;
 use ui::prelude::*;
 use util::ResultExt as _;
@@ -43,7 +44,7 @@ pub(crate) struct ProjectDiff {
 struct DiffBuffer {
     path_key: PathKey,
     buffer: Entity<Buffer>,
-    change_set: Entity<BufferChangeSet>,
+    diff: Entity<BufferDiff>,
 }
 
 const CONFLICT_NAMESPACE: &'static str = "0";
@@ -285,13 +286,13 @@ impl ProjectDiff {
                     let buffer = load_buffer.await?;
                     let changes = project
                         .update(&mut cx, |project, cx| {
-                            project.open_uncommitted_changes(buffer.clone(), cx)
+                            project.open_uncommitted_diff(buffer.clone(), cx)
                         })?
                         .await?;
                     Ok(DiffBuffer {
                         path_key,
                         buffer,
-                        change_set: changes,
+                        diff: changes,
                     })
                 }));
             }
@@ -312,15 +313,14 @@ impl ProjectDiff {
     ) {
         let path_key = diff_buffer.path_key;
         let buffer = diff_buffer.buffer;
-        let change_set = diff_buffer.change_set;
+        let diff = diff_buffer.diff;
 
         let snapshot = buffer.read(cx).snapshot();
-        let change_set = change_set.read(cx);
-        let diff_hunk_ranges = if change_set.base_text.is_none() {
+        let diff = diff.read(cx);
+        let diff_hunk_ranges = if diff.snapshot.base_text.is_none() {
             vec![Point::zero()..snapshot.max_point()]
         } else {
-            change_set
-                .diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot)
+            diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot)
                 .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
                 .collect::<Vec<_>>()
         };

crates/multi_buffer/Cargo.toml 🔗

@@ -14,9 +14,10 @@ doctest = false
 
 [features]
 test-support = [
-    "text/test-support",
-    "language/test-support",
+    "diff/test-support",
     "gpui/test-support",
+    "language/test-support",
+    "text/test-support",
     "util/test-support",
 ]
 
@@ -25,15 +26,14 @@ anyhow.workspace = true
 clock.workspace = true
 collections.workspace = true
 ctor.workspace = true
+diff.workspace = true
 env_logger.workspace = true
 futures.workspace = true
-git.workspace = true
 gpui.workspace = true
 itertools.workspace = true
 language.workspace = true
 log.workspace = true
 parking_lot.workspace = true
-project.workspace = true
 rand.workspace = true
 rope.workspace = true
 smol.workspace = true
@@ -47,12 +47,13 @@ tree-sitter.workspace = true
 util.workspace = true
 
 [dev-dependencies]
+diff = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
+indoc.workspace = true
 language = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
 project = { workspace = true, features = ["test-support"] }
 rand.workspace = true
 settings = { workspace = true, features = ["test-support"] }
 text = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
-pretty_assertions.workspace = true
-indoc.workspace = true

crates/multi_buffer/src/anchor.rs 🔗

@@ -70,15 +70,15 @@ impl Anchor {
                 return text_cmp;
             }
             if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() {
-                if let Some(diff_base) = snapshot.diffs.get(&excerpt.buffer_id) {
-                    let self_anchor = self
-                        .diff_base_anchor
-                        .filter(|a| diff_base.base_text.can_resolve(a));
-                    let other_anchor = other
-                        .diff_base_anchor
-                        .filter(|a| diff_base.base_text.can_resolve(a));
+                if let Some(base_text) = snapshot
+                    .diffs
+                    .get(&excerpt.buffer_id)
+                    .and_then(|diff| diff.base_text.as_ref())
+                {
+                    let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a));
+                    let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a));
                     return match (self_anchor, other_anchor) {
-                        (Some(a), Some(b)) => a.cmp(&b, &diff_base.base_text),
+                        (Some(a), Some(b)) => a.cmp(&b, base_text),
                         (Some(_), None) => match other.text_anchor.bias {
                             Bias::Left => Ordering::Greater,
                             Bias::Right => Ordering::Less,
@@ -107,9 +107,13 @@ impl Anchor {
                     excerpt_id: self.excerpt_id,
                     text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
                     diff_base_anchor: self.diff_base_anchor.map(|a| {
-                        if let Some(base) = snapshot.diffs.get(&excerpt.buffer_id) {
-                            if a.buffer_id == Some(base.base_text.remote_id()) {
-                                return a.bias_left(&base.base_text);
+                        if let Some(base_text) = snapshot
+                            .diffs
+                            .get(&excerpt.buffer_id)
+                            .and_then(|diff| diff.base_text.as_ref())
+                        {
+                            if a.buffer_id == Some(base_text.remote_id()) {
+                                return a.bias_left(base_text);
                             }
                         }
                         a
@@ -128,9 +132,13 @@ impl Anchor {
                     excerpt_id: self.excerpt_id,
                     text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
                     diff_base_anchor: self.diff_base_anchor.map(|a| {
-                        if let Some(base) = snapshot.diffs.get(&excerpt.buffer_id) {
-                            if a.buffer_id == Some(base.base_text.remote_id()) {
-                                return a.bias_right(&base.base_text);
+                        if let Some(base_text) = snapshot
+                            .diffs
+                            .get(&excerpt.buffer_id)
+                            .and_then(|diff| diff.base_text.as_ref())
+                        {
+                            if a.buffer_id == Some(base_text.remote_id()) {
+                                return a.bias_right(&base_text);
                             }
                         }
                         a

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -9,8 +9,8 @@ pub use position::{TypedOffset, TypedPoint, TypedRow};
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet};
+use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkStatus};
 use futures::{channel::mpsc, SinkExt};
-use git::diff::DiffHunkStatus;
 use gpui::{App, Context, Entity, EntityId, EventEmitter, Task};
 use itertools::Itertools;
 use language::{
@@ -21,7 +21,7 @@ use language::{
     TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions,
     Unclipped,
 };
-use project::buffer_store::{BufferChangeSet, BufferChangeSetEvent};
+
 use rope::DimensionPair;
 use smallvec::SmallVec;
 use smol::future::yield_now;
@@ -68,7 +68,7 @@ pub struct MultiBuffer {
     buffers: RefCell<HashMap<BufferId, BufferState>>,
     // only used by consumers using `set_excerpts_for_buffer`
     buffers_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
-    diff_bases: HashMap<BufferId, ChangeSetState>,
+    diffs: HashMap<BufferId, DiffState>,
     all_diff_hunks_expanded: bool,
     subscriptions: Topic,
     /// If true, the multi-buffer only contains a single [`Buffer`] and a single [`Excerpt`]
@@ -215,23 +215,21 @@ struct BufferState {
     _subscriptions: [gpui::Subscription; 2],
 }
 
-struct ChangeSetState {
-    change_set: Entity<BufferChangeSet>,
+struct DiffState {
+    diff: Entity<BufferDiff>,
     _subscription: gpui::Subscription,
 }
 
-impl ChangeSetState {
-    fn new(change_set: Entity<BufferChangeSet>, cx: &mut Context<MultiBuffer>) -> Self {
-        ChangeSetState {
-            _subscription: cx.subscribe(&change_set, |this, change_set, event, cx| match event {
-                BufferChangeSetEvent::DiffChanged { changed_range } => {
-                    this.buffer_diff_changed(change_set, changed_range.clone(), cx)
-                }
-                BufferChangeSetEvent::LanguageChanged => {
-                    this.buffer_diff_language_changed(change_set, cx)
+impl DiffState {
+    fn new(diff: Entity<BufferDiff>, cx: &mut Context<MultiBuffer>) -> Self {
+        DiffState {
+            _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event {
+                BufferDiffEvent::DiffChanged { changed_range } => {
+                    this.buffer_diff_changed(diff, changed_range.clone(), cx)
                 }
+                BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
             }),
-            change_set,
+            diff,
         }
     }
 }
@@ -242,7 +240,7 @@ pub struct MultiBufferSnapshot {
     singleton: bool,
     excerpts: SumTree<Excerpt>,
     excerpt_ids: SumTree<ExcerptIdMapping>,
-    diffs: TreeMap<BufferId, DiffSnapshot>,
+    diffs: TreeMap<BufferId, BufferDiffSnapshot>,
     pub diff_transforms: SumTree<DiffTransform>,
     trailing_excerpt_update_count: usize,
     non_text_state_update_count: usize,
@@ -268,12 +266,6 @@ pub enum DiffTransform {
     },
 }
 
-#[derive(Clone)]
-struct DiffSnapshot {
-    diff: git::diff::BufferDiff,
-    base_text: language::BufferSnapshot,
-}
-
 #[derive(Clone)]
 pub struct ExcerptInfo {
     pub id: ExcerptId,
@@ -318,7 +310,7 @@ pub struct RowInfo {
     pub buffer_id: Option<BufferId>,
     pub buffer_row: Option<u32>,
     pub multibuffer_row: Option<MultiBufferRow>,
-    pub diff_status: Option<git::diff::DiffHunkStatus>,
+    pub diff_status: Option<diff::DiffHunkStatus>,
 }
 
 /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`].
@@ -397,7 +389,7 @@ pub struct MultiBufferRows<'a> {
 pub struct MultiBufferChunks<'a> {
     excerpts: Cursor<'a, Excerpt, ExcerptOffset>,
     diff_transforms: Cursor<'a, DiffTransform, (usize, ExcerptOffset)>,
-    diffs: &'a TreeMap<BufferId, DiffSnapshot>,
+    diffs: &'a TreeMap<BufferId, BufferDiffSnapshot>,
     diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>,
     buffer_chunk: Option<Chunk<'a>>,
     range: Range<usize>,
@@ -431,7 +423,7 @@ pub struct ReversedMultiBufferBytes<'a> {
 struct MultiBufferCursor<'a, D: TextDimension> {
     excerpts: Cursor<'a, Excerpt, ExcerptDimension<D>>,
     diff_transforms: Cursor<'a, DiffTransform, (OutputDimension<D>, ExcerptDimension<D>)>,
-    diffs: &'a TreeMap<BufferId, DiffSnapshot>,
+    diffs: &'a TreeMap<BufferId, BufferDiffSnapshot>,
     cached_region: Option<MultiBufferRegion<'a, D>>,
 }
 
@@ -517,7 +509,7 @@ impl MultiBuffer {
                 ..MultiBufferSnapshot::default()
             }),
             buffers: RefCell::default(),
-            diff_bases: HashMap::default(),
+            diffs: HashMap::default(),
             all_diff_hunks_expanded: false,
             subscriptions: Topic::default(),
             singleton: false,
@@ -539,7 +531,7 @@ impl MultiBuffer {
             snapshot: Default::default(),
             buffers: Default::default(),
             buffers_by_path: Default::default(),
-            diff_bases: HashMap::default(),
+            diffs: HashMap::default(),
             all_diff_hunks_expanded: false,
             subscriptions: Default::default(),
             singleton: false,
@@ -573,17 +565,14 @@ impl MultiBuffer {
             );
         }
         let mut diff_bases = HashMap::default();
-        for (buffer_id, change_set_state) in self.diff_bases.iter() {
-            diff_bases.insert(
-                *buffer_id,
-                ChangeSetState::new(change_set_state.change_set.clone(), new_cx),
-            );
+        for (buffer_id, diff) in self.diffs.iter() {
+            diff_bases.insert(*buffer_id, DiffState::new(diff.diff.clone(), new_cx));
         }
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
             buffers: RefCell::new(buffers),
             buffers_by_path: Default::default(),
-            diff_bases,
+            diffs: diff_bases,
             all_diff_hunks_expanded: self.all_diff_hunks_expanded,
             subscriptions: Default::default(),
             singleton: self.singleton,
@@ -2152,71 +2141,49 @@ impl MultiBuffer {
         });
     }
 
-    fn buffer_diff_language_changed(
-        &mut self,
-        change_set: Entity<BufferChangeSet>,
-        cx: &mut Context<Self>,
-    ) {
+    fn buffer_diff_language_changed(&mut self, diff: Entity<BufferDiff>, cx: &mut Context<Self>) {
         self.sync(cx);
         let mut snapshot = self.snapshot.borrow_mut();
-        let change_set = change_set.read(cx);
-        let buffer_id = change_set.buffer_id;
-        let base_text = change_set.base_text.clone();
-        let diff = change_set.diff_to_buffer.clone();
-        if let Some(base_text) = base_text {
-            snapshot.diffs.insert(
-                buffer_id,
-                DiffSnapshot {
-                    diff: diff.clone(),
-                    base_text,
-                },
-            );
-        } else {
-            snapshot.diffs.remove(&buffer_id);
-        }
+        let diff = diff.read(cx);
+        let buffer_id = diff.buffer_id;
+        let diff = diff.snapshot.clone();
+        snapshot.diffs.insert(buffer_id, diff);
     }
 
     fn buffer_diff_changed(
         &mut self,
-        change_set: Entity<BufferChangeSet>,
+        diff: Entity<BufferDiff>,
         range: Range<text::Anchor>,
         cx: &mut Context<Self>,
     ) {
-        let change_set = change_set.read(cx);
-        let buffer_id = change_set.buffer_id;
-        let diff = change_set.diff_to_buffer.clone();
-        let base_text = change_set.base_text.clone();
         self.sync(cx);
-        let mut snapshot = self.snapshot.borrow_mut();
-        let base_text_changed = snapshot
-            .diffs
-            .get(&buffer_id)
-            .map_or(true, |diff_snapshot| {
-                change_set.base_text.as_ref().map_or(true, |base_text| {
-                    base_text.remote_id() != diff_snapshot.base_text.remote_id()
-                })
-            });
 
-        if let Some(base_text) = base_text {
-            snapshot.diffs.insert(
-                buffer_id,
-                DiffSnapshot {
-                    diff: diff.clone(),
-                    base_text,
-                },
-            );
-        } else if self.all_diff_hunks_expanded {
-            let base_text = Buffer::build_empty_snapshot(cx);
-            snapshot.diffs.insert(
-                buffer_id,
-                DiffSnapshot {
-                    diff: git::diff::BufferDiff::new_with_single_insertion(&base_text),
-                    base_text,
-                },
-            );
-        } else {
-            snapshot.diffs.remove(&buffer_id);
+        let diff = diff.read(cx);
+        let buffer_id = diff.buffer_id;
+        let mut diff = diff.snapshot.clone();
+        if diff.base_text.is_none() && self.all_diff_hunks_expanded {
+            diff = BufferDiffSnapshot::new_with_single_insertion(cx);
         }
+
+        let mut snapshot = self.snapshot.borrow_mut();
+        let base_text_changed =
+            snapshot
+                .diffs
+                .get(&buffer_id)
+                .map_or(true, |diff_snapshot| {
+                    match (&diff_snapshot.base_text, &diff.base_text) {
+                        (None, None) => false,
+                        (None, Some(_)) => true,
+                        (Some(_), None) => true,
+                        (Some(old), Some(new)) => {
+                            let (old_id, old_empty) = (old.remote_id(), old.is_empty());
+                            let (new_id, new_empty) = (new.remote_id(), new.is_empty());
+                            new_id != old_id && (!new_empty || !old_empty)
+                        }
+                    }
+                });
+        snapshot.diffs.insert(buffer_id, diff);
+
         let buffers = self.buffers.borrow();
         let Some(buffer_state) = buffers.get(&buffer_id) else {
             return;
@@ -2352,17 +2319,14 @@ impl MultiBuffer {
         self.as_singleton().unwrap().read(cx).is_parsing()
     }
 
-    pub fn add_change_set(&mut self, change_set: Entity<BufferChangeSet>, cx: &mut Context<Self>) {
-        let buffer_id = change_set.read(cx).buffer_id;
-        self.buffer_diff_changed(change_set.clone(), text::Anchor::MIN..text::Anchor::MAX, cx);
-        self.diff_bases
-            .insert(buffer_id, ChangeSetState::new(change_set, cx));
+    pub fn add_diff(&mut self, diff: Entity<BufferDiff>, cx: &mut Context<Self>) {
+        let buffer_id = diff.read(cx).buffer_id;
+        self.buffer_diff_changed(diff.clone(), text::Anchor::MIN..text::Anchor::MAX, cx);
+        self.diffs.insert(buffer_id, DiffState::new(diff, cx));
     }
 
-    pub fn change_set_for(&self, buffer_id: BufferId) -> Option<Entity<BufferChangeSet>> {
-        self.diff_bases
-            .get(&buffer_id)
-            .map(|state| state.change_set.clone())
+    pub fn diff_for(&self, buffer_id: BufferId) -> Option<Entity<BufferDiff>> {
+        self.diffs.get(&buffer_id).map(|state| state.diff.clone())
     }
 
     pub fn expand_diff_hunks(&mut self, ranges: Vec<Range<Anchor>>, cx: &mut Context<Self>) {
@@ -2920,9 +2884,11 @@ impl MultiBuffer {
         while let Some(excerpt) = excerpts.item() {
             // Recompute the expanded hunks in the portion of the excerpt that
             // intersects the edit.
-            if let Some(diff_state) = snapshot.diffs.get(&excerpt.buffer_id) {
-                let diff = &diff_state.diff;
-                let base_text = &diff_state.base_text;
+            if let Some((diff, base_text)) = snapshot
+                .diffs
+                .get(&excerpt.buffer_id)
+                .and_then(|diff| Some((diff, diff.base_text.as_ref()?)))
+            {
                 let buffer = &excerpt.buffer;
                 let excerpt_start = *excerpts.start();
                 let excerpt_end = excerpt_start + ExcerptOffset::new(excerpt.text_summary.len);
@@ -3445,8 +3411,7 @@ impl MultiBufferSnapshot {
             let buffer_start = buffer.anchor_before(buffer_range.start);
             let buffer_end = buffer.anchor_after(buffer_range.end);
             Some(
-                diff.diff
-                    .hunks_intersecting_range(buffer_start..buffer_end, buffer)
+                diff.hunks_intersecting_range(buffer_start..buffer_end, buffer)
                     .map(|hunk| {
                         (
                             Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
@@ -3782,8 +3747,8 @@ impl MultiBufferSnapshot {
             let buffer_end = excerpt.buffer.anchor_before(buffer_offset);
             let buffer_end_row = buffer_end.to_point(&excerpt.buffer).row;
 
-            if let Some(diff_state) = self.diffs.get(&excerpt.buffer_id) {
-                for hunk in diff_state.diff.hunks_intersecting_range_rev(
+            if let Some(diff) = self.diffs.get(&excerpt.buffer_id) {
+                for hunk in diff.hunks_intersecting_range_rev(
                     excerpt.range.context.start..buffer_end,
                     &excerpt.buffer,
                 ) {
@@ -3851,7 +3816,7 @@ impl MultiBufferSnapshot {
     }
 
     pub fn has_diff_hunks(&self) -> bool {
-        self.diffs.values().any(|diff| !diff.diff.is_empty())
+        self.diffs.values().any(|diff| !diff.is_empty())
     }
 
     pub fn surrounding_word<T: ToOffset>(
@@ -4313,7 +4278,11 @@ impl MultiBufferSnapshot {
             } => {
                 let buffer_start = base_text_byte_range.start + start_overshoot;
                 let mut buffer_end = base_text_byte_range.start + end_overshoot;
-                let Some(buffer_diff) = self.diffs.get(buffer_id) else {
+                let Some(base_text) = self
+                    .diffs
+                    .get(buffer_id)
+                    .and_then(|diff| diff.base_text.as_ref())
+                else {
                     panic!("{:?} is in non-existent deleted hunk", range.start)
                 };
 
@@ -4323,9 +4292,8 @@ impl MultiBufferSnapshot {
                     buffer_end -= 1;
                 }
 
-                let mut summary = buffer_diff
-                    .base_text
-                    .text_summary_for_range::<D, _>(buffer_start..buffer_end);
+                let mut summary =
+                    base_text.text_summary_for_range::<D, _>(buffer_start..buffer_end);
 
                 if include_trailing_newline {
                     summary.add_assign(&D::from_text_summary(&TextSummary::newline()))
@@ -4362,12 +4330,15 @@ impl MultiBufferSnapshot {
                 ..
             } => {
                 let buffer_end = base_text_byte_range.start + overshoot;
-                let Some(buffer_diff) = self.diffs.get(buffer_id) else {
-                    panic!("{:?} is in non-extant deleted hunk", range.end)
+                let Some(base_text) = self
+                    .diffs
+                    .get(buffer_id)
+                    .and_then(|diff| diff.base_text.as_ref())
+                else {
+                    panic!("{:?} is in non-existent deleted hunk", range.end)
                 };
 
-                let mut suffix = buffer_diff
-                    .base_text
+                let mut suffix = base_text
                     .text_summary_for_range::<D, _>(base_text_byte_range.start..buffer_end);
                 if *has_trailing_newline && buffer_end == base_text_byte_range.end + 1 {
                     suffix.add_assign(&D::from_text_summary(&TextSummary::newline()))
@@ -4467,14 +4438,18 @@ impl MultiBufferSnapshot {
                 }) => {
                     let mut in_deleted_hunk = false;
                     if let Some(diff_base_anchor) = &anchor.diff_base_anchor {
-                        if let Some(diff) = self.diffs.get(buffer_id) {
-                            if diff.base_text.can_resolve(&diff_base_anchor) {
-                                let base_text_offset = diff_base_anchor.to_offset(&diff.base_text);
+                        if let Some(base_text) = self
+                            .diffs
+                            .get(buffer_id)
+                            .and_then(|diff| diff.base_text.as_ref())
+                        {
+                            if base_text.can_resolve(&diff_base_anchor) {
+                                let base_text_offset = diff_base_anchor.to_offset(&base_text);
                                 if base_text_offset >= base_text_byte_range.start
                                     && base_text_offset <= base_text_byte_range.end
                                 {
-                                    let position_in_hunk =
-                                        diff.base_text.text_summary_for_range::<D, _>(
+                                    let position_in_hunk = base_text
+                                        .text_summary_for_range::<D, _>(
                                             base_text_byte_range.start..base_text_offset,
                                         );
                                     position.add_assign(&position_in_hunk);
@@ -4800,15 +4775,17 @@ impl MultiBufferSnapshot {
             ..
         }) = diff_transforms.item()
         {
-            let diff_base = self.diffs.get(buffer_id).expect("missing diff base");
+            let base_text = self
+                .diffs
+                .get(buffer_id)
+                .and_then(|diff| diff.base_text.as_ref())
+                .expect("missing diff base");
             if offset_in_transform > base_text_byte_range.len() {
                 debug_assert!(*has_trailing_newline);
                 bias = Bias::Right;
             } else {
                 diff_base_anchor = Some(
-                    diff_base
-                        .base_text
-                        .anchor_at(base_text_byte_range.start + offset_in_transform, bias),
+                    base_text.anchor_at(base_text_byte_range.start + offset_in_transform, bias),
                 );
                 bias = Bias::Left;
             }
@@ -6144,7 +6121,7 @@ where
                 ..
             } => {
                 let diff = self.diffs.get(&buffer_id)?;
-                let buffer = &diff.base_text;
+                let buffer = diff.base_text.as_ref()?;
                 let mut rope_cursor = buffer.as_rope().cursor(0);
                 let buffer_start = rope_cursor.summary::<D>(base_text_byte_range.start);
                 let buffer_range_len = rope_cursor.summary::<D>(base_text_byte_range.end);
@@ -7186,7 +7163,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> {
                     }
                     chunks
                 } else {
-                    let base_buffer = &self.diffs.get(&buffer_id)?.base_text;
+                    let base_buffer = &self.diffs.get(&buffer_id)?.base_text.as_ref()?;
                     base_buffer.chunks(base_text_start..base_text_end, self.language_aware)
                 };
 

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -1,5 +1,5 @@
 use super::*;
-use git::diff::DiffHunkStatus;
+use diff::DiffHunkStatus;
 use gpui::{App, TestAppContext};
 use indoc::indoc;
 use language::{Buffer, Rope};
@@ -361,11 +361,9 @@ fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
     let base_text = "one\ntwo\nthree\n";
     let text = "one\nthree\n";
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
-    multibuffer.update(cx, |multibuffer, cx| {
-        multibuffer.add_change_set(change_set, cx)
-    });
+    multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx));
 
     let (before, after) = multibuffer.update(cx, |multibuffer, cx| {
         let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0));
@@ -405,14 +403,14 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
     let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
     let text = "one\nfour\nseven\n";
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
     let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
         (multibuffer.snapshot(cx), multibuffer.subscribe())
     });
 
     multibuffer.update(cx, |multibuffer, cx| {
-        multibuffer.add_change_set(change_set, cx);
+        multibuffer.add_diff(diff, cx);
         multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
     });
 
@@ -498,11 +496,11 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
     let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
     let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(&base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 
     let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
-        multibuffer.add_change_set(change_set.clone(), cx);
+        multibuffer.add_diff(diff.clone(), cx);
         (multibuffer.snapshot(cx), multibuffer.subscribe())
     });
 
@@ -979,10 +977,10 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
     let buffer = cx.new(|cx| Buffer::local("", cx));
     let base_text = "a\nb\nc";
 
-    let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
     multibuffer.update(cx, |multibuffer, cx| {
         multibuffer.set_all_diff_hunks_expanded(cx);
-        multibuffer.add_change_set(change_set.clone(), cx);
+        multibuffer.add_diff(diff.clone(), cx);
         multibuffer.push_excerpts(
             buffer.clone(),
             [ExcerptRange {
@@ -1018,8 +1016,8 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
 
     buffer.update(cx, |buffer, cx| {
         buffer.edit([(0..0, "a\nb\nc")], None, cx);
-        change_set.update(cx, |change_set, cx| {
-            change_set.recalculate_diff_sync(buffer.snapshot().text, cx);
+        diff.update(cx, |diff, cx| {
+            diff.recalculate_diff_sync(buffer.snapshot().text, cx);
         });
         assert_eq!(buffer.text(), "a\nb\nc")
     });
@@ -1030,8 +1028,8 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
 
     buffer.update(cx, |buffer, cx| {
         buffer.undo(cx);
-        change_set.update(cx, |change_set, cx| {
-            change_set.recalculate_diff_sync(buffer.snapshot().text, cx);
+        diff.update(cx, |diff, cx| {
+            diff.recalculate_diff_sync(buffer.snapshot().text, cx);
         });
         assert_eq!(buffer.text(), "")
     });
@@ -1273,12 +1271,12 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) {
     );
 
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
     cx.run_until_parked();
 
     let multibuffer = cx.new(|cx| {
         let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
-        multibuffer.add_change_set(change_set.clone(), cx);
+        multibuffer.add_diff(diff.clone(), cx);
         multibuffer
     });
 
@@ -1463,8 +1461,8 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) {
     assert_line_indents(&snapshot);
 
     // Recalculate the diff, changing the first diff hunk.
-    change_set.update(cx, |change_set, cx| {
-        change_set.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx);
+    diff.update(cx, |diff, cx| {
+        diff.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx);
     });
     cx.run_until_parked();
     assert_new_snapshot(
@@ -1516,12 +1514,12 @@ fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
     );
 
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
     cx.run_until_parked();
 
     let multibuffer = cx.new(|cx| {
         let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
-        multibuffer.add_change_set(change_set.clone(), cx);
+        multibuffer.add_diff(diff.clone(), cx);
         multibuffer
     });
 
@@ -1918,8 +1916,8 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
 
     let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
     let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx));
-    let change_set_1 = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_1, &buffer_1, cx));
-    let change_set_2 = cx.new(|cx| BufferChangeSet::new_with_base_text(base_text_2, &buffer_2, cx));
+    let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx));
+    let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx));
     cx.run_until_parked();
 
     let multibuffer = cx.new(|cx| {
@@ -1940,8 +1938,8 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
             }],
             cx,
         );
-        multibuffer.add_change_set(change_set_1.clone(), cx);
-        multibuffer.add_change_set(change_set_2.clone(), cx);
+        multibuffer.add_diff(diff_1.clone(), cx);
+        multibuffer.add_diff(diff_2.clone(), cx);
         multibuffer
     });
 
@@ -2001,11 +1999,11 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
 
     let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id());
     let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id());
-    let base_id_1 = change_set_1.read_with(cx, |change_set, _| {
-        change_set.base_text.as_ref().unwrap().remote_id()
+    let base_id_1 = diff_1.read_with(cx, |diff, _| {
+        diff.snapshot.base_text.as_ref().unwrap().remote_id()
     });
-    let base_id_2 = change_set_2.read_with(cx, |change_set, _| {
-        change_set.base_text.as_ref().unwrap().remote_id()
+    let base_id_2 = diff_2.read_with(cx, |diff, _| {
+        diff.snapshot.base_text.as_ref().unwrap().remote_id()
     });
 
     let buffer_lines = (0..=snapshot.max_row().0)
@@ -2101,7 +2099,7 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
 #[derive(Default)]
 struct ReferenceMultibuffer {
     excerpts: Vec<ReferenceExcerpt>,
-    change_sets: HashMap<BufferId, Entity<BufferChangeSet>>,
+    diffs: HashMap<BufferId, Entity<BufferDiff>>,
 }
 
 #[derive(Debug)]
@@ -2190,10 +2188,10 @@ impl ReferenceMultibuffer {
             .unwrap();
         let buffer = excerpt.buffer.read(cx).snapshot();
         let buffer_id = buffer.remote_id();
-        let Some(change_set) = self.change_sets.get(&buffer_id) else {
+        let Some(diff) = self.diffs.get(&buffer_id) else {
             return;
         };
-        let diff = change_set.read(cx).diff_to_buffer.clone();
+        let diff = diff.read(cx).snapshot.clone();
         let excerpt_range = excerpt.range.to_offset(&buffer);
         for hunk in diff.hunks_intersecting_range(range, &buffer) {
             let hunk_range = hunk.buffer_range.to_offset(&buffer);
@@ -2227,9 +2225,9 @@ impl ReferenceMultibuffer {
             excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32));
             let buffer = excerpt.buffer.read(cx);
             let buffer_range = excerpt.range.to_offset(buffer);
-            let change_set = self.change_sets.get(&buffer.remote_id()).unwrap().read(cx);
-            let diff = change_set.diff_to_buffer.clone();
-            let base_buffer = change_set.base_text.as_ref().unwrap();
+            let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx);
+            let diff = diff.snapshot.clone();
+            let base_buffer = diff.base_text.as_ref().unwrap();
 
             let mut offset = buffer_range.start;
             let mut hunks = diff
@@ -2367,12 +2365,7 @@ impl ReferenceMultibuffer {
             let buffer = excerpt.buffer.read(cx).snapshot();
             let excerpt_range = excerpt.range.to_offset(&buffer);
             let buffer_id = buffer.remote_id();
-            let diff = &self
-                .change_sets
-                .get(&buffer_id)
-                .unwrap()
-                .read(cx)
-                .diff_to_buffer;
+            let diff = &self.diffs.get(&buffer_id).unwrap().read(cx).snapshot;
             let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable();
             excerpt.expanded_diff_hunks.retain(|hunk_anchor| {
                 if !hunk_anchor.is_valid(&buffer) {
@@ -2396,9 +2389,9 @@ impl ReferenceMultibuffer {
         }
     }
 
-    fn add_change_set(&mut self, change_set: Entity<BufferChangeSet>, cx: &mut App) {
-        let buffer_id = change_set.read(cx).buffer_id;
-        self.change_sets.insert(buffer_id, change_set);
+    fn add_diff(&mut self, diff: Entity<BufferDiff>, cx: &mut App) {
+        let buffer_id = diff.read(cx).buffer_id;
+        self.diffs.insert(buffer_id, diff);
     }
 }
 
@@ -2528,16 +2521,16 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
                 multibuffer.update(cx, |multibuffer, cx| {
                     for buffer in multibuffer.all_buffers() {
                         let snapshot = buffer.read(cx).snapshot();
-                        let _ = multibuffer
-                            .change_set_for(snapshot.remote_id())
-                            .unwrap()
-                            .update(cx, |change_set, cx| {
+                        let _ = multibuffer.diff_for(snapshot.remote_id()).unwrap().update(
+                            cx,
+                            |diff, cx| {
                                 log::info!(
                                     "recalculating diff for buffer {:?}",
                                     snapshot.remote_id(),
                                 );
-                                change_set.recalculate_diff_sync(snapshot.text, cx);
-                            });
+                                diff.recalculate_diff_sync(snapshot.text, cx);
+                            },
+                        );
                     }
                     reference.diffs_updated(cx);
                     needs_diff_calculation = false;
@@ -2550,12 +2543,11 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
                         .collect::<String>();
 
                     let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
-                    let change_set =
-                        cx.new(|cx| BufferChangeSet::new_with_base_text(&base_text, &buffer, cx));
+                    let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
 
                     multibuffer.update(cx, |multibuffer, cx| {
-                        reference.add_change_set(change_set.clone(), cx);
-                        multibuffer.add_change_set(change_set, cx)
+                        reference.add_diff(diff.clone(), cx);
+                        multibuffer.add_diff(diff, cx)
                     });
                     buffers.push(buffer);
                     buffers.last().unwrap()

crates/project/Cargo.toml 🔗

@@ -30,6 +30,7 @@ async-trait.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true
+diff.workspace = true
 fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
@@ -77,6 +78,7 @@ fancy-regex.workspace = true
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }
 collections = { workspace = true, features = ["test-support"] }
+diff = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
 fs = { workspace = true, features = ["test-support"] }
 git2.workspace = true

crates/project/src/buffer_store.rs 🔗

@@ -8,13 +8,10 @@ use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegist
 use anyhow::{anyhow, bail, Context as _, Result};
 use client::Client;
 use collections::{hash_map, HashMap, HashSet};
+use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot};
 use fs::Fs;
-use futures::{
-    channel::oneshot,
-    future::{OptionFuture, Shared},
-    Future, FutureExt as _, StreamExt,
-};
-use git::{blame::Blame, diff::BufferDiff, repository::RepoPath};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt};
+use git::{blame::Blame, repository::RepoPath};
 use gpui::{
     App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
 };
@@ -38,12 +35,12 @@ use std::{
     sync::Arc,
     time::Instant,
 };
-use text::{BufferId, Rope};
+use text::BufferId;
 use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
 use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId};
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-enum ChangeSetKind {
+enum DiffKind {
     Unstaged,
     Uncommitted,
 }
@@ -54,10 +51,8 @@ pub struct BufferStore {
     #[allow(clippy::type_complexity)]
     loading_buffers: HashMap<ProjectPath, Shared<Task<Result<Entity<Buffer>, Arc<anyhow::Error>>>>>,
     #[allow(clippy::type_complexity)]
-    loading_change_sets: HashMap<
-        (BufferId, ChangeSetKind),
-        Shared<Task<Result<Entity<BufferChangeSet>, Arc<anyhow::Error>>>>,
-    >,
+    loading_diffs:
+        HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
     worktree_store: Entity<WorktreeStore>,
     opened_buffers: HashMap<BufferId, OpenBuffer>,
     downstream_client: Option<(AnyProtoClient, u64)>,
@@ -67,14 +62,14 @@ pub struct BufferStore {
 #[derive(Hash, Eq, PartialEq, Clone)]
 struct SharedBuffer {
     buffer: Entity<Buffer>,
-    change_set: Option<Entity<BufferChangeSet>>,
+    diff: Option<Entity<BufferDiff>>,
     lsp_handle: Option<OpenLspBufferHandle>,
 }
 
 #[derive(Default)]
-struct BufferChangeSetState {
-    unstaged_changes: Option<WeakEntity<BufferChangeSet>>,
-    uncommitted_changes: Option<WeakEntity<BufferChangeSet>>,
+struct BufferDiffState {
+    unstaged_diff: Option<WeakEntity<BufferDiff>>,
+    uncommitted_diff: Option<WeakEntity<BufferDiff>>,
     recalculate_diff_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     language_registry: Option<Arc<LanguageRegistry>>,
@@ -99,21 +94,19 @@ enum DiffBasesChange {
     SetBoth(Option<String>),
 }
 
-impl BufferChangeSetState {
+impl BufferDiffState {
     fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
         self.language = buffer.read(cx).language().cloned();
         self.language_changed = true;
         let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
     }
 
-    fn unstaged_changes(&self) -> Option<Entity<BufferChangeSet>> {
-        self.unstaged_changes.as_ref().and_then(|set| set.upgrade())
+    fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
+        self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
     }
 
-    fn uncommitted_changes(&self) -> Option<Entity<BufferChangeSet>> {
-        self.uncommitted_changes
-            .as_ref()
-            .and_then(|set| set.upgrade())
+    fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
+        self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
     }
 
     fn handle_base_texts_updated(
@@ -199,8 +192,8 @@ impl BufferChangeSetState {
 
         let language = self.language.clone();
         let language_registry = self.language_registry.clone();
-        let unstaged_changes = self.unstaged_changes();
-        let uncommitted_changes = self.uncommitted_changes();
+        let unstaged_diff = self.unstaged_diff();
+        let uncommitted_diff = self.uncommitted_diff();
         let head = self.head_text.clone();
         let index = self.index_text.clone();
         let index_changed = self.index_changed;
@@ -212,90 +205,71 @@ impl BufferChangeSetState {
             _ => false,
         };
         self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
-            if let Some(unstaged_changes) = &unstaged_changes {
-                let staged_snapshot = if index_changed || language_changed {
-                    let staged_snapshot = cx.update(|cx| {
-                        index.as_ref().map(|head| {
-                            language::Buffer::build_snapshot(
-                                Rope::from(head.as_str()),
-                                language.clone(),
-                                language_registry.clone(),
+            if let Some(unstaged_diff) = &unstaged_diff {
+                let snapshot = if index_changed || language_changed {
+                    cx.update(|cx| {
+                        BufferDiffSnapshot::build(
+                            buffer.clone(),
+                            index,
+                            language.clone(),
+                            language_registry.clone(),
+                            cx,
+                        )
+                    })?
+                    .await
+                } else {
+                    unstaged_diff
+                        .read_with(&cx, |changes, cx| {
+                            BufferDiffSnapshot::build_with_base_buffer(
+                                buffer.clone(),
+                                index,
+                                changes.snapshot.base_text.clone(),
                                 cx,
                             )
-                        })
-                    })?;
-                    cx.background_executor()
-                        .spawn(OptionFuture::from(staged_snapshot))
-                } else {
-                    Task::ready(
-                        unstaged_changes
-                            .read_with(&cx, |change_set, _| change_set.base_text.clone())?,
-                    )
+                        })?
+                        .await
                 };
 
-                let diff =
-                    cx.background_executor().spawn({
-                        let buffer = buffer.clone();
-                        async move {
-                            BufferDiff::build(index.as_ref().map(|index| index.as_str()), &buffer)
-                        }
-                    });
-
-                let (staged_snapshot, diff) = futures::join!(staged_snapshot, diff);
-
-                unstaged_changes.update(&mut cx, |unstaged_changes, cx| {
-                    unstaged_changes.set_state(staged_snapshot.clone(), diff, &buffer, cx);
+                unstaged_diff.update(&mut cx, |unstaged_diff, cx| {
+                    unstaged_diff.set_state(snapshot, &buffer, cx);
                     if language_changed {
-                        cx.emit(BufferChangeSetEvent::LanguageChanged);
+                        cx.emit(BufferDiffEvent::LanguageChanged);
                     }
                 })?;
             }
 
-            if let Some(uncommitted_changes) = &uncommitted_changes {
-                let (snapshot, diff) = if let (Some(unstaged_changes), true) =
-                    (&unstaged_changes, index_matches_head)
-                {
-                    unstaged_changes.read_with(&cx, |change_set, _| {
-                        (
-                            change_set.base_text.clone(),
-                            change_set.diff_to_buffer.clone(),
-                        )
-                    })?
-                } else {
-                    let committed_snapshot = if head_changed || language_changed {
-                        let committed_snapshot = cx.update(|cx| {
-                            head.as_ref().map(|head| {
-                                language::Buffer::build_snapshot(
-                                    Rope::from(head.as_str()),
-                                    language.clone(),
-                                    language_registry.clone(),
+            if let Some(uncommitted_diff) = &uncommitted_diff {
+                let snapshot =
+                    if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) {
+                        unstaged_diff.read_with(&cx, |diff, _| diff.snapshot.clone())?
+                    } else if head_changed || language_changed {
+                        cx.update(|cx| {
+                            BufferDiffSnapshot::build(
+                                buffer.clone(),
+                                head,
+                                language.clone(),
+                                language_registry.clone(),
+                                cx,
+                            )
+                        })?
+                        .await
+                    } else {
+                        uncommitted_diff
+                            .read_with(&cx, |changes, cx| {
+                                BufferDiffSnapshot::build_with_base_buffer(
+                                    buffer.clone(),
+                                    head,
+                                    changes.snapshot.base_text.clone(),
                                     cx,
                                 )
-                            })
-                        })?;
-                        cx.background_executor()
-                            .spawn(OptionFuture::from(committed_snapshot))
-                    } else {
-                        Task::ready(
-                            uncommitted_changes
-                                .read_with(&cx, |change_set, _| change_set.base_text.clone())?,
-                        )
+                            })?
+                            .await
                     };
 
-                    let diff = cx.background_executor().spawn({
-                        let buffer = buffer.clone();
-                        let head = head.clone();
-                        async move {
-                            BufferDiff::build(head.as_ref().map(|head| head.as_str()), &buffer)
-                        }
-                    });
-                    futures::join!(committed_snapshot, diff)
-                };
-
-                uncommitted_changes.update(&mut cx, |change_set, cx| {
-                    change_set.set_state(snapshot, diff, &buffer, cx);
+                uncommitted_diff.update(&mut cx, |diff, cx| {
+                    diff.set_state(snapshot, &buffer, cx);
                     if language_changed {
-                        cx.emit(BufferChangeSetEvent::LanguageChanged);
+                        cx.emit(BufferDiffEvent::LanguageChanged);
                     }
                 })?;
             }
@@ -317,28 +291,6 @@ impl BufferChangeSetState {
     }
 }
 
-pub struct BufferChangeSet {
-    pub buffer_id: BufferId,
-    pub base_text: Option<language::BufferSnapshot>,
-    pub diff_to_buffer: BufferDiff,
-    pub unstaged_change_set: Option<Entity<BufferChangeSet>>,
-}
-
-impl std::fmt::Debug for BufferChangeSet {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("BufferChangeSet")
-            .field("buffer_id", &self.buffer_id)
-            .field("base_text", &self.base_text.as_ref().map(|s| s.text()))
-            .field("diff_to_buffer", &self.diff_to_buffer)
-            .finish()
-    }
-}
-
-pub enum BufferChangeSetEvent {
-    DiffChanged { changed_range: Range<text::Anchor> },
-    LanguageChanged,
-}
-
 enum BufferStoreState {
     Local(LocalBufferStore),
     Remote(RemoteBufferStore),
@@ -364,7 +316,7 @@ struct LocalBufferStore {
 enum OpenBuffer {
     Complete {
         buffer: WeakEntity<Buffer>,
-        change_set_state: Entity<BufferChangeSetState>,
+        diff_state: Entity<BufferDiffState>,
     },
     Operations(Vec<Operation>),
 }
@@ -384,12 +336,12 @@ pub struct ProjectTransaction(pub HashMap<Entity<Buffer>, language::Transaction>
 impl EventEmitter<BufferStoreEvent> for BufferStore {}
 
 impl RemoteBufferStore {
-    fn open_unstaged_changes(&self, buffer_id: BufferId, cx: &App) -> Task<Result<Option<String>>> {
+    fn open_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Task<Result<Option<String>>> {
         let project_id = self.project_id;
         let client = self.upstream_client.clone();
         cx.background_executor().spawn(async move {
             let response = client
-                .request(proto::OpenUnstagedChanges {
+                .request(proto::OpenUnstagedDiff {
                     project_id,
                     buffer_id: buffer_id.to_proto(),
                 })
@@ -398,18 +350,18 @@ impl RemoteBufferStore {
         })
     }
 
-    fn open_uncommitted_changes(
+    fn open_uncommitted_diff(
         &self,
         buffer_id: BufferId,
         cx: &App,
     ) -> Task<Result<DiffBasesChange>> {
-        use proto::open_uncommitted_changes_response::Mode;
+        use proto::open_uncommitted_diff_response::Mode;
 
         let project_id = self.project_id;
         let client = self.upstream_client.clone();
         cx.background_executor().spawn(async move {
             let response = client
-                .request(proto::OpenUncommittedChanges {
+                .request(proto::OpenUncommittedDiff {
                     project_id,
                     buffer_id: buffer_id.to_proto(),
                 })
@@ -839,13 +791,9 @@ impl LocalBufferStore {
     ) {
         debug_assert!(worktree_handle.read(cx).is_local());
 
-        let mut change_set_state_updates = Vec::new();
+        let mut diff_state_updates = Vec::new();
         for buffer in this.opened_buffers.values() {
-            let OpenBuffer::Complete {
-                buffer,
-                change_set_state,
-            } = buffer
-            else {
+            let OpenBuffer::Complete { buffer, diff_state } = buffer else {
                 continue;
             };
             let Some(buffer) = buffer.upgrade() else {
@@ -858,22 +806,22 @@ impl LocalBufferStore {
             if file.worktree != worktree_handle {
                 continue;
             }
-            let change_set_state = change_set_state.read(cx);
+            let diff_state = diff_state.read(cx);
             if changed_repos
                 .iter()
                 .any(|(work_dir, _)| file.path.starts_with(work_dir))
             {
                 let snapshot = buffer.text_snapshot();
-                change_set_state_updates.push((
+                diff_state_updates.push((
                     snapshot.clone(),
                     file.path.clone(),
-                    change_set_state
-                        .unstaged_changes
+                    diff_state
+                        .unstaged_diff
                         .as_ref()
                         .and_then(|set| set.upgrade())
                         .is_some(),
-                    change_set_state
-                        .uncommitted_changes
+                    diff_state
+                        .uncommitted_diff
                         .as_ref()
                         .and_then(|set| set.upgrade())
                         .is_some(),
@@ -881,7 +829,7 @@ impl LocalBufferStore {
             }
         }
 
-        if change_set_state_updates.is_empty() {
+        if diff_state_updates.is_empty() {
             return;
         }
 
@@ -891,7 +839,7 @@ impl LocalBufferStore {
             let diff_bases_changes_by_buffer = cx
                 .background_executor()
                 .spawn(async move {
-                    change_set_state_updates
+                    diff_state_updates
                         .into_iter()
                         .filter_map(
                             |(buffer_snapshot, path, needs_staged_text, needs_committed_text)| {
@@ -934,9 +882,8 @@ impl LocalBufferStore {
 
             this.update(&mut cx, |this, cx| {
                 for (buffer_snapshot, diff_bases_change) in diff_bases_changes_by_buffer {
-                    let Some(OpenBuffer::Complete {
-                        change_set_state, ..
-                    }) = this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
+                    let Some(OpenBuffer::Complete { diff_state, .. }) =
+                        this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
                     else {
                         continue;
                     };
@@ -944,7 +891,7 @@ impl LocalBufferStore {
                         continue;
                     };
 
-                    change_set_state.update(cx, |change_set_state, cx| {
+                    diff_state.update(cx, |diff_state, cx| {
                         use proto::update_diff_bases::Mode;
 
                         if let Some((client, project_id)) = this.downstream_client.as_ref() {
@@ -972,11 +919,8 @@ impl LocalBufferStore {
                             client.send(message).log_err();
                         }
 
-                        let _ = change_set_state.diff_bases_changed(
-                            buffer_snapshot,
-                            diff_bases_change,
-                            cx,
-                        );
+                        let _ =
+                            diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
                     });
                 }
             })
@@ -1282,8 +1226,8 @@ impl BufferStore {
         client.add_entity_request_handler(Self::handle_blame_buffer);
         client.add_entity_request_handler(Self::handle_reload_buffers);
         client.add_entity_request_handler(Self::handle_get_permalink_to_line);
-        client.add_entity_request_handler(Self::handle_open_unstaged_changes);
-        client.add_entity_request_handler(Self::handle_open_uncommitted_changes);
+        client.add_entity_request_handler(Self::handle_open_unstaged_diff);
+        client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
         client.add_entity_message_handler(Self::handle_update_diff_bases);
     }
 
@@ -1305,7 +1249,7 @@ impl BufferStore {
             opened_buffers: Default::default(),
             shared_buffers: Default::default(),
             loading_buffers: Default::default(),
-            loading_change_sets: Default::default(),
+            loading_diffs: Default::default(),
             worktree_store,
         }
     }
@@ -1328,7 +1272,7 @@ impl BufferStore {
             downstream_client: None,
             opened_buffers: Default::default(),
             loading_buffers: Default::default(),
-            loading_change_sets: Default::default(),
+            loading_diffs: Default::default(),
             shared_buffers: Default::default(),
             worktree_store,
         }
@@ -1401,33 +1345,30 @@ impl BufferStore {
             .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
-    pub fn open_unstaged_changes(
+    pub fn open_unstaged_diff(
         &mut self,
         buffer: Entity<Buffer>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<BufferChangeSet>>> {
+    ) -> Task<Result<Entity<BufferDiff>>> {
         let buffer_id = buffer.read(cx).remote_id();
-        if let Some(change_set) = self.get_unstaged_changes(buffer_id, cx) {
-            return Task::ready(Ok(change_set));
+        if let Some(diff) = self.get_unstaged_diff(buffer_id, cx) {
+            return Task::ready(Ok(diff));
         }
 
-        let task = match self
-            .loading_change_sets
-            .entry((buffer_id, ChangeSetKind::Unstaged))
-        {
+        let task = match self.loading_diffs.entry((buffer_id, DiffKind::Unstaged)) {
             hash_map::Entry::Occupied(e) => e.get().clone(),
             hash_map::Entry::Vacant(entry) => {
                 let staged_text = match &self.state {
                     BufferStoreState::Local(this) => this.load_staged_text(&buffer, cx),
-                    BufferStoreState::Remote(this) => this.open_unstaged_changes(buffer_id, cx),
+                    BufferStoreState::Remote(this) => this.open_unstaged_diff(buffer_id, cx),
                 };
 
                 entry
                     .insert(
                         cx.spawn(move |this, cx| async move {
-                            Self::open_change_set_internal(
+                            Self::open_diff_internal(
                                 this,
-                                ChangeSetKind::Unstaged,
+                                DiffKind::Unstaged,
                                 staged_text.await.map(DiffBasesChange::SetIndex),
                                 buffer,
                                 cx,
@@ -1445,20 +1386,17 @@ impl BufferStore {
             .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
-    pub fn open_uncommitted_changes(
+    pub fn open_uncommitted_diff(
         &mut self,
         buffer: Entity<Buffer>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<BufferChangeSet>>> {
+    ) -> Task<Result<Entity<BufferDiff>>> {
         let buffer_id = buffer.read(cx).remote_id();
-        if let Some(change_set) = self.get_uncommitted_changes(buffer_id, cx) {
-            return Task::ready(Ok(change_set));
+        if let Some(diff) = self.get_uncommitted_diff(buffer_id, cx) {
+            return Task::ready(Ok(diff));
         }
 
-        let task = match self
-            .loading_change_sets
-            .entry((buffer_id, ChangeSetKind::Uncommitted))
-        {
+        let task = match self.loading_diffs.entry((buffer_id, DiffKind::Uncommitted)) {
             hash_map::Entry::Occupied(e) => e.get().clone(),
             hash_map::Entry::Vacant(entry) => {
                 let changes = match &self.state {
@@ -1479,15 +1417,15 @@ impl BufferStore {
                             Ok(diff_bases_change)
                         })
                     }
-                    BufferStoreState::Remote(this) => this.open_uncommitted_changes(buffer_id, cx),
+                    BufferStoreState::Remote(this) => this.open_uncommitted_diff(buffer_id, cx),
                 };
 
                 entry
                     .insert(
                         cx.spawn(move |this, cx| async move {
-                            Self::open_change_set_internal(
+                            Self::open_diff_internal(
                                 this,
-                                ChangeSetKind::Uncommitted,
+                                DiffKind::Uncommitted,
                                 changes.await,
                                 buffer,
                                 cx,
@@ -1505,30 +1443,18 @@ impl BufferStore {
             .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn set_unstaged_change_set(
-        &mut self,
-        buffer_id: BufferId,
-        change_set: Entity<BufferChangeSet>,
-    ) {
-        self.loading_change_sets.insert(
-            (buffer_id, ChangeSetKind::Unstaged),
-            Task::ready(Ok(change_set)).shared(),
-        );
-    }
-
-    async fn open_change_set_internal(
+    async fn open_diff_internal(
         this: WeakEntity<Self>,
-        kind: ChangeSetKind,
+        kind: DiffKind,
         texts: Result<DiffBasesChange>,
         buffer: Entity<Buffer>,
         mut cx: AsyncApp,
-    ) -> Result<Entity<BufferChangeSet>> {
+    ) -> Result<Entity<BufferDiff>> {
         let diff_bases_change = match texts {
             Err(e) => {
                 this.update(&mut cx, |this, cx| {
                     let buffer_id = buffer.read(cx).remote_id();
-                    this.loading_change_sets.remove(&(buffer_id, kind));
+                    this.loading_diffs.remove(&(buffer_id, kind));
                 })?;
                 return Err(e);
             }
@@ -1537,15 +1463,14 @@ impl BufferStore {
 
         this.update(&mut cx, |this, cx| {
             let buffer_id = buffer.read(cx).remote_id();
-            this.loading_change_sets.remove(&(buffer_id, kind));
+            this.loading_diffs.remove(&(buffer_id, kind));
 
-            if let Some(OpenBuffer::Complete {
-                change_set_state, ..
-            }) = this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
+            if let Some(OpenBuffer::Complete { diff_state, .. }) =
+                this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
             {
-                change_set_state.update(cx, |change_set_state, cx| {
+                diff_state.update(cx, |diff_state, cx| {
                     let buffer_id = buffer.read(cx).remote_id();
-                    change_set_state.buffer_subscription.get_or_insert_with(|| {
+                    diff_state.buffer_subscription.get_or_insert_with(|| {
                         cx.subscribe(&buffer, |this, buffer, event, cx| match event {
                             BufferEvent::LanguageChanged => {
                                 this.buffer_language_changed(buffer, cx)
@@ -1554,47 +1479,41 @@ impl BufferStore {
                         })
                     });
 
-                    let change_set = cx.new(|cx| BufferChangeSet {
+                    let diff = cx.new(|cx| BufferDiff {
                         buffer_id,
-                        base_text: None,
-                        diff_to_buffer: BufferDiff::new(&buffer.read(cx).text_snapshot()),
-                        unstaged_change_set: None,
+                        snapshot: BufferDiffSnapshot::new(&buffer.read(cx).text_snapshot()),
+                        unstaged_diff: None,
                     });
                     match kind {
-                        ChangeSetKind::Unstaged => {
-                            change_set_state.unstaged_changes = Some(change_set.downgrade())
-                        }
-                        ChangeSetKind::Uncommitted => {
-                            let unstaged_change_set =
-                                if let Some(change_set) = change_set_state.unstaged_changes() {
-                                    change_set
-                                } else {
-                                    let unstaged_change_set = cx.new(|cx| BufferChangeSet {
-                                        buffer_id,
-                                        base_text: None,
-                                        diff_to_buffer: BufferDiff::new(
-                                            &buffer.read(cx).text_snapshot(),
-                                        ),
-                                        unstaged_change_set: None,
-                                    });
-                                    change_set_state.unstaged_changes =
-                                        Some(unstaged_change_set.downgrade());
-                                    unstaged_change_set
-                                };
+                        DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
+                        DiffKind::Uncommitted => {
+                            let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
+                                diff
+                            } else {
+                                let unstaged_diff = cx.new(|cx| BufferDiff {
+                                    buffer_id,
+                                    snapshot: BufferDiffSnapshot::new(
+                                        &buffer.read(cx).text_snapshot(),
+                                    ),
+                                    unstaged_diff: None,
+                                });
+                                diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
+                                unstaged_diff
+                            };
 
-                            change_set.update(cx, |change_set, _| {
-                                change_set.unstaged_change_set = Some(unstaged_change_set);
+                            diff.update(cx, |diff, _| {
+                                diff.unstaged_diff = Some(unstaged_diff);
                             });
-                            change_set_state.uncommitted_changes = Some(change_set.downgrade())
+                            diff_state.uncommitted_diff = Some(diff.downgrade())
                         }
                     };
 
                     let buffer = buffer.read(cx).text_snapshot();
-                    let rx = change_set_state.diff_bases_changed(buffer, diff_bases_change, cx);
+                    let rx = diff_state.diff_bases_changed(buffer, diff_bases_change, cx);
 
                     Ok(async move {
                         rx.await.ok();
-                        Ok(change_set)
+                        Ok(diff)
                     })
                 })
             } else {
@@ -1807,7 +1726,7 @@ impl BufferStore {
         let is_remote = buffer.read(cx).replica_id() != 0;
         let open_buffer = OpenBuffer::Complete {
             buffer: buffer.downgrade(),
-            change_set_state: cx.new(|_| BufferChangeSetState::default()),
+            diff_state: cx.new(|_| BufferDiffState::default()),
         };
 
         let handle = cx.entity().downgrade();
@@ -1888,39 +1807,21 @@ impl BufferStore {
         })
     }
 
-    pub fn get_unstaged_changes(
-        &self,
-        buffer_id: BufferId,
-        cx: &App,
-    ) -> Option<Entity<BufferChangeSet>> {
-        if let OpenBuffer::Complete {
-            change_set_state, ..
-        } = self.opened_buffers.get(&buffer_id)?
-        {
-            change_set_state
-                .read(cx)
-                .unstaged_changes
-                .as_ref()?
-                .upgrade()
+    pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
+        if let OpenBuffer::Complete { diff_state, .. } = self.opened_buffers.get(&buffer_id)? {
+            diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
         } else {
             None
         }
     }
 
-    pub fn get_uncommitted_changes(
+    pub fn get_uncommitted_diff(
         &self,
         buffer_id: BufferId,
         cx: &App,
-    ) -> Option<Entity<BufferChangeSet>> {
-        if let OpenBuffer::Complete {
-            change_set_state, ..
-        } = self.opened_buffers.get(&buffer_id)?
-        {
-            change_set_state
-                .read(cx)
-                .uncommitted_changes
-                .as_ref()?
-                .upgrade()
+    ) -> Option<Entity<BufferDiff>> {
+        if let OpenBuffer::Complete { diff_state, .. } = self.opened_buffers.get(&buffer_id)? {
+            diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
         } else {
             None
         }
@@ -2040,13 +1941,12 @@ impl BufferStore {
     ) -> impl Future<Output = ()> {
         let mut futures = Vec::new();
         for buffer in buffers {
-            if let Some(OpenBuffer::Complete {
-                change_set_state, ..
-            }) = self.opened_buffers.get_mut(&buffer.read(cx).remote_id())
+            if let Some(OpenBuffer::Complete { diff_state, .. }) =
+                self.opened_buffers.get_mut(&buffer.read(cx).remote_id())
             {
                 let buffer = buffer.read(cx).text_snapshot();
-                futures.push(change_set_state.update(cx, |change_set_state, cx| {
-                    change_set_state.recalculate_diffs(buffer, cx)
+                futures.push(diff_state.update(cx, |diff_state, cx| {
+                    diff_state.recalculate_diffs(buffer, cx)
                 }));
             }
         }
@@ -2156,7 +2056,7 @@ impl BufferStore {
                     .entry(buffer_id)
                     .or_insert_with(|| SharedBuffer {
                         buffer: buffer.clone(),
-                        change_set: None,
+                        diff: None,
                         lsp_handle: None,
                     });
 
@@ -2461,16 +2361,16 @@ impl BufferStore {
         })
     }
 
-    pub async fn handle_open_unstaged_changes(
+    pub async fn handle_open_unstaged_diff(
         this: Entity<Self>,
-        request: TypedEnvelope<proto::OpenUnstagedChanges>,
+        request: TypedEnvelope<proto::OpenUnstagedDiff>,
         mut cx: AsyncApp,
-    ) -> Result<proto::OpenUnstagedChangesResponse> {
+    ) -> Result<proto::OpenUnstagedDiffResponse> {
         let buffer_id = BufferId::new(request.payload.buffer_id)?;
-        let change_set = this
+        let diff = this
             .update(&mut cx, |this, cx| {
                 let buffer = this.get(buffer_id)?;
-                Some(this.open_unstaged_changes(buffer, cx))
+                Some(this.open_unstaged_diff(buffer, cx))
             })?
             .ok_or_else(|| anyhow!("no such buffer"))?
             .await?;
@@ -2481,25 +2381,25 @@ impl BufferStore {
                 .or_default();
             debug_assert!(shared_buffers.contains_key(&buffer_id));
             if let Some(shared) = shared_buffers.get_mut(&buffer_id) {
-                shared.change_set = Some(change_set.clone());
+                shared.diff = Some(diff.clone());
             }
         })?;
-        let staged_text = change_set.read_with(&cx, |change_set, _| {
-            change_set.base_text.as_ref().map(|buffer| buffer.text())
+        let staged_text = diff.read_with(&cx, |diff, _| {
+            diff.snapshot.base_text.as_ref().map(|buffer| buffer.text())
         })?;
-        Ok(proto::OpenUnstagedChangesResponse { staged_text })
+        Ok(proto::OpenUnstagedDiffResponse { staged_text })
     }
 
-    pub async fn handle_open_uncommitted_changes(
+    pub async fn handle_open_uncommitted_diff(
         this: Entity<Self>,
-        request: TypedEnvelope<proto::OpenUncommittedChanges>,
+        request: TypedEnvelope<proto::OpenUncommittedDiff>,
         mut cx: AsyncApp,
-    ) -> Result<proto::OpenUncommittedChangesResponse> {
+    ) -> Result<proto::OpenUncommittedDiffResponse> {
         let buffer_id = BufferId::new(request.payload.buffer_id)?;
-        let change_set = this
+        let diff = this
             .update(&mut cx, |this, cx| {
                 let buffer = this.get(buffer_id)?;
-                Some(this.open_uncommitted_changes(buffer, cx))
+                Some(this.open_uncommitted_diff(buffer, cx))
             })?
             .ok_or_else(|| anyhow!("no such buffer"))?
             .await?;
@@ -2510,21 +2410,21 @@ impl BufferStore {
                 .or_default();
             debug_assert!(shared_buffers.contains_key(&buffer_id));
             if let Some(shared) = shared_buffers.get_mut(&buffer_id) {
-                shared.change_set = Some(change_set.clone());
+                shared.diff = Some(diff.clone());
             }
         })?;
-        change_set.read_with(&cx, |change_set, cx| {
-            use proto::open_uncommitted_changes_response::Mode;
+        diff.read_with(&cx, |diff, cx| {
+            use proto::open_uncommitted_diff_response::Mode;
 
-            let staged_buffer = change_set
-                .unstaged_change_set
+            let staged_buffer = diff
+                .unstaged_diff
                 .as_ref()
-                .and_then(|change_set| change_set.read(cx).base_text.as_ref());
+                .and_then(|diff| diff.read(cx).snapshot.base_text.as_ref());
 
             let mode;
             let staged_text;
             let committed_text;
-            if let Some(committed_buffer) = &change_set.base_text {
+            if let Some(committed_buffer) = &diff.snapshot.base_text {
                 committed_text = Some(committed_buffer.text());
                 if let Some(staged_buffer) = staged_buffer {
                     if staged_buffer.remote_id() == committed_buffer.remote_id() {
@@ -2544,7 +2444,7 @@ impl BufferStore {
                 staged_text = staged_buffer.as_ref().map(|buffer| buffer.text());
             }
 
-            proto::OpenUncommittedChangesResponse {
+            proto::OpenUncommittedDiffResponse {
                 committed_text,
                 staged_text,
                 mode: mode.into(),
@@ -2559,15 +2459,13 @@ impl BufferStore {
     ) -> Result<()> {
         let buffer_id = BufferId::new(request.payload.buffer_id)?;
         this.update(&mut cx, |this, cx| {
-            if let Some(OpenBuffer::Complete {
-                change_set_state,
-                buffer,
-            }) = this.opened_buffers.get_mut(&buffer_id)
+            if let Some(OpenBuffer::Complete { diff_state, buffer }) =
+                this.opened_buffers.get_mut(&buffer_id)
             {
                 if let Some(buffer) = buffer.upgrade() {
                     let buffer = buffer.read(cx).text_snapshot();
-                    change_set_state.update(cx, |change_set_state, cx| {
-                        change_set_state.handle_base_texts_updated(buffer, request.payload, cx);
+                    diff_state.update(cx, |diff_state, cx| {
+                        diff_state.handle_base_texts_updated(buffer, request.payload, cx);
                     })
                 }
             }
@@ -2628,7 +2526,7 @@ impl BufferStore {
             buffer_id,
             SharedBuffer {
                 buffer: buffer.clone(),
-                change_set: None,
+                diff: None,
                 lsp_handle: None,
             },
         );
@@ -2783,126 +2681,6 @@ impl BufferStore {
     }
 }
 
-impl EventEmitter<BufferChangeSetEvent> for BufferChangeSet {}
-
-impl BufferChangeSet {
-    fn set_state(
-        &mut self,
-        base_text: Option<language::BufferSnapshot>,
-        diff: BufferDiff,
-        buffer: &text::BufferSnapshot,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(base_text) = base_text.as_ref() {
-            let changed_range = if Some(base_text.remote_id())
-                != self.base_text.as_ref().map(|buffer| buffer.remote_id())
-            {
-                Some(text::Anchor::MIN..text::Anchor::MAX)
-            } else {
-                diff.compare(&self.diff_to_buffer, buffer)
-            };
-            if let Some(changed_range) = changed_range {
-                cx.emit(BufferChangeSetEvent::DiffChanged { changed_range });
-            }
-        }
-        self.base_text = base_text;
-        self.diff_to_buffer = diff;
-    }
-
-    pub fn diff_hunks_intersecting_range<'a>(
-        &'a self,
-        range: Range<text::Anchor>,
-        buffer_snapshot: &'a text::BufferSnapshot,
-    ) -> impl 'a + Iterator<Item = git::diff::DiffHunk> {
-        self.diff_to_buffer
-            .hunks_intersecting_range(range, buffer_snapshot)
-    }
-
-    pub fn diff_hunks_intersecting_range_rev<'a>(
-        &'a self,
-        range: Range<text::Anchor>,
-        buffer_snapshot: &'a text::BufferSnapshot,
-    ) -> impl 'a + Iterator<Item = git::diff::DiffHunk> {
-        self.diff_to_buffer
-            .hunks_intersecting_range_rev(range, buffer_snapshot)
-    }
-
-    /// Used in cases where the change set isn't derived from git.
-    pub fn set_base_text(
-        &mut self,
-        base_buffer: Entity<language::Buffer>,
-        buffer: text::BufferSnapshot,
-        cx: &mut Context<Self>,
-    ) -> oneshot::Receiver<()> {
-        let (tx, rx) = oneshot::channel();
-        let this = cx.weak_entity();
-        let base_buffer = base_buffer.read(cx).snapshot();
-        cx.spawn(|_, mut cx| async move {
-            let diff = cx
-                .background_executor()
-                .spawn({
-                    let base_buffer = base_buffer.clone();
-                    let buffer = buffer.clone();
-                    async move { BufferDiff::build(Some(&base_buffer.text()), &buffer) }
-                })
-                .await;
-            let Some(this) = this.upgrade() else {
-                tx.send(()).ok();
-                return;
-            };
-            this.update(&mut cx, |this, cx| {
-                this.set_state(Some(base_buffer), diff, &buffer, cx);
-            })
-            .log_err();
-            tx.send(()).ok();
-        })
-        .detach();
-        rx
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn base_text_string(&self) -> Option<String> {
-        self.base_text.as_ref().map(|buffer| buffer.text())
-    }
-
-    pub fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
-        BufferChangeSet {
-            buffer_id: buffer.read(cx).remote_id(),
-            base_text: None,
-            diff_to_buffer: BufferDiff::new(&buffer.read(cx).text_snapshot()),
-            unstaged_change_set: None,
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn new_with_base_text(base_text: &str, buffer: &Entity<Buffer>, cx: &mut App) -> Self {
-        let mut base_text = base_text.to_owned();
-        text::LineEnding::normalize(&mut base_text);
-        let diff_to_buffer = BufferDiff::build(Some(&base_text), &buffer.read(cx).text_snapshot());
-        let base_text = language::Buffer::build_snapshot_sync(base_text.into(), None, None, cx);
-        BufferChangeSet {
-            buffer_id: buffer.read(cx).remote_id(),
-            base_text: Some(base_text),
-            diff_to_buffer,
-            unstaged_change_set: None,
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn recalculate_diff_sync(
-        &mut self,
-        snapshot: text::BufferSnapshot,
-        cx: &mut Context<Self>,
-    ) {
-        let mut base_text = self.base_text.as_ref().map(|buffer| buffer.text());
-        if let Some(base_text) = base_text.as_mut() {
-            text::LineEnding::normalize(base_text);
-        }
-        let diff_to_buffer = BufferDiff::build(base_text.as_deref(), &snapshot);
-        self.set_state(self.base_text.clone(), diff_to_buffer, &snapshot, cx);
-    }
-}
-
 impl OpenBuffer {
     fn upgrade(&self) -> Option<Entity<Buffer>> {
         match self {

crates/project/src/project.rs 🔗

@@ -21,6 +21,7 @@ mod project_tests;
 
 mod direnv;
 mod environment;
+use diff::BufferDiff;
 pub use environment::EnvironmentErrorMessage;
 use git::Repository;
 pub mod search_history;
@@ -28,7 +29,7 @@ mod yarn;
 
 use crate::git::GitState;
 use anyhow::{anyhow, Context as _, Result};
-use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent};
+use buffer_store::{BufferStore, BufferStoreEvent};
 use client::{
     proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore,
 };
@@ -1955,31 +1956,31 @@ impl Project {
         })
     }
 
-    pub fn open_unstaged_changes(
+    pub fn open_unstaged_diff(
         &mut self,
         buffer: Entity<Buffer>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<BufferChangeSet>>> {
+    ) -> Task<Result<Entity<BufferDiff>>> {
         if self.is_disconnected(cx) {
             return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
         }
 
         self.buffer_store.update(cx, |buffer_store, cx| {
-            buffer_store.open_unstaged_changes(buffer, cx)
+            buffer_store.open_unstaged_diff(buffer, cx)
         })
     }
 
-    pub fn open_uncommitted_changes(
+    pub fn open_uncommitted_diff(
         &mut self,
         buffer: Entity<Buffer>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<BufferChangeSet>>> {
+    ) -> Task<Result<Entity<BufferDiff>>> {
         if self.is_disconnected(cx) {
             return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
         }
 
         self.buffer_store.update(cx, |buffer_store, cx| {
-            buffer_store.open_uncommitted_changes(buffer, cx)
+            buffer_store.open_uncommitted_diff(buffer, cx)
         })
     }
 

crates/project/src/project_tests.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{Event, *};
-use ::git::diff::assert_hunks;
+use diff::assert_hunks;
 use fs::FakeFs;
 use futures::{future, StreamExt};
 use gpui::{App, SemanticVersion, UpdateGlobal};
@@ -5639,7 +5639,7 @@ async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
+async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let staged_contents = r#"
@@ -5681,20 +5681,20 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let unstaged_changes = project
+    let unstaged_diff = project
         .update(cx, |project, cx| {
-            project.open_unstaged_changes(buffer.clone(), cx)
+            project.open_unstaged_diff(buffer.clone(), cx)
         })
         .await
         .unwrap();
 
     cx.run_until_parked();
-    unstaged_changes.update(cx, |unstaged_changes, cx| {
+    unstaged_diff.update(cx, |unstaged_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
         assert_hunks(
-            unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            unstaged_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &unstaged_changes.base_text.as_ref().unwrap().text(),
+            &unstaged_diff.base_text_string().unwrap(),
             &[
                 (0..1, "", "// print goodbye\n"),
                 (
@@ -5719,19 +5719,19 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
     );
 
     cx.run_until_parked();
-    unstaged_changes.update(cx, |unstaged_changes, cx| {
+    unstaged_diff.update(cx, |unstaged_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
         assert_hunks(
-            unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            unstaged_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &unstaged_changes.base_text.as_ref().unwrap().text(),
+            &unstaged_diff.snapshot.base_text.as_ref().unwrap().text(),
             &[(2..3, "", "    println!(\"goodbye world\");\n")],
         );
     });
 }
 
 #[gpui::test]
-async fn test_uncommitted_changes_for_buffer(cx: &mut gpui::TestAppContext) {
+async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let committed_contents = r#"
@@ -5783,20 +5783,20 @@ async fn test_uncommitted_changes_for_buffer(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let uncommitted_changes = project
+    let uncommitted_diff = project
         .update(cx, |project, cx| {
-            project.open_uncommitted_changes(buffer.clone(), cx)
+            project.open_uncommitted_diff(buffer.clone(), cx)
         })
         .await
         .unwrap();
 
     cx.run_until_parked();
-    uncommitted_changes.update(cx, |uncommitted_changes, cx| {
+    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
         assert_hunks(
-            uncommitted_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &uncommitted_changes.base_text.as_ref().unwrap().text(),
+            &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(),
             &[
                 (0..1, "", "// print goodbye\n"),
                 (
@@ -5821,12 +5821,12 @@ async fn test_uncommitted_changes_for_buffer(cx: &mut gpui::TestAppContext) {
     );
 
     cx.run_until_parked();
-    uncommitted_changes.update(cx, |uncommitted_changes, cx| {
+    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
         assert_hunks(
-            uncommitted_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &uncommitted_changes.base_text.as_ref().unwrap().text(),
+            &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(),
             &[(2..3, "", "    println!(\"goodbye world\");\n")],
         );
     });
@@ -5874,20 +5874,20 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let uncommitted_changes = project
+    let uncommitted_diff = project
         .update(cx, |project, cx| {
-            project.open_uncommitted_changes(buffer.clone(), cx)
+            project.open_uncommitted_diff(buffer.clone(), cx)
         })
         .await
         .unwrap();
 
     cx.run_until_parked();
-    uncommitted_changes.update(cx, |uncommitted_changes, cx| {
+    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
         assert_hunks(
-            uncommitted_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &uncommitted_changes.base_text.as_ref().unwrap().text(),
+            &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(),
             &[(
                 1..2,
                 "    println!(\"hello from HEAD\");\n",

crates/proto/proto/zed.proto 🔗

@@ -304,8 +304,8 @@ message Envelope {
         SyncExtensionsResponse sync_extensions_response = 286;
         InstallExtension install_extension = 287;
 
-        OpenUnstagedChanges open_unstaged_changes = 288;
-        OpenUnstagedChangesResponse open_unstaged_changes_response = 289;
+        OpenUnstagedDiff open_unstaged_diff = 288;
+        OpenUnstagedDiffResponse open_unstaged_diff_response = 289;
 
         RegisterBufferWithLanguageServers register_buffer_with_language_servers = 290;
 
@@ -314,8 +314,8 @@ message Envelope {
         Commit commit = 295;
         OpenCommitMessageBuffer open_commit_message_buffer = 296;
 
-        OpenUncommittedChanges open_uncommitted_changes = 297;
-        OpenUncommittedChangesResponse open_uncommitted_changes_response = 298; // current max
+        OpenUncommittedDiff open_uncommitted_diff = 297;
+        OpenUncommittedDiffResponse open_uncommitted_diff_response = 298; // current max
     }
 
     reserved 87 to 88;
@@ -2062,21 +2062,21 @@ message UpdateDiffBases {
     Mode mode = 5;
 }
 
-message OpenUnstagedChanges {
+message OpenUnstagedDiff {
     uint64 project_id = 1;
     uint64 buffer_id = 2;
 }
 
-message OpenUnstagedChangesResponse {
+message OpenUnstagedDiffResponse {
     optional string staged_text = 1;
 }
 
-message OpenUncommittedChanges {
+message OpenUncommittedDiff {
     uint64 project_id = 1;
     uint64 buffer_id = 2;
 }
 
-message OpenUncommittedChangesResponse {
+message OpenUncommittedDiffResponse {
     enum Mode {
         INDEX_MATCHES_HEAD = 0;
         INDEX_AND_HEAD = 1;

crates/proto/src/proto.rs 🔗

@@ -219,10 +219,10 @@ messages!(
     (GetImplementationResponse, Background),
     (GetLlmToken, Background),
     (GetLlmTokenResponse, Background),
-    (OpenUnstagedChanges, Foreground),
-    (OpenUnstagedChangesResponse, Foreground),
-    (OpenUncommittedChanges, Foreground),
-    (OpenUncommittedChangesResponse, Foreground),
+    (OpenUnstagedDiff, Foreground),
+    (OpenUnstagedDiffResponse, Foreground),
+    (OpenUncommittedDiff, Foreground),
+    (OpenUncommittedDiffResponse, Foreground),
     (GetUsers, Foreground),
     (Hello, Foreground),
     (IncomingCall, Foreground),
@@ -424,8 +424,8 @@ request_messages!(
     (GetProjectSymbols, GetProjectSymbolsResponse),
     (GetReferences, GetReferencesResponse),
     (GetSignatureHelp, GetSignatureHelpResponse),
-    (OpenUnstagedChanges, OpenUnstagedChangesResponse),
-    (OpenUncommittedChanges, OpenUncommittedChangesResponse),
+    (OpenUnstagedDiff, OpenUnstagedDiffResponse),
+    (OpenUncommittedDiff, OpenUncommittedDiffResponse),
     (GetSupermavenApiKey, GetSupermavenApiKeyResponse),
     (GetTypeDefinition, GetTypeDefinitionResponse),
     (LinkedEditingRange, LinkedEditingRangeResponse),
@@ -546,8 +546,8 @@ entity_messages!(
     GetProjectSymbols,
     GetReferences,
     GetSignatureHelp,
-    OpenUnstagedChanges,
-    OpenUncommittedChanges,
+    OpenUnstagedDiff,
+    OpenUncommittedDiff,
     GetTypeDefinition,
     InlayHints,
     JoinProject,

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -84,18 +84,15 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
         })
         .await
         .unwrap();
-    let change_set = project
+    let diff = project
         .update(cx, |project, cx| {
-            project.open_unstaged_changes(buffer.clone(), cx)
+            project.open_unstaged_diff(buffer.clone(), cx)
         })
         .await
         .unwrap();
 
-    change_set.update(cx, |change_set, _| {
-        assert_eq!(
-            change_set.base_text_string().unwrap(),
-            "fn one() -> usize { 0 }"
-        );
+    diff.update(cx, |diff, _| {
+        assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
     });
 
     buffer.update(cx, |buffer, cx| {
@@ -155,9 +152,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
         &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())],
     );
     cx.executor().run_until_parked();
-    change_set.update(cx, |change_set, _| {
+    diff.update(cx, |diff, _| {
         assert_eq!(
-            change_set.base_text_string().unwrap(),
+            diff.base_text_string().unwrap(),
             "fn one() -> usize { 100 }"
         );
     });
@@ -1239,18 +1236,17 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
         })
         .await
         .unwrap();
-    let change_set = project
+    let diff = project
         .update(cx, |project, cx| {
-            project.open_uncommitted_changes(buffer.clone(), cx)
+            project.open_uncommitted_diff(buffer.clone(), cx)
         })
         .await
         .unwrap();
 
-    change_set.read_with(cx, |change_set, cx| {
-        assert_eq!(change_set.base_text_string().unwrap(), text_1);
+    diff.read_with(cx, |diff, cx| {
+        assert_eq!(diff.base_text_string().unwrap(), text_1);
         assert_eq!(
-            change_set
-                .unstaged_change_set
+            diff.unstaged_diff
                 .as_ref()
                 .unwrap()
                 .read(cx)
@@ -1267,11 +1263,10 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
     );
 
     cx.executor().run_until_parked();
-    change_set.read_with(cx, |change_set, cx| {
-        assert_eq!(change_set.base_text_string().unwrap(), text_1);
+    diff.read_with(cx, |diff, cx| {
+        assert_eq!(diff.base_text_string().unwrap(), text_1);
         assert_eq!(
-            change_set
-                .unstaged_change_set
+            diff.unstaged_diff
                 .as_ref()
                 .unwrap()
                 .read(cx)
@@ -1288,11 +1283,10 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC
     );
 
     cx.executor().run_until_parked();
-    change_set.read_with(cx, |change_set, cx| {
-        assert_eq!(change_set.base_text_string().unwrap(), text_2);
+    diff.read_with(cx, |diff, cx| {
+        assert_eq!(diff.base_text_string().unwrap(), text_2);
         assert_eq!(
-            change_set
-                .unstaged_change_set
+            diff.unstaged_diff
                 .as_ref()
                 .unwrap()
                 .read(cx)

crates/worktree/src/worktree.rs 🔗

@@ -5386,7 +5386,7 @@ fn send_status_update_inner(
 
     let new_snapshot = state.snapshot.clone();
     let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone());
-    let changes = build_change_set(phase, &old_snapshot, &new_snapshot, &state.changed_paths);
+    let changes = build_diff(phase, &old_snapshot, &new_snapshot, &state.changed_paths);
     state.changed_paths.clear();
 
     status_updates_tx
@@ -5399,7 +5399,7 @@ fn send_status_update_inner(
         .is_ok()
 }
 
-fn build_change_set(
+fn build_diff(
     phase: BackgroundScannerPhase,
     old_snapshot: &Snapshot,
     new_snapshot: &Snapshot,