Rework diff rendering to allow putting the cursor into deleted text, soft-wrapping and scrolling deleted text correctly (#22994)

Max Brunsfeld , Conrad , Cole , Mikayla , Conrad Irwin , Michael , Agus , and JoΓ£o created

Closes #12553

* [x] Fix `diff_hunk_before`
* [x] Fix failure to show deleted text when expanding hunk w/ cursor on
second line of the hunk
* [x] Failure to expand diff hunk below the cursor.
* [x] Delete the whole file, and expand the diff. Backspace over the
deleted hunk, panic!
* [x] Go-to-line now counts the diff hunks, but it should not
* [x] backspace at the beginning of a deleted hunk deletes too much text
* [x] Indent guides are rendered incorrectly 
* [ ] Fix randomized multi buffer tests

Maybe:
* [ ] Buffer search should include deleted text (in vim mode it turns
out I use `/x` all the time to jump to the next x I can see).
* [ ] vim: should refuse to switch into insert mode if selection is
fully within a diff.
* [ ] vim `o` command when cursor is on last line of deleted hunk.
* [ ] vim `shift-o` on first line of deleted hunk moves cursor but
doesn't insert line
* [x] `enter` at end of diff hunk inserts a new line but doesn't move
cursor
* [x] (`shift-enter` at start of diff hunk does nothing)
* [ ] Inserting a line just before an expanded hunk collapses it

Release Notes:


- Improved diff rendering, allowing you to navigate with your cursor
inside of deleted text in diff hunks.

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: JoΓ£o <joao@zed.dev>

Change summary

Cargo.lock                                                    |    6 
assets/keymaps/default-linux.json                             |    2 
assets/keymaps/default-macos.json                             |    2 
assets/keymaps/vim.json                                       |    2 
crates/assistant/src/inline_assistant.rs                      |   35 
crates/assistant2/src/buffer_codegen.rs                       |   12 
crates/assistant2/src/inline_assistant.rs                     |   22 
crates/collab/src/tests/editor_tests.rs                       |   33 
crates/collab/src/tests/integration_tests.rs                  |   16 
crates/collab/src/tests/random_project_collaboration_tests.rs |    4 
crates/diagnostics/src/items.rs                               |   10 
crates/editor/src/actions.rs                                  |    2 
crates/editor/src/display_map.rs                              |   23 
crates/editor/src/display_map/block_map.rs                    |   99 
crates/editor/src/display_map/fold_map.rs                     |   63 
crates/editor/src/display_map/inlay_map.rs                    |   31 
crates/editor/src/display_map/tab_map.rs                      |    4 
crates/editor/src/display_map/wrap_map.rs                     |   40 
crates/editor/src/editor.rs                                   |  571 +
crates/editor/src/editor_tests.rs                             |  672 +
crates/editor/src/element.rs                                  |  617 +
crates/editor/src/git/blame.rs                                |  134 
crates/editor/src/git/project_diff.rs                         |   46 
crates/editor/src/hover_popover.rs                            |   22 
crates/editor/src/hunk_diff.rs                                | 1505 -----
crates/editor/src/indent_guides.rs                            |   56 
crates/editor/src/items.rs                                    |   77 
crates/editor/src/proposed_changes_editor.rs                  |   26 
crates/editor/src/selections_collection.rs                    |    9 
crates/editor/src/signature_help.rs                           |   16 
crates/editor/src/tasks.rs                                    |    2 
crates/editor/src/test/editor_lsp_test_context.rs             |    7 
crates/editor/src/test/editor_test_context.rs                 |  164 
crates/file_finder/src/file_finder.rs                         |   10 
crates/git/src/diff.rs                                        |   34 
crates/go_to_line/Cargo.toml                                  |    1 
crates/go_to_line/src/cursor_position.rs                      |   22 
crates/go_to_line/src/go_to_line.rs                           |  132 
crates/language/src/buffer.rs                                 |  300 
crates/language/src/buffer_tests.rs                           |   88 
crates/language_tools/src/syntax_tree_view.rs                 |   14 
crates/multi_buffer/Cargo.toml                                |    7 
crates/multi_buffer/src/anchor.rs                             |   83 
crates/multi_buffer/src/multi_buffer.rs                       |  616 +
crates/multi_buffer/src/multi_buffer_tests.rs                 | 1187 ++-
crates/multi_buffer/src/position.rs                           |  264 
crates/outline_panel/src/outline_panel.rs                     |   28 
crates/project/src/buffer_store.rs                            |  120 
crates/project/src/lsp_store.rs                               |   11 
crates/project/src/project_tests.rs                           |    4 
crates/project/src/task_store.rs                              |   12 
crates/remote_server/src/remote_editing_tests.rs              |    8 
crates/rope/src/rope.rs                                       |  154 
crates/rope/src/unclipped.rs                                  |   16 
crates/sum_tree/src/sum_tree.rs                               |   19 
crates/terminal_view/src/terminal_view.rs                     |   17 
crates/text/src/patch.rs                                      |    1 
crates/text/src/text.rs                                       |   38 
crates/vim/src/command.rs                                     |   53 
crates/vim/src/motion.rs                                      |  111 
crates/vim/src/object.rs                                      |    6 
crates/workspace/src/workspace.rs                             |    7 
crates/zed/src/zed.rs                                         |    6 
crates/zed/src/zed/open_listener.rs                           |    9 
64 files changed, 3,661 insertions(+), 4,047 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -7754,15 +7754,21 @@ dependencies = [
  "ctor",
  "env_logger 0.11.6",
  "futures 0.3.31",
+ "git",
  "gpui",
+ "indoc",
  "itertools 0.14.0",
  "language",
  "log",
  "parking_lot",
+ "pretty_assertions",
+ "project",
  "rand 0.8.5",
+ "rope",
  "serde",
  "settings",
  "smallvec",
+ "smol",
  "sum_tree",
  "text",
  "theme",

assets/keymaps/default-linux.json πŸ”—

@@ -117,7 +117,7 @@
       "ctrl-alt-space": "editor::ShowCharacterPalette",
       "ctrl-;": "editor::ToggleLineNumbers",
       "ctrl-k ctrl-r": "editor::RevertSelectedHunks",
-      "ctrl-'": "editor::ToggleHunkDiff",
+      "ctrl-'": "editor::ToggleSelectedDiffHunks",
       "ctrl-\"": "editor::ExpandAllHunkDiffs",
       "ctrl-i": "editor::ShowSignatureHelp",
       "alt-g b": "editor::ToggleGitBlame",

assets/keymaps/default-macos.json πŸ”—

@@ -127,7 +127,7 @@
       "ctrl-cmd-space": "editor::ShowCharacterPalette",
       "cmd-;": "editor::ToggleLineNumbers",
       "cmd-alt-z": "editor::RevertSelectedHunks",
-      "cmd-'": "editor::ToggleHunkDiff",
+      "cmd-'": "editor::ToggleSelectedDiffHunks",
       "cmd-\"": "editor::ExpandAllHunkDiffs",
       "cmd-alt-g b": "editor::ToggleGitBlame",
       "cmd-i": "editor::ShowSignatureHelp",

assets/keymaps/vim.json πŸ”—

@@ -436,7 +436,7 @@
     "bindings": {
       "d": "vim::CurrentLine",
       "s": ["vim::PushOperator", "DeleteSurrounds"],
-      "o": "editor::ToggleHunkDiff", // "d o"
+      "o": "editor::ToggleSelectedDiffHunks", // "d o"
       "p": "editor::RevertSelectedHunks" // "d p"
     }
   },

crates/assistant/src/inline_assistant.rs πŸ”—

@@ -250,22 +250,19 @@ impl InlineAssistant {
         let newest_selection = newest_selection.unwrap();
 
         let mut codegen_ranges = Vec::new();
-        for (excerpt_id, buffer, buffer_range) in
-            snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
+        for (buffer, buffer_range, excerpt_id) in
+            snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| {
                 snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
             }))
         {
-            let start = Anchor {
-                buffer_id: Some(buffer.remote_id()),
-                excerpt_id,
-                text_anchor: buffer.anchor_before(buffer_range.start),
-            };
-            let end = Anchor {
-                buffer_id: Some(buffer.remote_id()),
+            let start = buffer.anchor_before(buffer_range.start);
+            let end = buffer.anchor_after(buffer_range.end);
+
+            codegen_ranges.push(Anchor::range_in_buffer(
                 excerpt_id,
-                text_anchor: buffer.anchor_after(buffer_range.end),
-            };
-            codegen_ranges.push(start..end);
+                buffer.remote_id(),
+                start..end,
+            ));
 
             if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
                 self.telemetry.report_assistant_event(AssistantEvent {
@@ -823,7 +820,7 @@ impl InlineAssistant {
                     let ranges = multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone());
                     ranges
                         .first()
-                        .and_then(|(excerpt, _)| excerpt.buffer().language())
+                        .and_then(|(buffer, _, _)| buffer.language())
                         .map(|language| language.name())
                 });
                 report_assistant_event(
@@ -2648,17 +2645,17 @@ impl CodegenAlternative {
     ) -> Self {
         let snapshot = multi_buffer.read(cx).snapshot(cx);
 
-        let (old_excerpt, _) = snapshot
+        let (buffer, _, _) = snapshot
             .range_to_buffer_ranges(range.clone())
             .pop()
             .unwrap();
         let old_buffer = cx.new_model(|cx| {
-            let text = old_excerpt.buffer().as_rope().clone();
-            let line_ending = old_excerpt.buffer().line_ending();
-            let language = old_excerpt.buffer().language().cloned();
+            let text = buffer.as_rope().clone();
+            let line_ending = buffer.line_ending();
+            let language = buffer.language().cloned();
             let language_registry = multi_buffer
                 .read(cx)
-                .buffer(old_excerpt.buffer_id())
+                .buffer(buffer.remote_id())
                 .unwrap()
                 .read(cx)
                 .language_registry();
@@ -2898,7 +2895,7 @@ impl CodegenAlternative {
             let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
             ranges
                 .first()
-                .and_then(|(excerpt, _)| excerpt.buffer().language())
+                .and_then(|(buffer, _, _)| buffer.language())
                 .map(|language| language.name())
         };
 

crates/assistant2/src/buffer_codegen.rs πŸ”—

@@ -255,17 +255,17 @@ impl CodegenAlternative {
     ) -> Self {
         let snapshot = buffer.read(cx).snapshot(cx);
 
-        let (old_excerpt, _) = snapshot
+        let (old_buffer, _, _) = snapshot
             .range_to_buffer_ranges(range.clone())
             .pop()
             .unwrap();
         let old_buffer = cx.new_model(|cx| {
-            let text = old_excerpt.buffer().as_rope().clone();
-            let line_ending = old_excerpt.buffer().line_ending();
-            let language = old_excerpt.buffer().language().cloned();
+            let text = old_buffer.as_rope().clone();
+            let line_ending = old_buffer.line_ending();
+            let language = old_buffer.language().cloned();
             let language_registry = buffer
                 .read(cx)
-                .buffer(old_excerpt.buffer_id())
+                .buffer(old_buffer.remote_id())
                 .unwrap()
                 .read(cx)
                 .language_registry();
@@ -475,7 +475,7 @@ impl CodegenAlternative {
             let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
             ranges
                 .first()
-                .and_then(|(excerpt, _)| excerpt.buffer().language())
+                .and_then(|(buffer, _, _)| buffer.language())
                 .map(|language| language.name())
         };
 

crates/assistant2/src/inline_assistant.rs πŸ”—

@@ -320,22 +320,18 @@ impl InlineAssistant {
         let newest_selection = newest_selection.unwrap();
 
         let mut codegen_ranges = Vec::new();
-        for (excerpt_id, buffer, buffer_range) in
-            snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
+        for (buffer, buffer_range, excerpt_id) in
+            snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| {
                 snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
             }))
         {
-            let start = Anchor {
-                buffer_id: Some(buffer.remote_id()),
+            let anchor_range = Anchor::range_in_buffer(
                 excerpt_id,
-                text_anchor: buffer.anchor_before(buffer_range.start),
-            };
-            let end = Anchor {
-                buffer_id: Some(buffer.remote_id()),
-                excerpt_id,
-                text_anchor: buffer.anchor_after(buffer_range.end),
-            };
-            codegen_ranges.push(start..end);
+                buffer.remote_id(),
+                buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end),
+            );
+
+            codegen_ranges.push(anchor_range);
 
             if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
                 self.telemetry.report_assistant_event(AssistantEvent {
@@ -901,7 +897,7 @@ impl InlineAssistant {
                     let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
                     ranges
                         .first()
-                        .and_then(|(excerpt, _)| excerpt.buffer().language())
+                        .and_then(|(buffer, _, _)| buffer.language())
                         .map(|language| language.name())
                 });
                 report_assistant_event(

crates/collab/src/tests/editor_tests.rs πŸ”—

@@ -10,7 +10,7 @@ use editor::{
         ToggleCodeActions, Undo,
     },
     test::editor_test_context::{AssertionContextManager, EditorTestContext},
-    Editor,
+    Editor, RowInfo,
 };
 use fs::Fs;
 use futures::StreamExt;
@@ -20,7 +20,6 @@ use language::{
     language_settings::{AllLanguageSettings, InlayHintSettings},
     FakeLspAdapter,
 };
-use multi_buffer::MultiBufferRow;
 use project::{
     project_settings::{InlineBlameSettings, ProjectSettings},
     SERVER_PROGRESS_THROTTLE_TIMEOUT,
@@ -2019,7 +2018,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
         let blame = editor_b.blame().expect("editor_b should have blame now");
         let entries = blame.update(cx, |blame, cx| {
             blame
-                .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
+                .blame_for_rows(
+                    &(0..4)
+                        .map(|row| RowInfo {
+                            buffer_row: Some(row),
+                            ..Default::default()
+                        })
+                        .collect::<Vec<_>>(),
+                    cx,
+                )
                 .collect::<Vec<_>>()
         });
 
@@ -2058,7 +2065,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
         let blame = editor_b.blame().expect("editor_b should have blame now");
         let entries = blame.update(cx, |blame, cx| {
             blame
-                .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
+                .blame_for_rows(
+                    &(0..4)
+                        .map(|row| RowInfo {
+                            buffer_row: Some(row),
+                            ..Default::default()
+                        })
+                        .collect::<Vec<_>>(),
+                    cx,
+                )
                 .collect::<Vec<_>>()
         });
 
@@ -2085,7 +2100,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
         let blame = editor_b.blame().expect("editor_b should have blame now");
         let entries = blame.update(cx, |blame, cx| {
             blame
-                .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
+                .blame_for_rows(
+                    &(0..4)
+                        .map(|row| RowInfo {
+                            buffer_row: Some(row),
+                            ..Default::default()
+                        })
+                        .collect::<Vec<_>>(),
+                    cx,
+                )
                 .collect::<Vec<_>>()
         });
 

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -2593,7 +2593,7 @@ async fn test_git_diff_base_change(
     change_set_local_a.read_with(cx_a, |change_set, cx| {
         let buffer = buffer_local_a.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2621,7 +2621,7 @@ async fn test_git_diff_base_change(
     change_set_remote_a.read_with(cx_b, |change_set, cx| {
         let buffer = buffer_remote_a.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2643,7 +2643,7 @@ async fn test_git_diff_base_change(
     change_set_local_a.read_with(cx_a, |change_set, cx| {
         let buffer = buffer_local_a.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(new_diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2657,7 +2657,7 @@ async fn test_git_diff_base_change(
     change_set_remote_a.read_with(cx_b, |change_set, cx| {
         let buffer = buffer_remote_a.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(new_diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2703,7 +2703,7 @@ async fn test_git_diff_base_change(
     change_set_local_b.read_with(cx_a, |change_set, cx| {
         let buffer = buffer_local_b.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2730,7 +2730,7 @@ async fn test_git_diff_base_change(
     change_set_remote_b.read_with(cx_b, |change_set, cx| {
         let buffer = buffer_remote_b.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2752,7 +2752,7 @@ async fn test_git_diff_base_change(
     change_set_local_b.read_with(cx_a, |change_set, cx| {
         let buffer = buffer_local_b.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(new_diff_base.as_str())
         );
         git::diff::assert_hunks(
@@ -2766,7 +2766,7 @@ async fn test_git_diff_base_change(
     change_set_remote_b.read_with(cx_b, |change_set, cx| {
         let buffer = buffer_remote_b.read(cx);
         assert_eq!(
-            change_set.base_text_string(cx).as_deref(),
+            change_set.base_text_string().as_deref(),
             Some(new_diff_base.as_str())
         );
         git::diff::assert_hunks(

crates/collab/src/tests/random_project_collaboration_tests.rs πŸ”—

@@ -1342,7 +1342,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                             .get_unstaged_changes(host_buffer.read(cx).remote_id())
                             .unwrap()
                             .read(cx)
-                            .base_text_string(cx)
+                            .base_text_string()
                     });
                     let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
                         project
@@ -1351,7 +1351,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                             .get_unstaged_changes(guest_buffer.read(cx).remote_id())
                             .unwrap()
                             .read(cx)
-                            .base_text_string(cx)
+                            .base_text_string()
                     });
                     assert_eq!(
                             guest_diff_base, host_diff_base,

crates/diagnostics/src/items.rs πŸ”—

@@ -1,11 +1,11 @@
 use std::time::Duration;
 
-use editor::{AnchorRangeExt, Editor};
+use editor::Editor;
 use gpui::{
     EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
     ViewContext, WeakView,
 };
-use language::{Diagnostic, DiagnosticEntry};
+use language::Diagnostic;
 use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
 
@@ -148,11 +148,7 @@ impl DiagnosticIndicator {
             (buffer, cursor_position)
         });
         let new_diagnostic = buffer
-            .diagnostics_in_range(cursor_position..cursor_position, false)
-            .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
-                diagnostic,
-                range: range.to_offset(&buffer),
-            })
+            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
             .filter(|entry| !entry.range.is_empty())
             .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
             .map(|entry| entry.diagnostic);

crates/editor/src/actions.rs πŸ”—

@@ -372,7 +372,7 @@ gpui::actions!(
         ToggleAutoSignatureHelp,
         ToggleGitBlame,
         ToggleGitBlameInline,
-        ToggleHunkDiff,
+        ToggleSelectedDiffHunks,
         ToggleIndentGuides,
         ToggleInlayHints,
         ToggleInlineCompletions,

crates/editor/src/display_map.rs πŸ”—

@@ -30,8 +30,8 @@ use crate::{
     hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
 };
 pub use block_map::{
-    Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap,
-    BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
+    Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
+    BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock,
     StickyHeaderExcerpt,
 };
 use block_map::{BlockRow, BlockSnapshot};
@@ -54,7 +54,7 @@ use language::{
 use lsp::DiagnosticSeverity;
 use multi_buffer::{
     Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
-    ToOffset, ToPoint,
+    RowInfo, ToOffset, ToPoint,
 };
 use serde::Deserialize;
 use std::{
@@ -68,7 +68,7 @@ use std::{
 };
 use sum_tree::{Bias, TreeMap};
 use tab_map::{TabMap, TabSnapshot};
-use text::LineIndent;
+use text::{BufferId, LineIndent};
 use ui::{px, SharedString, WindowContext};
 use unicode_segmentation::UnicodeSegmentation;
 use wrap_map::{WrapMap, WrapSnapshot};
@@ -367,10 +367,14 @@ impl DisplayMap {
         block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
     }
 
-    pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
+    pub(crate) fn is_buffer_folded(&self, buffer_id: language::BufferId) -> bool {
         self.block_map.folded_buffers.contains(&buffer_id)
     }
 
+    pub(crate) fn folded_buffers(&self) -> &HashSet<BufferId> {
+        &self.block_map.folded_buffers
+    }
+
     pub fn insert_creases(
         &mut self,
         creases: impl IntoIterator<Item = Crease<Anchor>>,
@@ -716,13 +720,8 @@ impl DisplaySnapshot {
         self.buffer_snapshot.len() == 0
     }
 
-    pub fn buffer_rows(
-        &self,
-        start_row: DisplayRow,
-    ) -> impl Iterator<Item = Option<MultiBufferRow>> + '_ {
-        self.block_snapshot
-            .buffer_rows(BlockRow(start_row.0))
-            .map(|row| row.map(MultiBufferRow))
+    pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator<Item = RowInfo> + '_ {
+        self.block_snapshot.row_infos(BlockRow(start_row.0))
     }
 
     pub fn widest_line_number(&self) -> u32 {

crates/editor/src/display_map/block_map.rs πŸ”—

@@ -7,8 +7,8 @@ use collections::{Bound, HashMap, HashSet};
 use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext};
 use language::{Chunk, Patch, Point};
 use multi_buffer::{
-    Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToOffset,
-    ToPoint as _,
+    Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, RowInfo,
+    ToOffset, ToPoint as _,
 };
 use parking_lot::Mutex;
 use std::{
@@ -399,9 +399,9 @@ pub struct BlockChunks<'a> {
 }
 
 #[derive(Clone)]
-pub struct BlockBufferRows<'a> {
+pub struct BlockRows<'a> {
     transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
-    input_buffer_rows: wrap_map::WrapBufferRows<'a>,
+    input_rows: wrap_map::WrapRows<'a>,
     output_row: BlockRow,
     started: bool,
 }
@@ -777,14 +777,12 @@ impl BlockMap {
             if let Some(new_buffer_id) = new_buffer_id {
                 let first_excerpt = excerpt_boundary.next.clone().unwrap();
                 if folded_buffers.contains(&new_buffer_id) {
-                    let mut buffer_end = Point::new(excerpt_boundary.row.0, 0)
-                        + excerpt_boundary.next.as_ref().unwrap().text_summary.lines;
+                    let mut last_excerpt_end_row = first_excerpt.end_row;
 
                     while let Some(next_boundary) = boundaries.peek() {
                         if let Some(next_excerpt_boundary) = &next_boundary.next {
                             if next_excerpt_boundary.buffer_id == new_buffer_id {
-                                buffer_end = Point::new(next_boundary.row.0, 0)
-                                    + next_excerpt_boundary.text_summary.lines;
+                                last_excerpt_end_row = next_excerpt_boundary.end_row;
                             } else {
                                 break;
                             }
@@ -793,7 +791,15 @@ impl BlockMap {
                         boundaries.next();
                     }
 
-                    let wrap_end_row = wrap_snapshot.make_wrap_point(buffer_end, Bias::Right).row();
+                    let wrap_end_row = wrap_snapshot
+                        .make_wrap_point(
+                            Point::new(
+                                last_excerpt_end_row.0,
+                                buffer.line_len(last_excerpt_end_row),
+                            ),
+                            Bias::Right,
+                        )
+                        .row();
 
                     return Some((
                         BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
@@ -1360,7 +1366,7 @@ impl BlockSnapshot {
         }
     }
 
-    pub(super) fn buffer_rows(&self, start_row: BlockRow) -> BlockBufferRows {
+    pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
         cursor.seek(&start_row, Bias::Right, &());
         let (output_start, input_start) = cursor.start();
@@ -1373,9 +1379,9 @@ impl BlockSnapshot {
             0
         };
         let input_start_row = input_start.0 + overshoot;
-        BlockBufferRows {
+        BlockRows {
             transforms: cursor,
-            input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row),
+            input_rows: self.wrap_snapshot.row_infos(input_start_row),
             output_row: start_row,
             started: false,
         }
@@ -1480,7 +1486,7 @@ impl BlockSnapshot {
             }
             BlockId::ExcerptBoundary(next_excerpt_id) => {
                 if let Some(next_excerpt_id) = next_excerpt_id {
-                    let excerpt_range = buffer.range_for_excerpt::<Point>(next_excerpt_id)?;
+                    let excerpt_range = buffer.range_for_excerpt(next_excerpt_id)?;
                     self.wrap_snapshot
                         .make_wrap_point(excerpt_range.start, Bias::Left)
                 } else {
@@ -1488,10 +1494,9 @@ impl BlockSnapshot {
                         .make_wrap_point(buffer.max_point(), Bias::Left)
                 }
             }
-            BlockId::FoldedBuffer(excerpt_id) => self.wrap_snapshot.make_wrap_point(
-                buffer.range_for_excerpt::<Point>(excerpt_id)?.start,
-                Bias::Left,
-            ),
+            BlockId::FoldedBuffer(excerpt_id) => self
+                .wrap_snapshot
+                .make_wrap_point(buffer.range_for_excerpt(excerpt_id)?.start, Bias::Left),
         };
         let wrap_row = WrapRow(wrap_point.row());
 
@@ -1832,8 +1837,8 @@ impl<'a> Iterator for BlockChunks<'a> {
     }
 }
 
-impl<'a> Iterator for BlockBufferRows<'a> {
-    type Item = Option<u32>;
+impl<'a> Iterator for BlockRows<'a> {
+    type Item = RowInfo;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.started {
@@ -1862,7 +1867,7 @@ impl<'a> Iterator for BlockBufferRows<'a> {
                 .as_ref()
                 .map_or(true, |block| block.is_replacement())
             {
-                self.input_buffer_rows.seek(self.transforms.start().1 .0);
+                self.input_rows.seek(self.transforms.start().1 .0);
             }
         }
 
@@ -1870,15 +1875,15 @@ impl<'a> Iterator for BlockBufferRows<'a> {
         if let Some(block) = transform.block.as_ref() {
             if block.is_replacement() && self.transforms.start().0 == self.output_row {
                 if matches!(block, Block::FoldedBuffer { .. }) {
-                    Some(None)
+                    Some(RowInfo::default())
                 } else {
-                    Some(self.input_buffer_rows.next().unwrap())
+                    Some(self.input_rows.next().unwrap())
                 }
             } else {
-                Some(None)
+                Some(RowInfo::default())
             }
         } else {
-            Some(self.input_buffer_rows.next().unwrap())
+            Some(self.input_rows.next().unwrap())
         }
     }
 }
@@ -2153,7 +2158,10 @@ mod tests {
         );
 
         assert_eq!(
-            snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            snapshot
+                .row_infos(BlockRow(0))
+                .map(|row_info| row_info.buffer_row)
+                .collect::<Vec<_>>(),
             &[
                 Some(0),
                 None,
@@ -2603,7 +2611,10 @@ mod tests {
             "\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n"
         );
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![
                 None,
                 None,
@@ -2679,7 +2690,10 @@ mod tests {
             "\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
         );
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![
                 None,
                 None,
@@ -2754,7 +2768,10 @@ mod tests {
             "\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
         );
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![
                 None,
                 None,
@@ -2819,7 +2836,10 @@ mod tests {
         );
         assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n");
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![
                 None,
                 None,
@@ -2873,7 +2893,10 @@ mod tests {
             "Should have extra newline for 111 buffer, due to a new block added when it was folded"
         );
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![
                 None,
                 None,
@@ -2927,7 +2950,10 @@ mod tests {
             "Should have a single, first buffer left after folding"
         );
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![
                 None,
                 None,
@@ -2997,7 +3023,10 @@ mod tests {
         );
         assert_eq!(blocks_snapshot.text(), "\n");
         assert_eq!(
-            blocks_snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
+            blocks_snapshot
+                .row_infos(BlockRow(0))
+                .map(|i| i.buffer_row)
+                .collect::<Vec<_>>(),
             vec![None, None],
             "When fully folded, should be no buffer rows"
         );
@@ -3295,7 +3324,8 @@ mod tests {
             let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
 
             let input_buffer_rows = buffer_snapshot
-                .buffer_rows(MultiBufferRow(0))
+                .row_infos(MultiBufferRow(0))
+                .map(|row| row.buffer_row)
                 .collect::<Vec<_>>();
             let mut expected_buffer_rows = Vec::new();
             let mut expected_text = String::new();
@@ -3450,7 +3480,8 @@ mod tests {
                 );
                 assert_eq!(
                     blocks_snapshot
-                        .buffer_rows(BlockRow(start_row as u32))
+                        .row_infos(BlockRow(start_row as u32))
+                        .map(|row_info| row_info.buffer_row)
                         .collect::<Vec<_>>(),
                     &expected_buffer_rows[start_row..],
                     "incorrect buffer_rows starting at row {:?}",

crates/editor/src/display_map/fold_map.rs πŸ”—

@@ -4,7 +4,9 @@ use super::{
 };
 use gpui::{AnyElement, ElementId, WindowContext};
 use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
-use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
+use multi_buffer::{
+    Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset,
+};
 use std::{
     any::TypeId,
     cmp::{self, Ordering},
@@ -336,9 +338,7 @@ impl FoldMap {
             let mut folds = self.snapshot.folds.iter().peekable();
             while let Some(fold) = folds.next() {
                 if let Some(next_fold) = folds.peek() {
-                    let comparison = fold
-                        .range
-                        .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
+                    let comparison = fold.range.cmp(&next_fold.range, self.snapshot.buffer());
                     assert!(comparison.is_le());
                 }
             }
@@ -578,6 +578,10 @@ pub struct FoldSnapshot {
 }
 
 impl FoldSnapshot {
+    pub fn buffer(&self) -> &MultiBufferSnapshot {
+        &self.inlay_snapshot.buffer
+    }
+
     #[cfg(test)]
     pub fn text(&self) -> String {
         self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
@@ -673,7 +677,7 @@ impl FoldSnapshot {
         (line_end - line_start) as u32
     }
 
-    pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows {
+    pub fn row_infos(&self, start_row: u32) -> FoldRows {
         if start_row > self.transforms.summary().output.lines.row {
             panic!("invalid display row {}", start_row);
         }
@@ -684,11 +688,11 @@ impl FoldSnapshot {
 
         let overshoot = fold_point.0 - cursor.start().0 .0;
         let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
-        let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
+        let input_rows = self.inlay_snapshot.row_infos(inlay_point.row());
 
-        FoldBufferRows {
+        FoldRows {
             fold_point,
-            input_buffer_rows,
+            input_rows,
             cursor,
         }
     }
@@ -843,8 +847,8 @@ fn push_isomorphic(transforms: &mut SumTree<Transform>, summary: TextSummary) {
     transforms.update_last(
         |last| {
             if !last.is_fold() {
-                last.summary.input += summary.clone();
-                last.summary.output += summary.clone();
+                last.summary.input += summary;
+                last.summary.output += summary;
                 did_merge = true;
             }
         },
@@ -854,7 +858,7 @@ fn push_isomorphic(transforms: &mut SumTree<Transform>, summary: TextSummary) {
         transforms.push(
             Transform {
                 summary: TransformSummary {
-                    input: summary.clone(),
+                    input: summary,
                     output: summary,
                 },
                 placeholder: None,
@@ -1134,25 +1138,25 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
 }
 
 #[derive(Clone)]
-pub struct FoldBufferRows<'a> {
+pub struct FoldRows<'a> {
     cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
-    input_buffer_rows: InlayBufferRows<'a>,
+    input_rows: InlayBufferRows<'a>,
     fold_point: FoldPoint,
 }
 
-impl<'a> FoldBufferRows<'a> {
+impl<'a> FoldRows<'a> {
     pub(crate) fn seek(&mut self, row: u32) {
         let fold_point = FoldPoint::new(row, 0);
         self.cursor.seek(&fold_point, Bias::Left, &());
         let overshoot = fold_point.0 - self.cursor.start().0 .0;
         let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot);
-        self.input_buffer_rows.seek(inlay_point.row());
+        self.input_rows.seek(inlay_point.row());
         self.fold_point = fold_point;
     }
 }
 
-impl<'a> Iterator for FoldBufferRows<'a> {
-    type Item = Option<u32>;
+impl<'a> Iterator for FoldRows<'a> {
+    type Item = RowInfo;
 
     fn next(&mut self) -> Option<Self::Item> {
         let mut traversed_fold = false;
@@ -1166,11 +1170,11 @@ impl<'a> Iterator for FoldBufferRows<'a> {
 
         if self.cursor.item().is_some() {
             if traversed_fold {
-                self.input_buffer_rows.seek(self.cursor.start().1.row());
-                self.input_buffer_rows.next();
+                self.input_rows.seek(self.cursor.start().1 .0.row);
+                self.input_rows.next();
             }
             *self.fold_point.row_mut() += 1;
-            self.input_buffer_rows.next()
+            self.input_rows.next()
         } else {
             None
         }
@@ -1683,12 +1687,12 @@ mod tests {
                     .row();
                 expected_buffer_rows.extend(
                     inlay_snapshot
-                        .buffer_rows(prev_row)
+                        .row_infos(prev_row)
                         .take((1 + fold_start - prev_row) as usize),
                 );
                 prev_row = 1 + fold_end;
             }
-            expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row));
+            expected_buffer_rows.extend(inlay_snapshot.row_infos(prev_row));
 
             assert_eq!(
                 expected_buffer_rows.len(),
@@ -1777,7 +1781,7 @@ mod tests {
             let mut fold_row = 0;
             while fold_row < expected_buffer_rows.len() as u32 {
                 assert_eq!(
-                    snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
+                    snapshot.row_infos(fold_row).collect::<Vec<_>>(),
                     expected_buffer_rows[(fold_row as usize)..],
                     "wrong buffer rows starting at fold row {}",
                     fold_row,
@@ -1892,10 +1896,19 @@ mod tests {
         let (snapshot, _) = map.read(inlay_snapshot, vec![]);
         assert_eq!(snapshot.text(), "aaβ‹―cccc\ndβ‹―eeeee\nffffff\n");
         assert_eq!(
-            snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            snapshot
+                .row_infos(0)
+                .map(|info| info.buffer_row)
+                .collect::<Vec<_>>(),
             [Some(0), Some(3), Some(5), Some(6)]
         );
-        assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
+        assert_eq!(
+            snapshot
+                .row_infos(3)
+                .map(|info| info.buffer_row)
+                .collect::<Vec<_>>(),
+            [Some(6)]
+        );
     }
 
     fn init_test(cx: &mut gpui::AppContext) {

crates/editor/src/display_map/inlay_map.rs πŸ”—

@@ -1,7 +1,9 @@
 use crate::{HighlightStyles, InlayId};
 use collections::BTreeSet;
 use language::{Chunk, Edit, Point, TextSummary};
-use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset};
+use multi_buffer::{
+    Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset,
+};
 use std::{
     cmp,
     ops::{Add, AddAssign, Range, Sub, SubAssign},
@@ -67,11 +69,11 @@ impl Inlay {
 impl sum_tree::Item for Transform {
     type Summary = TransformSummary;
 
-    fn summary(&self, _cx: &()) -> Self::Summary {
+    fn summary(&self, _: &()) -> Self::Summary {
         match self {
             Transform::Isomorphic(summary) => TransformSummary {
-                input: summary.clone(),
-                output: summary.clone(),
+                input: *summary,
+                output: *summary,
             },
             Transform::Inlay(inlay) => TransformSummary {
                 input: TextSummary::default(),
@@ -362,14 +364,14 @@ impl<'a> InlayBufferRows<'a> {
 }
 
 impl<'a> Iterator for InlayBufferRows<'a> {
-    type Item = Option<u32>;
+    type Item = RowInfo;
 
     fn next(&mut self) -> Option<Self::Item> {
         let buffer_row = if self.inlay_row == 0 {
             self.buffer_rows.next().unwrap()
         } else {
             match self.transforms.item()? {
-                Transform::Inlay(_) => None,
+                Transform::Inlay(_) => Default::default(),
                 Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
             }
         };
@@ -448,7 +450,7 @@ impl InlayMap {
                 new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
                 if let Some(Transform::Isomorphic(transform)) = cursor.item() {
                     if cursor.end(&()).0 == buffer_edit.old.start {
-                        push_isomorphic(&mut new_transforms, transform.clone());
+                        push_isomorphic(&mut new_transforms, *transform);
                         cursor.next(&());
                     }
                 }
@@ -892,7 +894,7 @@ impl InlaySnapshot {
     }
 
     pub fn text_summary(&self) -> TextSummary {
-        self.transforms.summary().output.clone()
+        self.transforms.summary().output
     }
 
     pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
@@ -945,7 +947,7 @@ impl InlaySnapshot {
         summary
     }
 
-    pub fn buffer_rows(&self, row: u32) -> InlayBufferRows<'_> {
+    pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> {
         let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
         let inlay_point = InlayPoint::new(row, 0);
         cursor.seek(&inlay_point, Bias::Left, &());
@@ -967,7 +969,7 @@ impl InlaySnapshot {
         InlayBufferRows {
             transforms: cursor,
             inlay_row: inlay_point.row(),
-            buffer_rows: self.buffer.buffer_rows(buffer_row),
+            buffer_rows: self.buffer.row_infos(buffer_row),
             max_buffer_row,
         }
     }
@@ -1477,7 +1479,10 @@ mod tests {
         );
         assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
         assert_eq!(
-            inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            inlay_snapshot
+                .row_infos(0)
+                .map(|info| info.buffer_row)
+                .collect::<Vec<_>>(),
             vec![Some(0), None, Some(1), None, None, Some(2)]
         );
     }
@@ -1548,7 +1553,7 @@ mod tests {
             }
             assert_eq!(inlay_snapshot.text(), expected_text.to_string());
 
-            let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::<Vec<_>>();
             assert_eq!(
                 expected_buffer_rows.len() as u32,
                 expected_text.max_point().row + 1
@@ -1556,7 +1561,7 @@ mod tests {
             for row_start in 0..expected_buffer_rows.len() {
                 assert_eq!(
                     inlay_snapshot
-                        .buffer_rows(row_start as u32)
+                        .row_infos(row_start as u32)
                         .collect::<Vec<_>>(),
                     &expected_buffer_rows[row_start..],
                     "incorrect buffer rows starting at {}",

crates/editor/src/display_map/tab_map.rs πŸ”—

@@ -272,8 +272,8 @@ impl TabSnapshot {
         }
     }
 
-    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
-        self.fold_snapshot.buffer_rows(row)
+    pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> {
+        self.fold_snapshot.row_infos(row)
     }
 
     #[cfg(test)]

crates/editor/src/display_map/wrap_map.rs πŸ”—

@@ -1,11 +1,11 @@
 use super::{
-    fold_map::FoldBufferRows,
+    fold_map::FoldRows,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
     Highlights,
 };
 use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
 use language::{Chunk, Point};
-use multi_buffer::MultiBufferSnapshot;
+use multi_buffer::{MultiBufferSnapshot, RowInfo};
 use smol::future::yield_now;
 use std::sync::LazyLock;
 use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
@@ -60,16 +60,16 @@ pub struct WrapChunks<'a> {
 }
 
 #[derive(Clone)]
-pub struct WrapBufferRows<'a> {
-    input_buffer_rows: FoldBufferRows<'a>,
-    input_buffer_row: Option<u32>,
+pub struct WrapRows<'a> {
+    input_buffer_rows: FoldRows<'a>,
+    input_buffer_row: RowInfo,
     output_row: u32,
     soft_wrapped: bool,
     max_output_row: u32,
     transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
 }
 
-impl<'a> WrapBufferRows<'a> {
+impl<'a> WrapRows<'a> {
     pub(crate) fn seek(&mut self, start_row: u32) {
         self.transforms
             .seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
@@ -717,7 +717,7 @@ impl WrapSnapshot {
         self.transforms.summary().output.longest_row
     }
 
-    pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
+    pub fn row_infos(&self, start_row: u32) -> WrapRows {
         let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
         transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
         let mut input_row = transforms.start().1.row();
@@ -725,9 +725,9 @@ impl WrapSnapshot {
             input_row += start_row - transforms.start().0.row();
         }
         let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
-        let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
+        let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
         let input_buffer_row = input_buffer_rows.next().unwrap();
-        WrapBufferRows {
+        WrapRows {
             transforms,
             input_buffer_row,
             input_buffer_rows,
@@ -847,7 +847,7 @@ impl WrapSnapshot {
             }
 
             let text = language::Rope::from(self.text().as_str());
-            let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
+            let mut input_buffer_rows = self.tab_snapshot.rows(0);
             let mut expected_buffer_rows = Vec::new();
             let mut prev_tab_row = 0;
             for display_row in 0..=self.max_point().row() {
@@ -855,7 +855,7 @@ impl WrapSnapshot {
                 if tab_point.row() == prev_tab_row && display_row != 0 {
                     expected_buffer_rows.push(None);
                 } else {
-                    expected_buffer_rows.push(input_buffer_rows.next().unwrap());
+                    expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row);
                 }
 
                 prev_tab_row = tab_point.row();
@@ -864,7 +864,8 @@ impl WrapSnapshot {
 
             for start_display_row in 0..expected_buffer_rows.len() {
                 assert_eq!(
-                    self.buffer_rows(start_display_row as u32)
+                    self.row_infos(start_display_row as u32)
+                        .map(|row_info| row_info.buffer_row)
                         .collect::<Vec<_>>(),
                     &expected_buffer_rows[start_display_row..],
                     "invalid buffer_rows({}..)",
@@ -958,8 +959,8 @@ impl<'a> Iterator for WrapChunks<'a> {
     }
 }
 
-impl<'a> Iterator for WrapBufferRows<'a> {
-    type Item = Option<u32>;
+impl<'a> Iterator for WrapRows<'a> {
+    type Item = RowInfo;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.output_row > self.max_output_row {
@@ -968,6 +969,7 @@ impl<'a> Iterator for WrapBufferRows<'a> {
 
         let buffer_row = self.input_buffer_row;
         let soft_wrapped = self.soft_wrapped;
+        let diff_status = self.input_buffer_row.diff_status;
 
         self.output_row += 1;
         self.transforms
@@ -979,7 +981,15 @@ impl<'a> Iterator for WrapBufferRows<'a> {
             self.soft_wrapped = true;
         }
 
-        Some(if soft_wrapped { None } else { buffer_row })
+        Some(if soft_wrapped {
+            RowInfo {
+                buffer_row: None,
+                multibuffer_row: None,
+                diff_status,
+            }
+        } else {
+            buffer_row
+        })
     }
 }
 

crates/editor/src/editor.rs πŸ”—

@@ -25,7 +25,6 @@ mod git;
 mod highlight_matching_bracket;
 mod hover_links;
 mod hover_popover;
-mod hunk_diff;
 mod indent_guides;
 mod inlay_hint_cache;
 pub mod items;
@@ -56,7 +55,7 @@ use anyhow::{anyhow, Context as _, Result};
 use blink_manager::BlinkManager;
 use client::{Collaborator, ParticipantIndex};
 use clock::ReplicaId;
-use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use collections::{BTreeMap, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
 use display_map::*;
 pub use display_map::{DisplayPoint, FoldPlaceholder};
@@ -89,8 +88,6 @@ use gpui::{
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
-pub(crate) use hunk_diff::HoveredHunk;
-use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot};
 use indent_guides::ActiveIndentGuidesState;
 use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
 pub use inline_completion::Direction;
@@ -101,7 +98,8 @@ use language::{
     language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
     markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
     CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize,
-    Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
+    Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId,
+    TreeSitterOptions,
 };
 use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
 use linked_editing_ranges::refresh_linked_ranges;
@@ -123,14 +121,13 @@ use lsp::{
 use language::BufferSnapshot;
 use movement::TextLayoutDetails;
 pub use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
-    ToPoint,
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
+    ToOffset, ToPoint,
 };
 use multi_buffer::{
     ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
 };
 use project::{
-    buffer_store::BufferChangeSet,
     lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
     project_settings::{GitGutterSetting, ProjectSettings},
     CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
@@ -165,7 +162,7 @@ use text::{BufferId, OffsetUtf16, Rope};
 use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
 use ui::{
     h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize,
-    PopoverMenuHandle, Tooltip,
+    Tooltip,
 };
 use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::item::{ItemHandle, PreviewTabsSettings};
@@ -271,7 +268,6 @@ impl InlayId {
     }
 }
 
-enum DiffRowHighlight {}
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
 enum InputComposition {}
@@ -650,7 +646,6 @@ pub struct Editor {
     nav_history: Option<ItemNavHistory>,
     context_menu: RefCell<Option<CodeContextMenu>>,
     mouse_context_menu: Option<MouseContextMenu>,
-    hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     signature_help_state: SignatureHelpState,
     auto_signature_help: Option<bool>,
@@ -685,7 +680,6 @@ pub struct Editor {
     show_inline_completions_override: Option<bool>,
     menu_inline_completions_policy: MenuInlineCompletionsPolicy,
     inlay_hint_cache: InlayHintCache,
-    diff_map: DiffMap,
     next_inlay_id: usize,
     _subscriptions: Vec<Subscription>,
     pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
@@ -755,7 +749,6 @@ pub struct EditorSnapshot {
     git_blame_gutter_max_author_length: Option<usize>,
     pub display_snapshot: DisplaySnapshot,
     pub placeholder_text: Option<Arc<str>>,
-    diff_map: DiffMapSnapshot,
     is_focused: bool,
     scroll_anchor: ScrollAnchor,
     ongoing_scroll: OngoingScroll,
@@ -1245,7 +1238,12 @@ impl Editor {
 
         let mut code_action_providers = Vec::new();
         if let Some(project) = project.clone() {
-            get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx);
+            get_unstaged_changes_for_buffers(
+                &project,
+                buffer.read(cx).all_buffers(),
+                buffer.clone(),
+                cx,
+            );
             code_action_providers.push(Rc::new(project) as Rc<_>);
         }
 
@@ -1295,7 +1293,6 @@ impl Editor {
             nav_history: None,
             context_menu: RefCell::new(None),
             mouse_context_menu: None,
-            hunk_controls_menu_handle: PopoverMenuHandle::default(),
             completion_tasks: Default::default(),
             signature_help_state: SignatureHelpState::default(),
             auto_signature_help: None,
@@ -1329,7 +1326,7 @@ impl Editor {
             inline_completion_provider: None,
             active_inline_completion: None,
             inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
-            diff_map: DiffMap::default(),
+
             gutter_hovered: false,
             pixel_position_of_newest_cursor: None,
             last_bounds: None,
@@ -1605,7 +1602,6 @@ impl Editor {
             scroll_anchor: self.scroll_manager.anchor(),
             ongoing_scroll: self.scroll_manager.ongoing_scroll(),
             placeholder_text: self.placeholder_text.clone(),
-            diff_map: self.diff_map.snapshot(),
             is_focused: self.focus_handle.is_focused(cx),
             current_line_highlight: self
                 .current_line_highlight
@@ -3602,9 +3598,9 @@ impl Editor {
         multi_buffer_snapshot
             .range_to_buffer_ranges(multi_buffer_visible_range)
             .into_iter()
-            .filter(|(_, excerpt_visible_range)| !excerpt_visible_range.is_empty())
-            .filter_map(|(excerpt, excerpt_visible_range)| {
-                let buffer_file = project::File::from_dyn(excerpt.buffer().file())?;
+            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+            .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
+                let buffer_file = project::File::from_dyn(buffer.file())?;
                 let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
                 let worktree_entry = buffer_worktree
                     .read(cx)
@@ -3613,17 +3609,17 @@ impl Editor {
                     return None;
                 }
 
-                let language = excerpt.buffer().language()?;
+                let language = buffer.language()?;
                 if let Some(restrict_to_languages) = restrict_to_languages {
                     if !restrict_to_languages.contains(language) {
                         return None;
                     }
                 }
                 Some((
-                    excerpt.id(),
+                    excerpt_id,
                     (
-                        multi_buffer.buffer(excerpt.buffer_id()).unwrap(),
-                        excerpt.buffer().version().clone(),
+                        multi_buffer.buffer(buffer.remote_id()).unwrap(),
+                        buffer.version().clone(),
                         excerpt_visible_range,
                     ),
                 ))
@@ -4536,10 +4532,12 @@ impl Editor {
                                 buffer_id,
                                 excerpt_id,
                                 text_anchor: start,
+                                diff_base_anchor: None,
                             }..Anchor {
                                 buffer_id,
                                 excerpt_id,
                                 text_anchor: end,
+                                diff_base_anchor: None,
                             };
                             if highlight.kind == lsp::DocumentHighlightKind::WRITE {
                                 write_ranges.push(range);
@@ -5262,7 +5260,7 @@ impl Editor {
             }))
     }
 
-    #[cfg(any(feature = "test-support", test))]
+    #[cfg(any(test, feature = "test-support"))]
     pub fn context_menu_visible(&self) -> bool {
         self.context_menu
             .borrow()
@@ -6126,10 +6124,9 @@ impl Editor {
     pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext<Self>) {
         let mut revert_changes = HashMap::default();
         let snapshot = self.snapshot(cx);
-        for hunk in hunks_for_ranges(
-            Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(),
-            &snapshot,
-        ) {
+        for hunk in snapshot
+            .hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter())
+        {
             self.prepare_revert_change(&mut revert_changes, &hunk, cx);
         }
         if !revert_changes.is_empty() {
@@ -6147,7 +6144,20 @@ impl Editor {
     }
 
     pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext<Self>) {
-        let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx);
+        let selections = self.selections.all(cx).into_iter().map(|s| s.range());
+        self.revert_hunks_in_ranges(selections, cx);
+    }
+
+    fn revert_hunks_in_ranges(
+        &mut self,
+        ranges: impl Iterator<Item = Range<Point>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let mut revert_changes = HashMap::default();
+        let snapshot = self.snapshot(cx);
+        for hunk in &snapshot.hunks_for_ranges(ranges) {
+            self.prepare_revert_change(&mut revert_changes, &hunk, cx);
+        }
         if !revert_changes.is_empty() {
             self.transact(cx, |editor, cx| {
                 editor.revert(revert_changes, cx);
@@ -6155,18 +6165,6 @@ impl Editor {
         }
     }
 
-    fn revert_hunk(&mut self, hunk: HoveredHunk, cx: &mut ViewContext<Editor>) {
-        let snapshot = self.buffer.read(cx).read(cx);
-        if let Some(hunk) = crate::hunk_diff::to_diff_hunk(&hunk, &snapshot) {
-            drop(snapshot);
-            let mut revert_changes = HashMap::default();
-            self.prepare_revert_change(&mut revert_changes, &hunk, cx);
-            if !revert_changes.is_empty() {
-                self.revert(revert_changes, cx)
-            }
-        }
-    }
-
     pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
         if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
             let project_path = buffer.read(cx).project_path(cx)?;
@@ -6184,33 +6182,20 @@ impl Editor {
         }
     }
 
-    fn gather_revert_changes(
-        &self,
-        selections: &[Selection<Point>],
-        cx: &mut ViewContext<Editor>,
-    ) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
-        let mut revert_changes = HashMap::default();
-        let snapshot = self.snapshot(cx);
-        for hunk in hunks_for_selections(&snapshot, selections) {
-            self.prepare_revert_change(&mut revert_changes, &hunk, cx);
-        }
-        revert_changes
-    }
-
     pub fn prepare_revert_change(
         &self,
         revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
         hunk: &MultiBufferDiffHunk,
-        cx: &AppContext,
+        cx: &mut WindowContext,
     ) -> Option<()> {
-        let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?;
+        let buffer = self.buffer.read(cx);
+        let change_set = buffer.change_set_for(hunk.buffer_id)?;
+        let buffer = buffer.buffer(hunk.buffer_id)?;
         let buffer = buffer.read(cx);
-        let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set;
         let original_text = change_set
             .read(cx)
             .base_text
             .as_ref()?
-            .read(cx)
             .as_rope()
             .slice(hunk.diff_base_byte_range.clone());
         let buffer_snapshot = buffer.snapshot();
@@ -6551,12 +6536,8 @@ impl Editor {
 
                 // Don't move lines across excerpts
                 if buffer
-                    .excerpt_boundaries_in_range((
-                        Bound::Excluded(insertion_point),
-                        Bound::Included(range_to_move.end),
-                    ))
-                    .next()
-                    .is_none()
+                    .excerpt_containing(insertion_point..range_to_move.end)
+                    .is_some()
                 {
                     let text = buffer
                         .text_for_range(range_to_move.clone())
@@ -6649,12 +6630,8 @@ impl Editor {
 
                 // Don't move lines across excerpt boundaries
                 if buffer
-                    .excerpt_boundaries_in_range((
-                        Bound::Excluded(range_to_move.start),
-                        Bound::Included(insertion_point),
-                    ))
-                    .next()
-                    .is_none()
+                    .excerpt_containing(range_to_move.start..insertion_point)
+                    .is_some()
                 {
                     let mut text = String::from("\n");
                     text.extend(buffer.text_for_range(range_to_move.clone()));
@@ -9282,11 +9259,7 @@ impl Editor {
             let snapshot = buffer.snapshot(cx);
             let mut excerpt_ids = selections
                 .iter()
-                .flat_map(|selection| {
-                    snapshot
-                        .excerpts_for_range(selection.range())
-                        .map(|excerpt| excerpt.id())
-                })
+                .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range()))
                 .collect::<Vec<_>>();
             excerpt_ids.sort();
             excerpt_ids.dedup();
@@ -9306,6 +9279,30 @@ impl Editor {
         })
     }
 
+    pub fn go_to_singleton_buffer_point(&mut self, point: Point, cx: &mut ViewContext<Self>) {
+        self.go_to_singleton_buffer_range(point..point, cx);
+    }
+
+    pub fn go_to_singleton_buffer_range(
+        &mut self,
+        range: Range<Point>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let multibuffer = self.buffer().read(cx);
+        let Some(buffer) = multibuffer.as_singleton() else {
+            return;
+        };
+        let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else {
+            return;
+        };
+        let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else {
+            return;
+        };
+        self.change_selections(Some(Autoscroll::center()), cx, |s| {
+            s.select_anchor_ranges([start..end])
+        });
+    }
+
     fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
         self.go_to_diagnostic_impl(Direction::Next, cx)
     }
@@ -9321,7 +9318,14 @@ impl Editor {
         // If there is an active Diagnostic Popover jump to its diagnostic instead.
         if direction == Direction::Next {
             if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
-                self.activate_diagnostics(popover.group_id(), cx);
+                let Some(buffer_id) = popover.local_diagnostic.range.start.buffer_id else {
+                    return;
+                };
+                self.activate_diagnostics(
+                    buffer_id,
+                    popover.local_diagnostic.diagnostic.group_id,
+                    cx,
+                );
                 if let Some(active_diagnostics) = self.active_diagnostics.as_ref() {
                     let primary_range_start = active_diagnostics.primary_range.start;
                     self.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -9352,25 +9356,27 @@ impl Editor {
         };
         let snapshot = self.snapshot(cx);
         loop {
-            let diagnostics = if direction == Direction::Prev {
-                buffer.diagnostics_in_range(0..search_start, true)
+            let mut diagnostics;
+            if direction == Direction::Prev {
+                diagnostics = buffer
+                    .diagnostics_in_range::<_, usize>(0..search_start)
+                    .collect::<Vec<_>>();
+                diagnostics.reverse();
             } else {
-                buffer.diagnostics_in_range(search_start..buffer.len(), false)
-            }
-            .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start));
-            let search_start_anchor = buffer.anchor_after(search_start);
+                diagnostics = buffer
+                    .diagnostics_in_range::<_, usize>(search_start..buffer.len())
+                    .collect::<Vec<_>>();
+            };
             let group = diagnostics
+                .into_iter()
+                .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start))
                 // relies on diagnostics_in_range to return diagnostics with the same starting range to
                 // be sorted in a stable way
                 // skip until we are at current active diagnostic, if it exists
                 .skip_while(|entry| {
                     let is_in_range = match direction {
-                        Direction::Prev => {
-                            entry.range.start.cmp(&search_start_anchor, &buffer).is_ge()
-                        }
-                        Direction::Next => {
-                            entry.range.start.cmp(&search_start_anchor, &buffer).is_le()
-                        }
+                        Direction::Prev => entry.range.end > search_start,
+                        Direction::Next => entry.range.start < search_start,
                     };
                     is_in_range
                         && self
@@ -9381,7 +9387,7 @@ impl Editor {
                 .find_map(|entry| {
                     if entry.diagnostic.is_primary
                         && entry.diagnostic.severity <= DiagnosticSeverity::WARNING
-                        && !(entry.range.start == entry.range.end)
+                        && entry.range.start != entry.range.end
                         // if we match with the active diagnostic, skip it
                         && Some(entry.diagnostic.group_id)
                             != self.active_diagnostics.as_ref().map(|d| d.group_id)
@@ -9393,8 +9399,10 @@ impl Editor {
                 });
 
             if let Some((primary_range, group_id)) = group {
-                self.activate_diagnostics(group_id, cx);
-                let primary_range = primary_range.to_offset(&buffer);
+                let Some(buffer_id) = buffer.anchor_after(primary_range.start).buffer_id else {
+                    return;
+                };
+                self.activate_diagnostics(buffer_id, group_id, cx);
                 if self.active_diagnostics.is_some() {
                     self.change_selections(Some(Autoscroll::fit()), cx, |s| {
                         s.select(vec![Selection {
@@ -9439,21 +9447,25 @@ impl Editor {
         position: Point,
         cx: &mut ViewContext<Editor>,
     ) -> Option<MultiBufferDiffHunk> {
-        for (ix, position) in [position, Point::zero()].into_iter().enumerate() {
-            if let Some(hunk) = self.go_to_next_hunk_in_direction(
-                snapshot,
-                position,
-                ix > 0,
-                snapshot.diff_map.diff_hunks_in_range(
-                    position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(),
-                    &snapshot.buffer_snapshot,
-                ),
-                cx,
-            ) {
-                return Some(hunk);
-            }
+        let mut hunk = snapshot
+            .buffer_snapshot
+            .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point())
+            .find(|hunk| hunk.row_range.start.0 > position.row);
+        if hunk.is_none() {
+            hunk = snapshot
+                .buffer_snapshot
+                .diff_hunks_in_range(Point::zero()..position)
+                .find(|hunk| hunk.row_range.end.0 < position.row)
         }
-        None
+        if let Some(hunk) = &hunk {
+            let destination = Point::new(hunk.row_range.start.0, 0);
+            self.unfold_ranges(&[destination..destination], false, false, cx);
+            self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges(vec![destination..destination]);
+            });
+        }
+
+        hunk
     }
 
     fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
@@ -9468,52 +9480,19 @@ impl Editor {
         position: Point,
         cx: &mut ViewContext<Editor>,
     ) -> Option<MultiBufferDiffHunk> {
-        for (ix, position) in [position, snapshot.buffer_snapshot.max_point()]
-            .into_iter()
-            .enumerate()
-        {
-            if let Some(hunk) = self.go_to_next_hunk_in_direction(
-                snapshot,
-                position,
-                ix > 0,
-                snapshot
-                    .diff_map
-                    .diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot),
-                cx,
-            ) {
-                return Some(hunk);
-            }
+        let mut hunk = snapshot.buffer_snapshot.diff_hunk_before(position);
+        if hunk.is_none() {
+            hunk = snapshot.buffer_snapshot.diff_hunk_before(Point::MAX);
         }
-        None
-    }
-
-    fn go_to_next_hunk_in_direction(
-        &mut self,
-        snapshot: &DisplaySnapshot,
-        initial_point: Point,
-        is_wrapped: bool,
-        hunks: impl Iterator<Item = MultiBufferDiffHunk>,
-        cx: &mut ViewContext<Editor>,
-    ) -> Option<MultiBufferDiffHunk> {
-        let display_point = initial_point.to_display_point(snapshot);
-        let mut hunks = hunks
-            .map(|hunk| (diff_hunk_to_display(&hunk, snapshot), hunk))
-            .filter(|(display_hunk, _)| {
-                is_wrapped || !display_hunk.contains_display_row(display_point.row())
-            })
-            .dedup();
-
-        if let Some((display_hunk, hunk)) = hunks.next() {
+        if let Some(hunk) = &hunk {
+            let destination = Point::new(hunk.row_range.start.0, 0);
+            self.unfold_ranges(&[destination..destination], false, false, cx);
             self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                let row = display_hunk.start_display_row();
-                let point = DisplayPoint::new(row, 0);
-                s.select_display_ranges([point..point]);
+                s.select_ranges(vec![destination..destination]);
             });
-
-            Some(hunk)
-        } else {
-            None
         }
+
+        hunk
     }
 
     pub fn go_to_definition(
@@ -9762,15 +9741,12 @@ impl Editor {
                     };
                     let pane = workspace.read(cx).active_pane().clone();
 
-                    let range = target.range.to_offset(target.buffer.read(cx));
+                    let range = target.range.to_point(target.buffer.read(cx));
                     let range = editor.range_for_match(&range);
+                    let range = collapse_multiline_range(range);
 
                     if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
-                        let buffer = target.buffer.read(cx);
-                        let range = check_multiline_range(buffer, range);
-                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                            s.select_ranges([range]);
-                        });
+                        editor.go_to_singleton_buffer_range(range.clone(), cx);
                     } else {
                         cx.window_context().defer(move |cx| {
                             let target_editor: View<Self> =
@@ -9793,15 +9769,7 @@ impl Editor {
                                 // When selecting a definition in a different buffer, disable the nav history
                                 // to avoid creating a history entry at the previous cursor location.
                                 pane.update(cx, |pane, _| pane.disable_history());
-                                let buffer = target.buffer.read(cx);
-                                let range = check_multiline_range(buffer, range);
-                                target_editor.change_selections(
-                                    Some(Autoscroll::focused()),
-                                    cx,
-                                    |s| {
-                                        s.select_ranges([range]);
-                                    },
-                                );
+                                target_editor.go_to_singleton_buffer_range(range, cx);
                                 pane.update(cx, |pane, _| pane.enable_history());
                             });
                         });
@@ -10420,11 +10388,12 @@ impl Editor {
                 let mut buffer_id_to_ranges: BTreeMap<BufferId, Vec<Range<text::Anchor>>> =
                     BTreeMap::new();
                 for selection_range in selection_ranges {
-                    for (excerpt, buffer_range) in snapshot.range_to_buffer_ranges(selection_range)
+                    for (buffer, buffer_range, _) in
+                        snapshot.range_to_buffer_ranges(selection_range)
                     {
-                        let buffer_id = excerpt.buffer_id();
-                        let start = excerpt.buffer().anchor_before(buffer_range.start);
-                        let end = excerpt.buffer().anchor_after(buffer_range.end);
+                        let buffer_id = buffer.remote_id();
+                        let start = buffer.anchor_before(buffer_range.start);
+                        let end = buffer.anchor_after(buffer_range.end);
                         buffers.insert(multi_buffer.buffer(buffer_id).unwrap());
                         buffer_id_to_ranges
                             .entry(buffer_id)
@@ -10499,12 +10468,11 @@ impl Editor {
             let buffer = self.buffer.read(cx).snapshot(cx);
             let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
             let is_valid = buffer
-                .diagnostics_in_range(active_diagnostics.primary_range.clone(), false)
+                .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone())
                 .any(|entry| {
-                    let range = entry.range.to_offset(&buffer);
                     entry.diagnostic.is_primary
-                        && !range.is_empty()
-                        && range.start == primary_range_start
+                        && !entry.range.is_empty()
+                        && entry.range.start == primary_range_start
                         && entry.diagnostic.message == active_diagnostics.primary_message
                 });
 
@@ -10524,7 +10492,12 @@ impl Editor {
         }
     }
 
-    fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) {
+    fn activate_diagnostics(
+        &mut self,
+        buffer_id: BufferId,
+        group_id: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.dismiss_diagnostics(cx);
         let snapshot = self.snapshot(cx);
         self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
@@ -10532,21 +10505,17 @@ impl Editor {
 
             let mut primary_range = None;
             let mut primary_message = None;
-            let mut group_end = Point::zero();
             let diagnostic_group = buffer
-                .diagnostic_group(group_id)
+                .diagnostic_group(buffer_id, group_id)
                 .filter_map(|entry| {
-                    let start = entry.range.start.to_point(&buffer);
-                    let end = entry.range.end.to_point(&buffer);
+                    let start = entry.range.start;
+                    let end = entry.range.end;
                     if snapshot.is_line_folded(MultiBufferRow(start.row))
                         && (start.row == end.row
                             || snapshot.is_line_folded(MultiBufferRow(end.row)))
                     {
                         return None;
                     }
-                    if end > group_end {
-                        group_end = end;
-                    }
                     if entry.diagnostic.is_primary {
                         primary_range = Some(entry.range.clone());
                         primary_message = Some(entry.diagnostic.message.clone());
@@ -10579,7 +10548,8 @@ impl Editor {
                 .collect();
 
             Some(ActiveDiagnosticGroup {
-                primary_range,
+                primary_range: buffer.anchor_before(primary_range.start)
+                    ..buffer.anchor_after(primary_range.end),
                 primary_message,
                 group_id,
                 blocks,
@@ -10721,17 +10691,16 @@ impl Editor {
             }
         } else {
             let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-            let mut toggled_buffers = HashSet::default();
-            for (_, buffer_snapshot, _) in
-                multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges())
-            {
-                let buffer_id = buffer_snapshot.remote_id();
-                if toggled_buffers.insert(buffer_id) {
-                    if self.buffer_folded(buffer_id, cx) {
-                        self.unfold_buffer(buffer_id, cx);
-                    } else {
-                        self.fold_buffer(buffer_id, cx);
-                    }
+            let buffer_ids: HashSet<_> = multi_buffer_snapshot
+                .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
+                .map(|(snapshot, _, _)| snapshot.remote_id())
+                .collect();
+
+            for buffer_id in buffer_ids {
+                if self.is_buffer_folded(buffer_id, cx) {
+                    self.unfold_buffer(buffer_id, cx);
+                } else {
+                    self.fold_buffer(buffer_id, cx);
                 }
             }
         }
@@ -10804,14 +10773,13 @@ impl Editor {
             self.fold_creases(to_fold, true, cx);
         } else {
             let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-            let mut folded_buffers = HashSet::default();
-            for (_, buffer_snapshot, _) in
-                multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges())
-            {
-                let buffer_id = buffer_snapshot.remote_id();
-                if folded_buffers.insert(buffer_id) {
-                    self.fold_buffer(buffer_id, cx);
-                }
+
+            let buffer_ids: HashSet<_> = multi_buffer_snapshot
+                .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
+                .map(|(snapshot, _, _)| snapshot.remote_id())
+                .collect();
+            for buffer_id in buffer_ids {
+                self.fold_buffer(buffer_id, cx);
             }
         }
     }
@@ -10885,11 +10853,14 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
-        let Some((_, _, buffer)) = snapshot.as_singleton() else {
-            return;
-        };
-        let creases = buffer
-            .function_body_fold_ranges(0..buffer.len())
+
+        let ranges = snapshot
+            .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default())
+            .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range))
+            .collect::<Vec<_>>();
+
+        let creases = ranges
+            .into_iter()
             .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone()))
             .collect();
 
@@ -10967,14 +10938,12 @@ impl Editor {
             self.unfold_ranges(&ranges, true, true, cx);
         } else {
             let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-            let mut unfolded_buffers = HashSet::default();
-            for (_, buffer_snapshot, _) in
-                multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges())
-            {
-                let buffer_id = buffer_snapshot.remote_id();
-                if unfolded_buffers.insert(buffer_id) {
-                    self.unfold_buffer(buffer_id, cx);
-                }
+            let buffer_ids: HashSet<_> = multi_buffer_snapshot
+                .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges())
+                .map(|(snapshot, _, _)| snapshot.remote_id())
+                .collect();
+            for buffer_id in buffer_ids {
+                self.unfold_buffer(buffer_id, cx);
             }
         }
     }
@@ -11096,10 +11065,6 @@ impl Editor {
             self.request_autoscroll(Autoscroll::fit(), cx);
         }
 
-        for buffer_id in buffers_affected {
-            Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx);
-        }
-
         cx.notify();
 
         if let Some(active_diagnostics) = self.active_diagnostics.take() {
@@ -11131,7 +11096,7 @@ impl Editor {
     }
 
     pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
-        if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) {
+        if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) {
             return;
         }
         let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
@@ -11148,7 +11113,7 @@ impl Editor {
     }
 
     pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
-        if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) {
+        if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) {
             return;
         }
         let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
@@ -11165,8 +11130,12 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
-        self.display_map.read(cx).buffer_folded(buffer)
+    pub fn is_buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
+        self.display_map.read(cx).is_buffer_folded(buffer)
+    }
+
+    pub fn folded_buffers<'a>(&self, cx: &'a AppContext) -> &'a HashSet<BufferId> {
+        self.display_map.read(cx).folded_buffers()
     }
 
     /// Removes any folds with the given ranges.
@@ -11207,10 +11176,6 @@ impl Editor {
             self.request_autoscroll(Autoscroll::fit(), cx);
         }
 
-        for buffer_id in buffers_affected {
-            Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx);
-        }
-
         cx.notify();
         self.scrollbar_marker_state.dirty = true;
         self.active_indent_guides_state.dirty = true;
@@ -11220,6 +11185,108 @@ impl Editor {
         self.display_map.read(cx).fold_placeholder.clone()
     }
 
+    pub fn set_expand_all_diff_hunks(&mut self, cx: &mut AppContext) {
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.set_all_diff_hunks_expanded(cx);
+        });
+    }
+
+    pub fn expand_all_diff_hunks(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx)
+        });
+    }
+
+    pub fn toggle_selected_diff_hunks(
+        &mut self,
+        _: &ToggleSelectedDiffHunks,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
+        self.toggle_diff_hunks_in_ranges(ranges, cx);
+    }
+
+    pub fn expand_selected_diff_hunks(&mut self, cx: &mut ViewContext<Self>) {
+        let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
+        self.buffer
+            .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx))
+    }
+
+    pub fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.buffer.update(cx, |buffer, cx| {
+            let ranges = vec![Anchor::min()..Anchor::max()];
+            if !buffer.all_diff_hunks_expanded()
+                && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx)
+            {
+                buffer.collapse_diff_hunks(ranges, cx);
+                true
+            } else {
+                false
+            }
+        })
+    }
+
+    fn toggle_diff_hunks_in_ranges(
+        &mut self,
+        ranges: Vec<Range<Anchor>>,
+        cx: &mut ViewContext<'_, Editor>,
+    ) {
+        self.buffer.update(cx, |buffer, cx| {
+            if buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) {
+                buffer.collapse_diff_hunks(ranges, cx)
+            } else {
+                buffer.expand_diff_hunks(ranges, cx)
+            }
+        })
+    }
+
+    pub(crate) fn apply_all_diff_hunks(
+        &mut self,
+        _: &ApplyAllDiffHunks,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let buffers = self.buffer.read(cx).all_buffers();
+        for branch_buffer in buffers {
+            branch_buffer.update(cx, |branch_buffer, cx| {
+                branch_buffer.merge_into_base(Vec::new(), cx);
+            });
+        }
+
+        if let Some(project) = self.project.clone() {
+            self.save(true, project, cx).detach_and_log_err(cx);
+        }
+    }
+
+    pub(crate) fn apply_selected_diff_hunks(
+        &mut self,
+        _: &ApplyDiffHunk,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let snapshot = self.snapshot(cx);
+        let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx).into_iter());
+        let mut ranges_by_buffer = HashMap::default();
+        self.transact(cx, |editor, cx| {
+            for hunk in hunks {
+                if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
+                    ranges_by_buffer
+                        .entry(buffer.clone())
+                        .or_insert_with(Vec::new)
+                        .push(hunk.buffer_range.to_offset(buffer.read(cx)));
+                }
+            }
+
+            for (buffer, ranges) in ranges_by_buffer {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.merge_into_base(ranges, cx);
+                });
+            }
+        });
+
+        if let Some(project) = self.project.clone() {
+            self.save(true, project, cx).detach_and_log_err(cx);
+        }
+    }
+
     pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext<Self>) {
         if hovered != self.gutter_hovered {
             self.gutter_hovered = hovered;
@@ -11751,29 +11818,22 @@ impl Editor {
             let selection = self.selections.newest::<Point>(cx);
             let selection_range = selection.range();
 
-            let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() {
-                (buffer, selection_range.start.row..selection_range.end.row)
-            } else {
-                let multi_buffer = self.buffer().read(cx);
-                let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-                let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range);
-
-                let (excerpt, range) = if selection.reversed {
-                    buffer_ranges.first()
-                } else {
-                    buffer_ranges.last()
-                }?;
+            let multi_buffer = self.buffer().read(cx);
+            let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+            let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range);
 
-                let snapshot = excerpt.buffer();
-                let selection = text::ToPoint::to_point(&range.start, &snapshot).row
-                    ..text::ToPoint::to_point(&range.end, &snapshot).row;
-                (
-                    multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(),
-                    selection,
-                )
-            };
+            let (buffer, range, _) = if selection.reversed {
+                buffer_ranges.first()
+            } else {
+                buffer_ranges.last()
+            }?;
 
-            Some((buffer, selection))
+            let selection = text::ToPoint::to_point(&range.start, &buffer).row
+                ..text::ToPoint::to_point(&range.end, &buffer).row;
+            Some((
+                multi_buffer.buffer(buffer.remote_id()).unwrap().clone(),
+                selection,
+            ))
         });
 
         let Some((buffer, selection)) = buffer_and_selection else {
@@ -12543,9 +12603,14 @@ impl Editor {
             } => {
                 self.tasks_update_task = Some(self.refresh_runnables(cx));
                 let buffer_id = buffer.read(cx).remote_id();
-                if !self.diff_map.diff_bases.contains_key(&buffer_id) {
+                if self.buffer.read(cx).change_set_for(buffer_id).is_none() {
                     if let Some(project) = &self.project {
-                        get_unstaged_changes_for_buffers(project, [buffer.clone()], cx);
+                        get_unstaged_changes_for_buffers(
+                            project,
+                            [buffer.clone()],
+                            self.buffer.clone(),
+                            cx,
+                        );
                     }
                 }
                 cx.emit(EditorEvent::ExcerptsAdded {
@@ -12664,14 +12729,14 @@ impl Editor {
         let multi_buffer_snapshot = multi_buffer.snapshot(cx);
         let mut new_selections_by_buffer = HashMap::default();
         for selection in selections {
-            for (excerpt, range) in
+            for (buffer, range, _) in
                 multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end)
             {
-                let mut range = range.to_point(excerpt.buffer());
+                let mut range = range.to_point(buffer);
                 range.start.column = 0;
-                range.end.column = excerpt.buffer().line_len(range.end.row);
+                range.end.column = buffer.line_len(range.end.row);
                 new_selections_by_buffer
-                    .entry(multi_buffer.buffer(excerpt.buffer_id()).unwrap())
+                    .entry(multi_buffer.buffer(buffer.remote_id()).unwrap())
                     .or_insert(Vec::new())
                     .push(range)
             }
@@ -12772,13 +12837,13 @@ impl Editor {
                 let selections = self.selections.all::<usize>(cx);
                 let multi_buffer = self.buffer.read(cx);
                 for selection in selections {
-                    for (excerpt, mut range) in multi_buffer
+                    for (buffer, mut range, _) in multi_buffer
                         .snapshot(cx)
                         .range_to_buffer_ranges(selection.range())
                     {
                         // When editing branch buffers, jump to the corresponding location
                         // in their base buffer.
-                        let mut buffer_handle = multi_buffer.buffer(excerpt.buffer_id()).unwrap();
+                        let mut buffer_handle = multi_buffer.buffer(buffer.remote_id()).unwrap();
                         let buffer = buffer_handle.read(cx);
                         if let Some(base_buffer) = buffer.base_buffer() {
                             range = buffer.range_to_version(range, &base_buffer.read(cx).version());

crates/editor/src/editor_tests.rs πŸ”—

@@ -19,11 +19,11 @@ use language::{
     },
     BracketPairConfig,
     Capability::ReadWrite,
-    FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
-    LanguageName, Override, ParsedMarkdown, Point,
+    FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
+    Override, ParsedMarkdown, Point,
 };
 use language_settings::{Formatter, FormatterList, IndentGuideSettings};
-use multi_buffer::MultiBufferIndentGuide;
+use multi_buffer::IndentGuide;
 use parking_lot::Mutex;
 use pretty_assertions::{assert_eq, assert_ne};
 use project::{buffer_store::BufferChangeSet, FakeFs};
@@ -3363,8 +3363,8 @@ async fn test_custom_newlines_cause_no_false_positive_diffs(
         let snapshot = editor.snapshot(cx);
         assert_eq!(
             snapshot
-                .diff_map
-                .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot)
+                .buffer_snapshot
+                .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
                 .collect::<Vec<_>>(),
             Vec::new(),
             "Should not have any diffs for files with custom newlines"
@@ -5480,6 +5480,109 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let base_text = r#"
+        impl A {
+            // this is an unstaged comment
+
+            fn b() {
+                c();
+            }
+
+            // this is another unstaged comment
+
+            fn d() {
+                // e
+                // f
+            }
+        }
+
+        fn g() {
+            // h
+        }
+    "#
+    .unindent();
+
+    let text = r#"
+        Λ‡impl A {
+
+            fn b() {
+                c();
+            }
+
+            fn d() {
+                // e
+                // f
+            }
+        }
+
+        fn g() {
+            // h
+        }
+    "#
+    .unindent();
+
+    let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
+    cx.set_state(&text);
+    cx.set_diff_base(&base_text);
+    cx.update_editor(|editor, cx| {
+        editor.expand_all_diff_hunks(&Default::default(), cx);
+    });
+
+    cx.assert_state_with_diff(
+        "
+        Λ‡impl A {
+      -     // this is an unstaged comment
+
+            fn b() {
+                c();
+            }
+
+      -     // this is another unstaged comment
+      -
+            fn d() {
+                // e
+                // f
+            }
+        }
+
+        fn g() {
+            // h
+        }
+    "
+        .unindent(),
+    );
+
+    let expected_display_text = "
+        impl A {
+            // this is an unstaged comment
+
+            fn b() {
+                β‹―
+            }
+
+            // this is another unstaged comment
+
+            fn d() {
+                β‹―
+            }
+        }
+
+        fn g() {
+            β‹―
+        }
+        "
+    .unindent();
+
+    cx.update_editor(|editor, cx| {
+        editor.fold_function_bodies(&FoldFunctionBodies, cx);
+        assert_eq!(editor.display_text(cx), expected_display_text);
+    });
+}
+
 #[gpui::test]
 async fn test_autoindent(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -10319,7 +10422,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
 }
 
 #[gpui::test]
-async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
+async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
 
     let mut cx = EditorTestContext::new(cx).await;
@@ -10420,7 +10523,26 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext)
     );
 
     cx.update_editor(|editor, cx| {
-        for _ in 0..3 {
+        editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        Λ‡use some::modified;
+
+
+        fn main() {
+            println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        for _ in 0..2 {
             editor.go_to_prev_hunk(&GoToPrevHunk, cx);
         }
     });
@@ -10442,11 +10564,10 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext)
 
     cx.update_editor(|editor, cx| {
         editor.fold(&Fold, cx);
+    });
 
-        //Make sure that the fold only gets one hunk
-        for _ in 0..4 {
-            editor.go_to_next_hunk(&GoToHunk, cx);
-        }
+    cx.update_editor(|editor, cx| {
+        editor.go_to_next_hunk(&GoToHunk, cx);
     });
 
     cx.assert_editor_state(
@@ -11815,6 +11936,39 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_deleting_over_diff_hunk(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
+    let base_text = indoc! {r#"
+        one
+
+        two
+        three
+        "#};
+
+    cx.set_diff_base(base_text);
+    cx.set_state("\nˇ\n");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, cx| {
+        editor.expand_selected_diff_hunks(cx);
+    });
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, cx| {
+        editor.backspace(&Default::default(), cx);
+    });
+    cx.run_until_parked();
+    cx.assert_state_with_diff(
+        indoc! {r#"
+
+        - two
+        - threeˇ
+        +
+        "#}
+        .to_string(),
+    );
+}
+
 #[gpui::test]
 async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -12019,13 +12173,11 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
             (buffer_3.clone(), base_text_3),
         ] {
             let change_set = cx.new_model(|cx| {
-                BufferChangeSet::new_with_base_text(
-                    diff_base.to_string(),
-                    buffer.read(cx).text_snapshot(),
-                    cx,
-                )
+                BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx)
             });
-            editor.diff_map.add_change_set(change_set, cx)
+            editor
+                .buffer
+                .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
         }
     });
     cx.executor().run_until_parked();
@@ -12385,7 +12537,10 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
+async fn test_toggle_selected_diff_hunks(
+    executor: BackgroundExecutor,
+    cx: &mut gpui::TestAppContext,
+) {
     init_test(cx, |_| {});
 
     let mut cx = EditorTestContext::new(cx).await;
@@ -12423,7 +12578,7 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
 
     cx.update_editor(|editor, cx| {
         editor.go_to_next_hunk(&GoToHunk, cx);
-        editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
+        editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx);
     });
     executor.run_until_parked();
     cx.assert_state_with_diff(
@@ -12443,12 +12598,34 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
     );
 
     cx.update_editor(|editor, cx| {
-        for _ in 0..3 {
+        for _ in 0..2 {
             editor.go_to_next_hunk(&GoToHunk, cx);
-            editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
+            editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx);
         }
     });
     executor.run_until_parked();
+    cx.assert_state_with_diff(
+        r#"
+        - use some::mod;
+        + Λ‡use some::modified;
+
+
+          fn main() {
+        -     println!("hello");
+        +     println!("hello there");
+
+        +     println!("around the");
+              println!("world");
+          }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_next_hunk(&GoToHunk, cx);
+        editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx);
+    });
+    executor.run_until_parked();
     cx.assert_state_with_diff(
         r#"
         - use some::mod;
@@ -12534,7 +12711,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
     executor.run_until_parked();
 
     cx.update_editor(|editor, cx| {
-        editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
+        editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
     });
     executor.run_until_parked();
     cx.assert_state_with_diff(
@@ -12579,7 +12756,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
     );
 
     cx.update_editor(|editor, cx| {
-        editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
+        editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
     });
     executor.run_until_parked();
     cx.assert_state_with_diff(
@@ -12602,170 +12779,6 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
     );
 }
 
-#[gpui::test]
-async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
-    init_test(cx, |_| {});
-
-    let mut cx = EditorTestContext::new(cx).await;
-
-    let diff_base = r#"
-        use some::mod1;
-        use some::mod2;
-
-        const A: u32 = 42;
-        const B: u32 = 42;
-        const C: u32 = 42;
-
-        fn main() {
-            println!("hello");
-
-            println!("world");
-        }
-
-        fn another() {
-            println!("another");
-        }
-
-        fn another2() {
-            println!("another2");
-        }
-        "#
-    .unindent();
-
-    cx.set_state(
-        &r#"
-        Β«use some::mod2;
-
-        const A: u32 = 42;
-        const C: u32 = 42;
-
-        fn main() {
-            //println!("hello");
-
-            println!("world");
-            //
-            //Λ‡Β»
-        }
-
-        fn another() {
-            println!("another");
-            println!("another");
-        }
-
-            println!("another2");
-        }
-        "#
-        .unindent(),
-    );
-
-    cx.set_diff_base(&diff_base);
-    executor.run_until_parked();
-
-    cx.update_editor(|editor, cx| {
-        editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
-    });
-    executor.run_until_parked();
-
-    cx.assert_state_with_diff(
-        r#"
-        - use some::mod1;
-          Β«use some::mod2;
-
-          const A: u32 = 42;
-        - const B: u32 = 42;
-          const C: u32 = 42;
-
-          fn main() {
-        -     println!("hello");
-        +     //println!("hello");
-
-              println!("world");
-        +     //
-        +     //Λ‡Β»
-          }
-
-          fn another() {
-              println!("another");
-        +     println!("another");
-          }
-
-        - fn another2() {
-              println!("another2");
-          }
-        "#
-        .unindent(),
-    );
-
-    // Fold across some of the diff hunks. They should no longer appear expanded.
-    cx.update_editor(|editor, cx| editor.fold_selected_ranges(&FoldSelectedRanges, cx));
-    cx.executor().run_until_parked();
-
-    // Hunks are not shown if their position is within a fold
-    cx.assert_state_with_diff(
-        r#"
-          Β«use some::mod2;
-
-          const A: u32 = 42;
-          const C: u32 = 42;
-
-          fn main() {
-              //println!("hello");
-
-              println!("world");
-              //
-              //Λ‡Β»
-          }
-
-          fn another() {
-              println!("another");
-        +     println!("another");
-          }
-
-        - fn another2() {
-              println!("another2");
-          }
-        "#
-        .unindent(),
-    );
-
-    cx.update_editor(|editor, cx| {
-        editor.select_all(&SelectAll, cx);
-        editor.unfold_lines(&UnfoldLines, cx);
-    });
-    cx.executor().run_until_parked();
-
-    // The deletions reappear when unfolding.
-    cx.assert_state_with_diff(
-        r#"
-        - use some::mod1;
-          Β«use some::mod2;
-
-          const A: u32 = 42;
-        - const B: u32 = 42;
-          const C: u32 = 42;
-
-          fn main() {
-        -     println!("hello");
-        +     //println!("hello");
-
-              println!("world");
-        +     //
-        +     //
-          }
-
-          fn another() {
-              println!("another");
-        +     println!("another");
-          }
-
-        - fn another2() {
-              println!("another2");
-          }
-          Λ‡Β»"#
-        .unindent(),
-    );
-}
-
 #[gpui::test]
 async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -12849,13 +12862,11 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
                 (buffer_3.clone(), file_3_old),
             ] {
                 let change_set = cx.new_model(|cx| {
-                    BufferChangeSet::new_with_base_text(
-                        diff_base.to_string(),
-                        buffer.read(cx).text_snapshot(),
-                        cx,
-                    )
+                    BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx)
                 });
-                editor.diff_map.add_change_set(change_set, cx)
+                editor
+                    .buffer
+                    .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
             }
         })
         .unwrap();
@@ -12895,7 +12906,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
 
     cx.update_editor(|editor, cx| {
         editor.select_all(&SelectAll, cx);
-        editor.toggle_hunk_diff(&ToggleHunkDiff, cx);
+        editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, cx);
     });
     cx.executor().run_until_parked();
 
@@ -12962,17 +12973,18 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
     let editor = cx.add_window(|cx| Editor::new(EditorMode::Full, multi_buffer, None, true, cx));
     editor
         .update(cx, |editor, cx| {
-            let buffer = buffer.read(cx).text_snapshot();
             let change_set = cx
-                .new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), buffer, cx));
-            editor.diff_map.add_change_set(change_set, cx)
+                .new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), &buffer, cx));
+            editor
+                .buffer
+                .update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
         })
         .unwrap();
 
     let mut cx = EditorTestContext::for_editor(editor, cx).await;
     cx.run_until_parked();
 
-    cx.update_editor(|editor, cx| editor.expand_all_hunk_diffs(&Default::default(), cx));
+    cx.update_editor(|editor, cx| editor.expand_all_diff_hunks(&Default::default(), cx));
     cx.executor().run_until_parked();
 
     cx.assert_state_with_diff(
@@ -12981,8 +12993,6 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
           - bbb
           + BBB
 
-          - ddd
-          - eee
           + EEE
             fff
         "
@@ -13036,7 +13046,7 @@ async fn test_edits_around_expanded_insertion_hunks(
     executor.run_until_parked();
 
     cx.update_editor(|editor, cx| {
-        editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
+        editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
     });
     executor.run_until_parked();
 
@@ -13055,7 +13065,7 @@ async fn test_edits_around_expanded_insertion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13078,7 +13088,7 @@ async fn test_edits_around_expanded_insertion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13102,7 +13112,7 @@ async fn test_edits_around_expanded_insertion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13127,7 +13137,7 @@ async fn test_edits_around_expanded_insertion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13153,7 +13163,7 @@ async fn test_edits_around_expanded_insertion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13164,21 +13174,63 @@ async fn test_edits_around_expanded_insertion_hunks(
     executor.run_until_parked();
     cx.assert_state_with_diff(
         r#"
-        use some::mod1;
-      - use some::mod2;
-      -
-      - const A: u32 = 42;
         Λ‡
         fn main() {
             println!("hello");
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 }
 
+#[gpui::test]
+async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_diff_base(indoc! { "
+        one
+        two
+        three
+        four
+        five
+        "
+    });
+    cx.set_state(indoc! { "
+        one
+        Λ‡three
+        five
+    "});
+    cx.run_until_parked();
+    cx.update_editor(|editor, cx| {
+        editor.toggle_selected_diff_hunks(&Default::default(), cx);
+    });
+    cx.assert_state_with_diff(
+        indoc! { "
+        one
+      - two
+        Λ‡three
+      - four
+        five
+    "}
+        .to_string(),
+    );
+    cx.update_editor(|editor, cx| {
+        editor.toggle_selected_diff_hunks(&Default::default(), cx);
+    });
+
+    cx.assert_state_with_diff(
+        indoc! { "
+        one
+        Λ‡three
+        five
+    "}
+        .to_string(),
+    );
+}
+
 #[gpui::test]
 async fn test_edits_around_expanded_deletion_hunks(
     executor: BackgroundExecutor,
@@ -13227,7 +13279,7 @@ async fn test_edits_around_expanded_deletion_hunks(
     executor.run_until_parked();
 
     cx.update_editor(|editor, cx| {
-        editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
+        editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
     });
     executor.run_until_parked();
 
@@ -13246,7 +13298,7 @@ async fn test_edits_around_expanded_deletion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13269,7 +13321,7 @@ async fn test_edits_around_expanded_deletion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13292,7 +13344,7 @@ async fn test_edits_around_expanded_deletion_hunks(
 
             println!("world");
         }
-        "#
+      "#
         .unindent(),
     );
 
@@ -13316,6 +13368,71 @@ async fn test_edits_around_expanded_deletion_hunks(
 
             println!("world");
         }
+      "#
+        .unindent(),
+    );
+}
+
+#[gpui::test]
+async fn test_backspace_after_deletion_hunk(
+    executor: BackgroundExecutor,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let base_text = r#"
+        one
+        two
+        three
+        four
+        five
+    "#
+    .unindent();
+    executor.run_until_parked();
+    cx.set_state(
+        &r#"
+        one
+        two
+        fˇour
+        five
+        "#
+        .unindent(),
+    );
+
+    cx.set_diff_base(&base_text);
+    executor.run_until_parked();
+
+    cx.update_editor(|editor, cx| {
+        editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
+    });
+    executor.run_until_parked();
+
+    cx.assert_state_with_diff(
+        r#"
+          one
+          two
+        - three
+          fˇour
+          five
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        editor.backspace(&Backspace, cx);
+        editor.backspace(&Backspace, cx);
+    });
+    executor.run_until_parked();
+    cx.assert_state_with_diff(
+        r#"
+          one
+          two
+        - threeˇ
+        - four
+        + our
+          five
         "#
         .unindent(),
     );
@@ -13369,7 +13486,7 @@ async fn test_edit_after_expanded_modification_hunk(
     cx.set_diff_base(&diff_base);
     executor.run_until_parked();
     cx.update_editor(|editor, cx| {
-        editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx);
+        editor.expand_all_diff_hunks(&ExpandAllHunkDiffs, cx);
     });
     executor.run_until_parked();
 
@@ -13478,22 +13595,14 @@ fn assert_indent_guides(
         );
     }
 
-    let expected: Vec<_> = expected
-        .into_iter()
-        .map(|guide| MultiBufferIndentGuide {
-            multibuffer_row_range: MultiBufferRow(guide.start_row)..MultiBufferRow(guide.end_row),
-            buffer: guide,
-        })
-        .collect();
-
     assert_eq!(indent_guides, expected, "Indent guides do not match");
 }
 
 fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
     IndentGuide {
         buffer_id,
-        start_row,
-        end_row,
+        start_row: MultiBufferRow(start_row),
+        end_row: MultiBufferRow(end_row),
         depth,
         tab_size: 4,
         settings: IndentGuideSettings {
@@ -13945,6 +14054,105 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont
     );
 }
 
+#[gpui::test]
+async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    let text = indoc! {
+        "
+        impl A {
+            fn b() {
+                0;
+                3;
+                5;
+                6;
+                7;
+            }
+        }
+        "
+    };
+    let base_text = indoc! {
+        "
+        impl A {
+            fn b() {
+                0;
+                1;
+                2;
+                3;
+                4;
+            }
+            fn c() {
+                5;
+                6;
+                7;
+            }
+        }
+        "
+    };
+
+    cx.update_editor(|editor, cx| {
+        editor.set_text(text, cx);
+
+        editor.buffer().update(cx, |multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap();
+            let change_set = cx.new_model(|cx| {
+                let mut change_set = BufferChangeSet::new(&buffer, cx);
+                change_set.recalculate_diff_sync(
+                    base_text.into(),
+                    buffer.read(cx).text_snapshot(),
+                    true,
+                    cx,
+                );
+                change_set
+            });
+
+            multibuffer.set_all_diff_hunks_expanded(cx);
+            multibuffer.add_change_set(change_set, cx);
+
+            buffer.read(cx).remote_id()
+        })
+    });
+
+    cx.assert_state_with_diff(
+        indoc! { "
+          impl A {
+              fn b() {
+                  0;
+        -         1;
+        -         2;
+                  3;
+        -         4;
+        -     }
+        -     fn c() {
+                  5;
+                  6;
+                  7;
+              }
+          }
+          Λ‡"
+        }
+        .to_string(),
+    );
+
+    let mut actual_guides = cx.update_editor(|editor, cx| {
+        editor
+            .snapshot(cx)
+            .buffer_snapshot
+            .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
+            .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
+            .collect::<Vec<_>>()
+    });
+    actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
+    assert_eq!(
+        actual_guides,
+        vec![
+            (MultiBufferRow(1)..=MultiBufferRow(12), 0),
+            (MultiBufferRow(2)..=MultiBufferRow(6), 1),
+            (MultiBufferRow(9)..=MultiBufferRow(11), 1),
+        ]
+    );
+}
+
 #[gpui::test]
 fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -15229,7 +15437,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
 #[track_caller]
 fn assert_hunk_revert(
     not_reverted_text_with_selections: &str,
-    expected_not_reverted_hunk_statuses: Vec<DiffHunkStatus>,
+    expected_hunk_statuses_before: Vec<DiffHunkStatus>,
     expected_reverted_text_with_selections: &str,
     base_text: &str,
     cx: &mut EditorLspTestContext,
@@ -15238,12 +15446,12 @@ fn assert_hunk_revert(
     cx.set_diff_base(base_text);
     cx.executor().run_until_parked();
 
-    let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
+    let actual_hunk_statuses_before = cx.update_editor(|editor, cx| {
         let snapshot = editor.snapshot(cx);
         let reverted_hunk_statuses = snapshot
-            .diff_map
-            .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot)
-            .map(|hunk| hunk_status(&hunk))
+            .buffer_snapshot
+            .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
+            .map(|hunk| hunk.status())
             .collect::<Vec<_>>();
 
         editor.revert_selected_hunks(&RevertSelectedHunks, cx);
@@ -15251,5 +15459,5 @@ fn assert_hunk_revert(
     });
     cx.executor().run_until_parked();
     cx.assert_editor_state(expected_reverted_text_with_selections);
-    assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses);
+    assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
 }

crates/editor/src/element.rs πŸ”—

@@ -12,19 +12,17 @@ use crate::{
     hover_popover::{
         self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
     },
-    hunk_diff::{diff_hunk_to_display, DisplayDiffHunk},
-    hunk_status,
     items::BufferSearchHighlights,
     mouse_context_menu::{self, MenuPosition, MouseContextMenu},
     scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
     BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
     DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
-    EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions,
-    HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData,
-    LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase,
-    Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR,
-    FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
-    MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
+    EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
+    GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
+    InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point,
+    RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
+    StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
+    GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
 };
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap, HashSet};
@@ -46,12 +44,12 @@ use language::{
         IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
         ShowWhitespaceSetting,
     },
-    ChunkRendererContext, DiagnosticEntry,
+    ChunkRendererContext,
 };
 use lsp::DiagnosticSeverity;
 use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint,
-    MultiBufferRow, ToOffset,
+    Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
+    RowInfo, ToOffset,
 };
 use project::project_settings::{GitGutterSetting, ProjectSettings};
 use settings::Settings;
@@ -70,12 +68,27 @@ use sum_tree::Bias;
 use text::BufferId;
 use theme::{ActiveTheme, Appearance, PlayerColor};
 use ui::{
-    prelude::*, ButtonLike, ButtonStyle, ContextMenu, KeyBinding, Tooltip, POPOVER_Y_PADDING,
+    h_flex, prelude::*, ButtonLike, ButtonStyle, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
+    POPOVER_Y_PADDING,
 };
 use unicode_segmentation::UnicodeSegmentation;
 use util::{RangeExt, ResultExt};
 use workspace::{item::Item, notifications::NotifyTaskExt, Workspace};
 
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DisplayDiffHunk {
+    Folded {
+        display_row: DisplayRow,
+    },
+
+    Unfolded {
+        diff_base_byte_range: Range<usize>,
+        display_row_range: Range<DisplayRow>,
+        multi_buffer_range: Range<Anchor>,
+        status: DiffHunkStatus,
+    },
+}
+
 struct SelectionLayout {
     head: DisplayPoint,
     cursor_shape: CursorShape,
@@ -381,8 +394,8 @@ impl EditorElement {
         register_action(view, cx, Editor::copy_file_location);
         register_action(view, cx, Editor::toggle_git_blame);
         register_action(view, cx, Editor::toggle_git_blame_inline);
-        register_action(view, cx, Editor::toggle_hunk_diff);
-        register_action(view, cx, Editor::expand_all_hunk_diffs);
+        register_action(view, cx, Editor::toggle_selected_diff_hunks);
+        register_action(view, cx, Editor::expand_all_diff_hunks);
         register_action(view, cx, |editor, action, cx| {
             if let Some(task) = editor.format(action, cx) {
                 task.detach_and_notify_err(cx);
@@ -509,7 +522,7 @@ impl EditorElement {
     fn mouse_left_down(
         editor: &mut Editor,
         event: &MouseDownEvent,
-        hovered_hunk: Option<HoveredHunk>,
+        hovered_hunk: Option<Range<Anchor>>,
         position_map: &PositionMap,
         text_hitbox: &Hitbox,
         gutter_hitbox: &Hitbox,
@@ -524,7 +537,7 @@ impl EditorElement {
         let mut modifiers = event.modifiers;
 
         if let Some(hovered_hunk) = hovered_hunk {
-            editor.toggle_hovered_hunk(&hovered_hunk, cx);
+            editor.toggle_diff_hunks_in_ranges(vec![hovered_hunk], cx);
             cx.notify();
             return;
         } else if gutter_hitbox.is_hovered(cx) {
@@ -1252,7 +1265,7 @@ impl EditorElement {
                     let editor = self.editor.read(cx);
                     let is_singleton = editor.is_singleton(cx);
                     // Git
-                    (is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
+                    (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
                     ||
                     // Buffer Search Results
                     (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
@@ -1491,108 +1504,77 @@ impl EditorElement {
 
     // Folds contained in a hunk are ignored apart from shrinking visual size
     // If a fold contains any hunks then that fold line is marked as modified
-    fn layout_gutter_git_hunks(
+    fn layout_gutter_diff_hunks(
         &self,
         line_height: Pixels,
         gutter_hitbox: &Hitbox,
         display_rows: Range<DisplayRow>,
-        anchor_range: Range<Anchor>,
         snapshot: &EditorSnapshot,
         cx: &mut WindowContext,
     ) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
-        let buffer_snapshot = &snapshot.buffer_snapshot;
         let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot);
         let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot);
 
-        let git_gutter_setting = ProjectSettings::get_global(cx)
-            .git
-            .git_gutter
-            .unwrap_or_default();
+        let mut display_hunks = Vec::<(DisplayDiffHunk, Option<Hitbox>)>::new();
+        let folded_buffers = self.editor.read(cx).folded_buffers(cx);
 
-        self.editor.update(cx, |editor, cx| {
-            let expanded_hunks = &editor.diff_map.hunks;
-            let expanded_hunks_start_ix = expanded_hunks
-                .binary_search_by(|hunk| {
-                    hunk.hunk_range
-                        .end
-                        .cmp(&anchor_range.start, &buffer_snapshot)
-                        .then(Ordering::Less)
-                })
-                .unwrap_err();
-            let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable();
+        for hunk in snapshot
+            .buffer_snapshot
+            .diff_hunks_in_range(buffer_start..buffer_end)
+        {
+            if folded_buffers.contains(&hunk.buffer_id) {
+                continue;
+            }
 
-            let mut display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)> = editor
-                .diff_map
-                .snapshot
-                .diff_hunks_in_range(buffer_start..buffer_end, &buffer_snapshot)
-                .filter_map(|hunk| {
-                    let display_hunk = diff_hunk_to_display(&hunk, snapshot);
+            let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
+            let hunk_end_point = Point::new(hunk.row_range.end.0, 0);
 
-                    if let DisplayDiffHunk::Unfolded {
-                        multi_buffer_range,
-                        status,
-                        ..
-                    } = &display_hunk
-                    {
-                        let mut is_expanded = false;
-                        while let Some(expanded_hunk) = expanded_hunks.peek() {
-                            match expanded_hunk
-                                .hunk_range
-                                .start
-                                .cmp(&multi_buffer_range.start, &buffer_snapshot)
-                            {
-                                Ordering::Less => {
-                                    expanded_hunks.next();
-                                }
-                                Ordering::Equal => {
-                                    is_expanded = true;
-                                    break;
-                                }
-                                Ordering::Greater => {
-                                    break;
-                                }
-                            }
-                        }
-                        match status {
-                            DiffHunkStatus::Added => {}
-                            DiffHunkStatus::Modified => {}
-                            DiffHunkStatus::Removed => {
-                                if is_expanded {
-                                    return None;
-                                }
-                            }
-                        }
-                    }
+            let hunk_display_start = snapshot.point_to_display_point(hunk_start_point, Bias::Left);
+            let hunk_display_end = snapshot.point_to_display_point(hunk_end_point, Bias::Right);
 
-                    Some(display_hunk)
-                })
-                .dedup()
-                .map(|hunk| (hunk, None))
-                .collect();
+            let display_hunk = if hunk_display_start.column() != 0 || hunk_display_end.column() != 0
+            {
+                DisplayDiffHunk::Folded {
+                    display_row: hunk_display_start.row(),
+                }
+            } else {
+                DisplayDiffHunk::Unfolded {
+                    status: hunk.status(),
+                    diff_base_byte_range: hunk.diff_base_byte_range,
+                    display_row_range: hunk_display_start.row()..hunk_display_end.row(),
+                    multi_buffer_range: Anchor::range_in_buffer(
+                        hunk.excerpt_id,
+                        hunk.buffer_id,
+                        hunk.buffer_range,
+                    ),
+                }
+            };
 
-            if let GitGutterSetting::TrackedFiles = git_gutter_setting {
-                for (hunk, hitbox) in &mut display_hunks {
-                    if let DisplayDiffHunk::Unfolded { .. } = hunk {
-                        let hunk_bounds = Self::diff_hunk_bounds(
-                            snapshot,
-                            line_height,
-                            gutter_hitbox.bounds,
-                            &hunk,
-                        );
-                        *hitbox = Some(cx.insert_hitbox(hunk_bounds, true));
-                    };
+            display_hunks.push((display_hunk, None));
+        }
+
+        let git_gutter_setting = ProjectSettings::get_global(cx)
+            .git
+            .git_gutter
+            .unwrap_or_default();
+        if let GitGutterSetting::TrackedFiles = git_gutter_setting {
+            for (hunk, hitbox) in &mut display_hunks {
+                if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
+                    let hunk_bounds =
+                        Self::diff_hunk_bounds(snapshot, line_height, gutter_hitbox.bounds, hunk);
+                    *hitbox = Some(cx.insert_hitbox(hunk_bounds, true));
                 }
             }
+        }
 
-            display_hunks
-        })
+        display_hunks
     }
 
     #[allow(clippy::too_many_arguments)]
     fn layout_inline_blame(
         &self,
         display_row: DisplayRow,
-        display_snapshot: &DisplaySnapshot,
+        row_info: &RowInfo,
         line_layout: &LineWithInvisibles,
         crease_trailer: Option<&CreaseTrailerLayout>,
         em_width: Pixels,
@@ -1615,9 +1597,6 @@ impl EditorElement {
             .as_ref()
             .map(|(w, _)| w.clone());
 
-        let display_point = DisplayPoint::new(display_row, 0);
-        let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
-
         let editor = self.editor.read(cx);
         let blame = editor.blame.clone()?;
         let padding = {
@@ -1641,7 +1620,7 @@ impl EditorElement {
 
         let blame_entry = blame
             .update(cx, |blame, cx| {
-                blame.blame_for_rows([Some(buffer_row)], cx).next()
+                blame.blame_for_rows(&[*row_info], cx).next()
             })
             .flatten()?;
 
@@ -1680,7 +1659,7 @@ impl EditorElement {
     #[allow(clippy::too_many_arguments)]
     fn layout_blame_entries(
         &self,
-        buffer_rows: impl Iterator<Item = Option<MultiBufferRow>>,
+        buffer_rows: &[RowInfo],
         em_width: Pixels,
         scroll_position: gpui::Point<f32>,
         line_height: Pixels,
@@ -1776,7 +1755,7 @@ impl EditorElement {
                     let start_x = content_origin.x + total_width - scroll_pixel_position.x;
                     if start_x >= text_origin.x {
                         let (offset_y, length) = Self::calculate_indent_guide_bounds(
-                            indent_guide.multibuffer_row_range.clone(),
+                            indent_guide.start_row..indent_guide.end_row,
                             line_height,
                             snapshot,
                         );
@@ -1910,7 +1889,7 @@ impl EditorElement {
                         .buffer_snapshot
                         .buffer_line_for_row(multibuffer_row)
                         .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
-                        .map(|buffer_id| editor.buffer_folded(buffer_id, cx))
+                        .map(|buffer_id| editor.is_buffer_folded(buffer_id, cx))
                         .unwrap_or(false);
                     if buffer_folded {
                         return None;
@@ -2017,7 +1996,7 @@ impl EditorElement {
         let end = rows.end.max(relative_to);
 
         let buffer_rows = snapshot
-            .buffer_rows(start)
+            .row_infos(start)
             .take(1 + end.minus(start) as usize)
             .collect::<Vec<_>>();
 
@@ -2025,7 +2004,7 @@ impl EditorElement {
         let mut delta = 1;
         let mut i = head_idx + 1;
         while i < buffer_rows.len() as u32 {
-            if buffer_rows[i as usize].is_some() {
+            if buffer_rows[i as usize].buffer_row.is_some() {
                 if rows.contains(&DisplayRow(i + start.0)) {
                     relative_rows.insert(DisplayRow(i + start.0), delta);
                 }
@@ -2035,13 +2014,13 @@ impl EditorElement {
         }
         delta = 1;
         i = head_idx.min(buffer_rows.len() as u32 - 1);
-        while i > 0 && buffer_rows[i as usize].is_none() {
+        while i > 0 && buffer_rows[i as usize].buffer_row.is_none() {
             i -= 1;
         }
 
         while i > 0 {
             i -= 1;
-            if buffer_rows[i as usize].is_some() {
+            if buffer_rows[i as usize].buffer_row.is_some() {
                 if rows.contains(&DisplayRow(i + start.0)) {
                     relative_rows.insert(DisplayRow(i + start.0), delta);
                 }
@@ -2060,7 +2039,7 @@ impl EditorElement {
         line_height: Pixels,
         scroll_position: gpui::Point<f32>,
         rows: Range<DisplayRow>,
-        buffer_rows: impl Iterator<Item = Option<MultiBufferRow>>,
+        buffer_rows: &[RowInfo],
         newest_selection_head: Option<DisplayPoint>,
         snapshot: &EditorSnapshot,
         cx: &mut WindowContext,
@@ -2100,15 +2079,17 @@ impl EditorElement {
         let line_numbers = buffer_rows
             .into_iter()
             .enumerate()
-            .flat_map(|(ix, buffer_row)| {
-                let buffer_row = buffer_row?;
-                line_number.clear();
+            .flat_map(|(ix, row_info)| {
                 let display_row = DisplayRow(rows.start.0 + ix as u32);
-                let non_relative_number = buffer_row.0 + 1;
+                line_number.clear();
+                let non_relative_number = row_info.buffer_row? + 1;
                 let number = relative_rows
                     .get(&display_row)
                     .unwrap_or(&non_relative_number);
                 write!(&mut line_number, "{number}").unwrap();
+                if row_info.diff_status == Some(DiffHunkStatus::Removed) {
+                    return None;
+                }
 
                 let color = cx.theme().colors().editor_line_number;
                 let shaped_line = self
@@ -2152,7 +2133,7 @@ impl EditorElement {
     fn layout_crease_toggles(
         &self,
         rows: Range<DisplayRow>,
-        buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
+        row_infos: &[RowInfo],
         active_rows: &BTreeMap<DisplayRow, bool>,
         snapshot: &EditorSnapshot,
         cx: &mut WindowContext,
@@ -2161,22 +2142,15 @@ impl EditorElement {
             && snapshot.mode == EditorMode::Full
             && self.editor.read(cx).is_singleton(cx);
         if include_fold_statuses {
-            buffer_rows
+            row_infos
                 .into_iter()
                 .enumerate()
-                .map(|(ix, row)| {
-                    if let Some(multibuffer_row) = row {
-                        let display_row = DisplayRow(rows.start.0 + ix as u32);
-                        let active = active_rows.contains_key(&display_row);
-                        snapshot.render_crease_toggle(
-                            multibuffer_row,
-                            active,
-                            self.editor.clone(),
-                            cx,
-                        )
-                    } else {
-                        None
-                    }
+                .map(|(ix, info)| {
+                    let row = info.multibuffer_row?;
+                    let display_row = DisplayRow(rows.start.0 + ix as u32);
+                    let active = active_rows.contains_key(&display_row);
+
+                    snapshot.render_crease_toggle(row, active, self.editor.clone(), cx)
                 })
                 .collect()
         } else {
@@ -2186,15 +2160,15 @@ impl EditorElement {
 
     fn layout_crease_trailers(
         &self,
-        buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
+        buffer_rows: impl IntoIterator<Item = RowInfo>,
         snapshot: &EditorSnapshot,
         cx: &mut WindowContext,
     ) -> Vec<Option<AnyElement>> {
         buffer_rows
             .into_iter()
-            .map(|row| {
-                if let Some(multibuffer_row) = row {
-                    snapshot.render_crease_trailer(multibuffer_row, cx)
+            .map(|row_info| {
+                if let Some(row) = row_info.multibuffer_row {
+                    snapshot.render_crease_trailer(row, cx)
                 } else {
                     None
                 }
@@ -3687,6 +3661,76 @@ impl EditorElement {
         }
     }
 
+    #[allow(clippy::too_many_arguments)]
+    fn layout_diff_hunk_controls(
+        &self,
+        row_range: Range<DisplayRow>,
+        row_infos: &[RowInfo],
+        text_hitbox: &Hitbox,
+        position_map: &PositionMap,
+        newest_cursor_position: Option<DisplayPoint>,
+        line_height: Pixels,
+        scroll_pixel_position: gpui::Point<Pixels>,
+        display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
+        editor: View<Editor>,
+        cx: &mut WindowContext,
+    ) -> Vec<AnyElement> {
+        let point_for_position =
+            position_map.point_for_position(text_hitbox.bounds, cx.mouse_position());
+
+        let mut controls = vec![];
+
+        let active_positions = [
+            Some(point_for_position.previous_valid),
+            newest_cursor_position,
+        ];
+
+        for (hunk, _) in display_hunks {
+            if let DisplayDiffHunk::Unfolded {
+                display_row_range,
+                multi_buffer_range,
+                status,
+                ..
+            } = &hunk
+            {
+                if display_row_range.start < row_range.start
+                    || display_row_range.start >= row_range.end
+                {
+                    continue;
+                }
+                let row_ix = (display_row_range.start - row_range.start).0 as usize;
+                if row_infos[row_ix].diff_status.is_none() {
+                    continue;
+                }
+                if row_infos[row_ix].diff_status == Some(DiffHunkStatus::Added)
+                    && *status != DiffHunkStatus::Added
+                {
+                    continue;
+                }
+                if active_positions
+                    .iter()
+                    .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row())))
+                {
+                    let y = display_row_range.start.as_f32() * line_height
+                        + text_hitbox.bounds.top()
+                        - scroll_pixel_position.y;
+                    let x = text_hitbox.bounds.right() - px(100.);
+
+                    let mut element =
+                        diff_hunk_controls(multi_buffer_range.clone(), line_height, &editor, cx);
+                    element.prepaint_as_root(
+                        gpui::Point::new(x, y),
+                        size(px(100.0), line_height).into(),
+                        cx,
+                    );
+                    controls.push(element);
+                }
+            }
+        }
+
+        controls
+    }
+
     #[allow(clippy::too_many_arguments)]
     fn layout_signature_help(
         &self,
@@ -4047,31 +4091,38 @@ impl EditorElement {
                             Corners::all(px(0.)),
                         ))
                     }
-                    DisplayDiffHunk::Unfolded { status, .. } => {
-                        hitbox.as_ref().map(|hunk_hitbox| match status {
-                            DiffHunkStatus::Added => (
-                                hunk_hitbox.bounds,
-                                cx.theme().status().created,
-                                Corners::all(px(0.)),
-                            ),
-                            DiffHunkStatus::Modified => (
-                                hunk_hitbox.bounds,
-                                cx.theme().status().modified,
-                                Corners::all(px(0.)),
-                            ),
-                            DiffHunkStatus::Removed => (
-                                Bounds::new(
-                                    point(
-                                        hunk_hitbox.origin.x - hunk_hitbox.size.width,
-                                        hunk_hitbox.origin.y,
-                                    ),
-                                    size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height),
+                    DisplayDiffHunk::Unfolded {
+                        status,
+                        display_row_range,
+                        ..
+                    } => hitbox.as_ref().map(|hunk_hitbox| match status {
+                        DiffHunkStatus::Added => (
+                            hunk_hitbox.bounds,
+                            cx.theme().status().created,
+                            Corners::all(px(0.)),
+                        ),
+                        DiffHunkStatus::Modified => (
+                            hunk_hitbox.bounds,
+                            cx.theme().status().modified,
+                            Corners::all(px(0.)),
+                        ),
+                        DiffHunkStatus::Removed if !display_row_range.is_empty() => (
+                            hunk_hitbox.bounds,
+                            cx.theme().status().deleted,
+                            Corners::all(px(0.)),
+                        ),
+                        DiffHunkStatus::Removed => (
+                            Bounds::new(
+                                point(
+                                    hunk_hitbox.origin.x - hunk_hitbox.size.width,
+                                    hunk_hitbox.origin.y,
                                 ),
-                                cx.theme().status().deleted,
-                                Corners::all(1. * line_height),
+                                size(hunk_hitbox.size.width * px(2.), hunk_hitbox.size.height),
                             ),
-                        })
-                    }
+                            cx.theme().status().deleted,
+                            Corners::all(1. * line_height),
+                        ),
+                    }),
                 };
 
                 if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint {
@@ -4110,8 +4161,19 @@ impl EditorElement {
                 display_row_range,
                 status,
                 ..
-            } => match status {
-                DiffHunkStatus::Added | DiffHunkStatus::Modified => {
+            } => {
+                if *status == DiffHunkStatus::Removed && display_row_range.is_empty() {
+                    let row = display_row_range.start;
+
+                    let offset = line_height / 2.;
+                    let start_y = row.as_f32() * line_height - offset - scroll_top;
+                    let end_y = start_y + line_height;
+
+                    let width = (0.35 * line_height).floor();
+                    let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
+                    let highlight_size = size(width, end_y - start_y);
+                    Bounds::new(highlight_origin, highlight_size)
+                } else {
                     let start_row = display_row_range.start;
                     let end_row = display_row_range.end;
                     // If we're in a multibuffer, row range span might include an
@@ -4139,19 +4201,7 @@ impl EditorElement {
                     let highlight_size = size(width, end_y - start_y);
                     Bounds::new(highlight_origin, highlight_size)
                 }
-                DiffHunkStatus::Removed => {
-                    let row = display_row_range.start;
-
-                    let offset = line_height / 2.;
-                    let start_y = row.as_f32() * line_height - offset - scroll_top;
-                    let end_y = start_y + line_height;
-
-                    let width = (0.35 * line_height).floor();
-                    let highlight_origin = gutter_bounds.origin + point(px(0.), start_y);
-                    let highlight_size = size(width, end_y - start_y);
-                    Bounds::new(highlight_origin, highlight_size)
-                }
-            },
+            }
         }
     }
 
@@ -4266,6 +4316,7 @@ impl EditorElement {
                 self.paint_redactions(layout, cx);
                 self.paint_cursors(layout, cx);
                 self.paint_inline_blame(layout, cx);
+                self.paint_diff_hunk_controls(layout, cx);
                 cx.with_element_namespace("crease_trailers", |cx| {
                     for trailer in layout.crease_trailers.iter_mut().flatten() {
                         trailer.element.paint(cx);
@@ -4733,10 +4784,8 @@ impl EditorElement {
                             let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
                             let mut marker_quads = Vec::new();
                             if scrollbar_settings.git_diff {
-                                let marker_row_ranges = snapshot
-                                    .diff_map
-                                    .diff_hunks(&snapshot.buffer_snapshot)
-                                    .map(|hunk| {
+                                let marker_row_ranges =
+                                    snapshot.buffer_snapshot.diff_hunks().map(|hunk| {
                                         let start_display_row =
                                             MultiBufferPoint::new(hunk.row_range.start.0, 0)
                                                 .to_display_point(&snapshot.display_snapshot)
@@ -4748,7 +4797,7 @@ impl EditorElement {
                                         if end_display_row != start_display_row {
                                             end_display_row.0 -= 1;
                                         }
-                                        let color = match hunk_status(&hunk) {
+                                        let color = match &hunk.status() {
                                             DiffHunkStatus::Added => theme.status().created,
                                             DiffHunkStatus::Modified => theme.status().modified,
                                             DiffHunkStatus::Removed => theme.status().deleted,
@@ -4804,11 +4853,7 @@ impl EditorElement {
                             if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
                                 let diagnostics = snapshot
                                     .buffer_snapshot
-                                    .diagnostics_in_range(Point::zero()..max_point, false)
-                                    .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
-                                        diagnostic,
-                                        range: range.to_point(&snapshot.buffer_snapshot),
-                                    })
+                                    .diagnostics_in_range::<_, Point>(Point::zero()..max_point)
                                     // Don't show diagnostics the user doesn't care about
                                     .filter(|diagnostic| {
                                         match (
@@ -4948,6 +4993,12 @@ impl EditorElement {
         }
     }
 
+    fn paint_diff_hunk_controls(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
+        for mut diff_hunk_control in layout.diff_hunk_controls.drain(..) {
+            diff_hunk_control.paint(cx);
+        }
+    }
+
     fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
         for mut block in layout.blocks.drain(..) {
             block.element.paint(cx);
@@ -5033,12 +5084,7 @@ impl EditorElement {
         });
     }
 
-    fn paint_mouse_listeners(
-        &mut self,
-        layout: &EditorLayout,
-        hovered_hunk: Option<HoveredHunk>,
-        cx: &mut WindowContext,
-    ) {
+    fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut WindowContext) {
         self.paint_scroll_wheel_listener(layout, cx);
 
         cx.on_mouse_event({
@@ -5046,6 +5092,26 @@ impl EditorElement {
             let editor = self.editor.clone();
             let text_hitbox = layout.text_hitbox.clone();
             let gutter_hitbox = layout.gutter_hitbox.clone();
+            let hovered_hunk =
+                layout
+                    .display_hunks
+                    .iter()
+                    .find_map(|(hunk, hunk_hitbox)| match hunk {
+                        DisplayDiffHunk::Folded { .. } => None,
+                        DisplayDiffHunk::Unfolded {
+                            multi_buffer_range, ..
+                        } => {
+                            if hunk_hitbox
+                                .as_ref()
+                                .map(|hitbox| hitbox.is_hovered(cx))
+                                .unwrap_or(false)
+                            {
+                                Some(multi_buffer_range.clone())
+                            } else {
+                                None
+                            }
+                        }
+                    });
             let line_numbers = layout.line_numbers.clone();
 
             move |event: &MouseDownEvent, phase, cx| {
@@ -6232,12 +6298,15 @@ impl Element for EditorElement {
                     );
                     let end_row = DisplayRow(end_row);
 
-                    let buffer_rows = snapshot
-                        .buffer_rows(start_row)
+                    let row_infos = snapshot
+                        .row_infos(start_row)
                         .take((start_row..end_row).len())
-                        .collect::<Vec<_>>();
-                    let is_row_soft_wrapped =
-                        |row| buffer_rows.get(row).copied().flatten().is_none();
+                        .collect::<Vec<RowInfo>>();
+                    let is_row_soft_wrapped = |row: usize| {
+                        row_infos
+                            .get(row)
+                            .map_or(true, |info| info.buffer_row.is_none())
+                    };
 
                     let start_anchor = if start_row == Default::default() {
                         Anchor::min()
@@ -6254,9 +6323,21 @@ impl Element for EditorElement {
                         )
                     };
 
-                    let highlighted_rows = self
+                    let mut highlighted_rows = self
                         .editor
                         .update(cx, |editor, cx| editor.highlighted_display_rows(cx));
+
+                    for (ix, row_info) in row_infos.iter().enumerate() {
+                        let color = match row_info.diff_status {
+                            Some(DiffHunkStatus::Added) => style.status.created_background,
+                            Some(DiffHunkStatus::Removed) => style.status.deleted_background,
+                            _ => continue,
+                        };
+                        highlighted_rows
+                            .entry(start_row + DisplayRow(ix as u32))
+                            .or_insert(color);
+                    }
+
                     let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
                         start_anchor..end_anchor,
                         &snapshot.display_snapshot,
@@ -6288,7 +6369,7 @@ impl Element for EditorElement {
                             for selection in all_selections {
                                 for buffer_id in snapshot
                                     .buffer_snapshot
-                                    .buffer_ids_in_selected_rows(selection)
+                                    .buffer_ids_for_range(selection.range())
                                 {
                                     if selected_buffer_ids.last() != Some(&buffer_id) {
                                         selected_buffer_ids.push(buffer_id);
@@ -6323,7 +6404,7 @@ impl Element for EditorElement {
                         line_height,
                         scroll_position,
                         start_row..end_row,
-                        buffer_rows.iter().copied(),
+                        &row_infos,
                         newest_selection_head,
                         &snapshot,
                         cx,
@@ -6332,21 +6413,20 @@ impl Element for EditorElement {
                     let mut crease_toggles = cx.with_element_namespace("crease_toggles", |cx| {
                         self.layout_crease_toggles(
                             start_row..end_row,
-                            buffer_rows.iter().copied(),
+                            &row_infos,
                             &active_rows,
                             &snapshot,
                             cx,
                         )
                     });
                     let crease_trailers = cx.with_element_namespace("crease_trailers", |cx| {
-                        self.layout_crease_trailers(buffer_rows.iter().copied(), &snapshot, cx)
+                        self.layout_crease_trailers(row_infos.iter().copied(), &snapshot, cx)
                     });
 
-                    let display_hunks = self.layout_gutter_git_hunks(
+                    let display_hunks = self.layout_gutter_diff_hunks(
                         line_height,
                         &gutter_hitbox,
                         start_row..end_row,
-                        start_anchor..end_anchor,
                         &snapshot,
                         cx,
                     );
@@ -6504,11 +6584,12 @@ impl Element for EditorElement {
                         let display_row = newest_selection_head.row();
                         if (start_row..end_row).contains(&display_row) {
                             let line_ix = display_row.minus(start_row) as usize;
+                            let row_info = &row_infos[line_ix];
                             let line_layout = &line_layouts[line_ix];
                             let crease_trailer_layout = crease_trailers[line_ix].as_ref();
                             inline_blame = self.layout_inline_blame(
                                 display_row,
-                                &snapshot.display_snapshot,
+                                row_info,
                                 line_layout,
                                 crease_trailer_layout,
                                 em_width,
@@ -6521,7 +6602,7 @@ impl Element for EditorElement {
                     }
 
                     let blamed_display_rows = self.layout_blame_entries(
-                        buffer_rows.into_iter(),
+                        &row_infos,
                         em_width,
                         scroll_position,
                         line_height,
@@ -6612,22 +6693,6 @@ impl Element for EditorElement {
 
                     let gutter_settings = EditorSettings::get_global(cx).gutter;
 
-                    let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| {
-                        editor
-                            .diff_map
-                            .hunks(false)
-                            .filter(|hunk| hunk.status == DiffHunkStatus::Added)
-                            .map(|expanded_hunk| {
-                                let start_row = expanded_hunk
-                                    .hunk_range
-                                    .start
-                                    .to_display_point(&snapshot)
-                                    .row();
-                                (start_row, expanded_hunk.clone())
-                            })
-                            .collect::<HashMap<_, _>>()
-                    });
-
                     let rows_with_hunk_bounds = display_hunks
                         .iter()
                         .filter_map(|(hunk, hitbox)| Some((hunk, hitbox.as_ref()?.bounds)))
@@ -6670,38 +6735,32 @@ impl Element for EditorElement {
                             if show_code_actions {
                                 let newest_selection_point =
                                     newest_selection_head.to_point(&snapshot.display_snapshot);
-                                let newest_selection_display_row =
-                                    newest_selection_point.to_display_point(&snapshot).row();
-                                if !expanded_add_hunks_by_rows
-                                    .contains_key(&newest_selection_display_row)
+                                if !snapshot
+                                    .is_line_folded(MultiBufferRow(newest_selection_point.row))
                                 {
-                                    if !snapshot
-                                        .is_line_folded(MultiBufferRow(newest_selection_point.row))
-                                    {
-                                        let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
-                                            MultiBufferRow(newest_selection_point.row),
-                                        );
-                                        if let Some((buffer, range)) = buffer {
-                                            let buffer_id = buffer.remote_id();
-                                            let row = range.start.row;
-                                            let has_test_indicator = self
-                                                .editor
-                                                .read(cx)
-                                                .tasks
-                                                .contains_key(&(buffer_id, row));
-
-                                            if !has_test_indicator {
-                                                code_actions_indicator = self
-                                                    .layout_code_actions_indicator(
-                                                        line_height,
-                                                        newest_selection_head,
-                                                        scroll_pixel_position,
-                                                        &gutter_dimensions,
-                                                        &gutter_hitbox,
-                                                        &rows_with_hunk_bounds,
-                                                        cx,
-                                                    );
-                                            }
+                                    let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
+                                        MultiBufferRow(newest_selection_point.row),
+                                    );
+                                    if let Some((buffer, range)) = buffer {
+                                        let buffer_id = buffer.remote_id();
+                                        let row = range.start.row;
+                                        let has_test_indicator = self
+                                            .editor
+                                            .read(cx)
+                                            .tasks
+                                            .contains_key(&(buffer_id, row));
+
+                                        if !has_test_indicator {
+                                            code_actions_indicator = self
+                                                .layout_code_actions_indicator(
+                                                    line_height,
+                                                    newest_selection_head,
+                                                    scroll_pixel_position,
+                                                    &gutter_dimensions,
+                                                    &gutter_hitbox,
+                                                    &rows_with_hunk_bounds,
+                                                    cx,
+                                                );
                                         }
                                     }
                                 }
@@ -6816,18 +6875,35 @@ impl Element for EditorElement {
                         )
                         .unwrap();
 
+                    let mode = snapshot.mode;
+
+                    let position_map = Rc::new(PositionMap {
+                        size: bounds.size,
+                        scroll_pixel_position,
+                        scroll_max,
+                        line_layouts,
+                        line_height,
+                        em_width,
+                        em_advance,
+                        snapshot,
+                    });
+
+                    let hunk_controls = self.layout_diff_hunk_controls(
+                        start_row..end_row,
+                        &row_infos,
+                        &text_hitbox,
+                        &position_map,
+                        newest_selection_head,
+                        line_height,
+                        scroll_pixel_position,
+                        &display_hunks,
+                        self.editor.clone(),
+                        cx,
+                    );
+
                     EditorLayout {
-                        mode: snapshot.mode,
-                        position_map: Rc::new(PositionMap {
-                            size: bounds.size,
-                            scroll_pixel_position,
-                            scroll_max,
-                            line_layouts,
-                            line_height,
-                            em_width,
-                            em_advance,
-                            snapshot,
-                        }),
+                        mode,
+                        position_map,
                         visible_display_row_range: start_row..end_row,
                         wrap_guides,
                         indent_guides,
@@ -6851,6 +6927,7 @@ impl Element for EditorElement {
                         visible_cursors,
                         selections,
                         inline_completion_popover,
+                        diff_hunk_controls: hunk_controls,
                         mouse_context_menu,
                         test_indicators,
                         code_actions_indicator,
@@ -6889,37 +6966,11 @@ impl Element for EditorElement {
             line_height: Some(self.style.text.line_height),
             ..Default::default()
         };
-        let hovered_hunk = layout
-            .display_hunks
-            .iter()
-            .find_map(|(hunk, hunk_hitbox)| match hunk {
-                DisplayDiffHunk::Folded { .. } => None,
-                DisplayDiffHunk::Unfolded {
-                    diff_base_byte_range,
-                    multi_buffer_range,
-                    status,
-                    ..
-                } => {
-                    if hunk_hitbox
-                        .as_ref()
-                        .map(|hitbox| hitbox.is_hovered(cx))
-                        .unwrap_or(false)
-                    {
-                        Some(HoveredHunk {
-                            status: *status,
-                            multi_buffer_range: multi_buffer_range.clone(),
-                            diff_base_byte_range: diff_base_byte_range.clone(),
-                        })
-                    } else {
-                        None
-                    }
-                }
-            });
         let rem_size = self.rem_size(cx);
         cx.with_rem_size(rem_size, |cx| {
             cx.with_text_style(Some(text_style), |cx| {
                 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                    self.paint_mouse_listeners(layout, hovered_hunk, cx);
+                    self.paint_mouse_listeners(layout, cx);
                     self.paint_background(layout, cx);
                     self.paint_indent_guides(layout, cx);
 

crates/editor/src/git/blame.rs πŸ”—

@@ -1,5 +1,3 @@
-use std::{sync::Arc, time::Duration};
-
 use anyhow::Result;
 use collections::HashMap;
 use git::{
@@ -9,9 +7,10 @@ use git::{
 use gpui::{AppContext, Model, ModelContext, Subscription, Task};
 use http_client::HttpClient;
 use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
-use multi_buffer::MultiBufferRow;
+use multi_buffer::RowInfo;
 use project::{Project, ProjectItem};
 use smallvec::SmallVec;
+use std::{sync::Arc, time::Duration};
 use sum_tree::SumTree;
 use url::Url;
 
@@ -194,15 +193,15 @@ impl GitBlame {
 
     pub fn blame_for_rows<'a>(
         &'a mut self,
-        rows: impl 'a + IntoIterator<Item = Option<MultiBufferRow>>,
+        rows: &'a [RowInfo],
         cx: &AppContext,
     ) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
         self.sync(cx);
 
         let mut cursor = self.entries.cursor::<u32>(&());
-        rows.into_iter().map(move |row| {
-            let row = row?;
-            cursor.seek_forward(&row.0, Bias::Right, &());
+        rows.into_iter().map(move |info| {
+            let row = info.buffer_row?;
+            cursor.seek_forward(&row, Bias::Right, &());
             cursor.item()?.blame.clone()
         })
     }
@@ -563,15 +562,38 @@ mod tests {
     use unindent::Unindent as _;
     use util::RandomCharIter;
 
-    macro_rules! assert_blame_rows {
-        ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
-            assert_eq!(
-                $blame
-                    .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx)
-                    .collect::<Vec<_>>(),
-                $expected
-            );
-        };
+    // macro_rules! assert_blame_rows {
+    //     ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
+    //         assert_eq!(
+    //             $blame
+    //                 .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx)
+    //                 .collect::<Vec<_>>(),
+    //             $expected
+    //         );
+    //     };
+    // }
+
+    #[track_caller]
+    fn assert_blame_rows(
+        blame: &mut GitBlame,
+        rows: Range<u32>,
+        expected: Vec<Option<BlameEntry>>,
+        cx: &mut ModelContext<GitBlame>,
+    ) {
+        assert_eq!(
+            blame
+                .blame_for_rows(
+                    &rows
+                        .map(|row| RowInfo {
+                            buffer_row: Some(row),
+                            ..Default::default()
+                        })
+                        .collect::<Vec<_>>(),
+                    cx
+                )
+                .collect::<Vec<_>>(),
+            expected
+        );
     }
 
     fn init_test(cx: &mut gpui::TestAppContext) {
@@ -634,7 +656,15 @@ mod tests {
         blame.update(cx, |blame, cx| {
             assert_eq!(
                 blame
-                    .blame_for_rows((0..1).map(MultiBufferRow).map(Some), cx)
+                    .blame_for_rows(
+                        &(0..1)
+                            .map(|row| RowInfo {
+                                buffer_row: Some(row),
+                                ..Default::default()
+                            })
+                            .collect::<Vec<_>>(),
+                        cx
+                    )
                     .collect::<Vec<_>>(),
                 vec![None]
             );
@@ -698,7 +728,15 @@ mod tests {
             // All lines
             assert_eq!(
                 blame
-                    .blame_for_rows((0..8).map(MultiBufferRow).map(Some), cx)
+                    .blame_for_rows(
+                        &(0..8)
+                            .map(|buffer_row| RowInfo {
+                                buffer_row: Some(buffer_row),
+                                ..Default::default()
+                            })
+                            .collect::<Vec<_>>(),
+                        cx
+                    )
                     .collect::<Vec<_>>(),
                 vec![
                     Some(blame_entry("1b1b1b", 0..1)),
@@ -714,7 +752,15 @@ mod tests {
             // Subset of lines
             assert_eq!(
                 blame
-                    .blame_for_rows((1..4).map(MultiBufferRow).map(Some), cx)
+                    .blame_for_rows(
+                        &(1..4)
+                            .map(|buffer_row| RowInfo {
+                                buffer_row: Some(buffer_row),
+                                ..Default::default()
+                            })
+                            .collect::<Vec<_>>(),
+                        cx
+                    )
                     .collect::<Vec<_>>(),
                 vec![
                     Some(blame_entry("0d0d0d", 1..2)),
@@ -725,7 +771,17 @@ mod tests {
             // Subset of lines, with some not displayed
             assert_eq!(
                 blame
-                    .blame_for_rows(vec![Some(MultiBufferRow(1)), None, None], cx)
+                    .blame_for_rows(
+                        &[
+                            RowInfo {
+                                buffer_row: Some(1),
+                                ..Default::default()
+                            },
+                            Default::default(),
+                            Default::default(),
+                        ],
+                        cx
+                    )
                     .collect::<Vec<_>>(),
                 vec![Some(blame_entry("0d0d0d", 1..2)), None, None]
             );
@@ -777,16 +833,16 @@ mod tests {
         git_blame.update(cx, |blame, cx| {
             // Sanity check before edits: make sure that we get the same blame entry for all
             // lines.
-            assert_blame_rows!(
+            assert_blame_rows(
                 blame,
-                (0..4),
+                0..4,
                 vec![
                     Some(blame_entry("1b1b1b", 0..4)),
                     Some(blame_entry("1b1b1b", 0..4)),
                     Some(blame_entry("1b1b1b", 0..4)),
                     Some(blame_entry("1b1b1b", 0..4)),
                 ],
-                cx
+                cx,
             );
         });
 
@@ -795,11 +851,11 @@ mod tests {
             buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "X")], None, cx);
         });
         git_blame.update(cx, |blame, cx| {
-            assert_blame_rows!(
+            assert_blame_rows(
                 blame,
-                (0..2),
+                0..2,
                 vec![None, Some(blame_entry("1b1b1b", 0..4))],
-                cx
+                cx,
             );
         });
         // Modify a single line, in the middle of the line
@@ -807,21 +863,21 @@ mod tests {
             buffer.edit([(Point::new(1, 2)..Point::new(1, 2), "X")], None, cx);
         });
         git_blame.update(cx, |blame, cx| {
-            assert_blame_rows!(
+            assert_blame_rows(
                 blame,
-                (1..4),
+                1..4,
                 vec![
                     None,
                     Some(blame_entry("1b1b1b", 0..4)),
-                    Some(blame_entry("1b1b1b", 0..4))
+                    Some(blame_entry("1b1b1b", 0..4)),
                 ],
-                cx
+                cx,
             );
         });
 
         // Before we insert a newline at the end, sanity check:
         git_blame.update(cx, |blame, cx| {
-            assert_blame_rows!(blame, (3..4), vec![Some(blame_entry("1b1b1b", 0..4))], cx);
+            assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
         });
         // Insert a newline at the end
         buffer.update(cx, |buffer, cx| {
@@ -829,17 +885,17 @@ mod tests {
         });
         // Only the new line is marked as edited:
         git_blame.update(cx, |blame, cx| {
-            assert_blame_rows!(
+            assert_blame_rows(
                 blame,
-                (3..5),
+                3..5,
                 vec![Some(blame_entry("1b1b1b", 0..4)), None],
-                cx
+                cx,
             );
         });
 
         // Before we insert a newline at the start, sanity check:
         git_blame.update(cx, |blame, cx| {
-            assert_blame_rows!(blame, (2..3), vec![Some(blame_entry("1b1b1b", 0..4)),], cx);
+            assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
         });
 
         // Usage example
@@ -849,11 +905,11 @@ mod tests {
         });
         // Only the new line is marked as edited:
         git_blame.update(cx, |blame, cx| {
-            assert_blame_rows!(
+            assert_blame_rows(
                 blame,
-                (2..4),
-                vec![None, Some(blame_entry("1b1b1b", 0..4)),],
-                cx
+                2..4,
+                vec![None, Some(blame_entry("1b1b1b", 0..4))],
+                cx,
             );
         });
     }

crates/editor/src/git/project_diff.rs πŸ”—

@@ -146,7 +146,7 @@ impl ProjectDiffEditor {
         let editor = cx.new_view(|cx| {
             let mut diff_display_editor =
                 Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx);
-            diff_display_editor.set_expand_all_diff_hunks();
+            diff_display_editor.set_expand_all_diff_hunks(cx);
             diff_display_editor
         });
 
@@ -310,9 +310,11 @@ impl ProjectDiffEditor {
                     .update(&mut cx, |project_diff_editor, cx| {
                         project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
                         project_diff_editor.editor.update(cx, |editor, cx| {
-                            for change_set in change_sets {
-                                editor.diff_map.add_change_set(change_set, cx)
-                            }
+                            editor.buffer.update(cx, |buffer, cx| {
+                                for change_set in change_sets {
+                                    buffer.add_change_set(change_set, cx)
+                                }
+                            });
                         });
                     })
                     .ok();
@@ -1105,6 +1107,8 @@ mod tests {
         path::{Path, PathBuf},
     };
 
+    use crate::test::editor_test_context::assert_state_with_diff;
+
     use super::*;
 
     // TODO finish
@@ -1183,19 +1187,13 @@ mod tests {
             let change_set = cx.new_model(|cx| {
                 BufferChangeSet::new_with_base_text(
                     old_text.clone(),
-                    file_a_editor
-                        .buffer()
-                        .read(cx)
-                        .as_singleton()
-                        .unwrap()
-                        .read(cx)
-                        .text_snapshot(),
+                    &file_a_editor.buffer().read(cx).as_singleton().unwrap(),
                     cx,
                 )
             });
-            file_a_editor
-                .diff_map
-                .add_change_set(change_set.clone(), cx);
+            file_a_editor.buffer.update(cx, |buffer, cx| {
+                buffer.add_change_set(change_set.clone(), cx)
+            });
             project.update(cx, |project, cx| {
                 project.buffer_store().update(cx, |buffer_store, cx| {
                     buffer_store.set_change_set(
@@ -1225,15 +1223,17 @@ mod tests {
         cx.executor()
             .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
-
-        project_diff_editor.update(cx, |project_diff_editor, cx| {
-            assert_eq!(
-                // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added)
-                project_diff_editor.editor.read(cx).text(cx),
-                format!("{change}{old_text}"),
-                "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards"
-            );
-        });
+        let editor = project_diff_editor.update(cx, |view, _| view.editor.clone());
+
+        assert_state_with_diff(
+            &editor,
+            cx,
+            indoc::indoc! {
+            "
+                - This is file_a
+                + an edit after git addThis is file_aˇ",
+            },
+        );
     }
 
     fn init_test(cx: &mut gpui::TestAppContext) {

crates/editor/src/hover_popover.rs πŸ”—

@@ -265,12 +265,9 @@ fn show_hover(
 
             let local_diagnostic = snapshot
                 .buffer_snapshot
-                .diagnostics_in_range(anchor..anchor, false)
+                .diagnostics_in_range::<_, usize>(anchor..anchor)
                 // Find the entry with the most specific range
-                .min_by_key(|entry| {
-                    let range = entry.range.to_offset(&snapshot.buffer_snapshot);
-                    range.end - range.start
-                });
+                .min_by_key(|entry| entry.range.len());
 
             let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
                 let text = match local_diagnostic.diagnostic.source {
@@ -279,6 +276,15 @@ fn show_hover(
                     }
                     None => local_diagnostic.diagnostic.message.clone(),
                 };
+                let local_diagnostic = DiagnosticEntry {
+                    diagnostic: local_diagnostic.diagnostic,
+                    range: snapshot
+                        .buffer_snapshot
+                        .anchor_before(local_diagnostic.range.start)
+                        ..snapshot
+                            .buffer_snapshot
+                            .anchor_after(local_diagnostic.range.end),
+                };
 
                 let mut border_color: Option<Hsla> = None;
                 let mut background_color: Option<Hsla> = None;
@@ -770,7 +776,7 @@ impl InfoPopover {
 
 #[derive(Debug, Clone)]
 pub struct DiagnosticPopover {
-    local_diagnostic: DiagnosticEntry<Anchor>,
+    pub(crate) local_diagnostic: DiagnosticEntry<Anchor>,
     parsed_content: Option<View<Markdown>>,
     border_color: Option<Hsla>,
     background_color: Option<Hsla>,
@@ -823,10 +829,6 @@ impl DiagnosticPopover {
 
         diagnostic_div.into_any_element()
     }
-
-    pub fn group_id(&self) -> usize {
-        self.local_diagnostic.diagnostic.group_id
-    }
 }
 
 #[cfg(test)]

crates/editor/src/hunk_diff.rs πŸ”—

@@ -1,1505 +0,0 @@
-use collections::{HashMap, HashSet};
-use git::diff::DiffHunkStatus;
-use gpui::{
-    Action, AppContext, Corner, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View,
-};
-use language::{Buffer, BufferId, Point};
-use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
-    MultiBufferSnapshot, ToOffset, ToPoint,
-};
-use project::buffer_store::BufferChangeSet;
-use std::{ops::Range, sync::Arc};
-use sum_tree::TreeMap;
-use text::OffsetRangeExt;
-use ui::{
-    prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
-    ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext,
-};
-use util::RangeExt;
-use workspace::Item;
-
-use crate::{
-    editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks,
-    ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight,
-    DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
-    RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
-};
-
-#[derive(Debug, Clone)]
-pub(super) struct HoveredHunk {
-    pub multi_buffer_range: Range<Anchor>,
-    pub status: DiffHunkStatus,
-    pub diff_base_byte_range: Range<usize>,
-}
-
-#[derive(Default)]
-pub(super) struct DiffMap {
-    pub(crate) hunks: Vec<ExpandedHunk>,
-    pub(crate) diff_bases: HashMap<BufferId, DiffBaseState>,
-    pub(crate) snapshot: DiffMapSnapshot,
-    hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
-    expand_all: bool,
-}
-
-#[derive(Debug, Clone)]
-pub(super) struct ExpandedHunk {
-    pub blocks: Vec<CustomBlockId>,
-    pub hunk_range: Range<Anchor>,
-    pub diff_base_byte_range: Range<usize>,
-    pub status: DiffHunkStatus,
-    pub folded: bool,
-}
-
-#[derive(Clone, Debug, Default)]
-pub(crate) struct DiffMapSnapshot(TreeMap<BufferId, git::diff::BufferDiff>);
-
-pub(crate) struct DiffBaseState {
-    pub(crate) change_set: Model<BufferChangeSet>,
-    pub(crate) last_version: Option<usize>,
-    _subscription: Subscription,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum DisplayDiffHunk {
-    Folded {
-        display_row: DisplayRow,
-    },
-
-    Unfolded {
-        diff_base_byte_range: Range<usize>,
-        display_row_range: Range<DisplayRow>,
-        multi_buffer_range: Range<Anchor>,
-        status: DiffHunkStatus,
-    },
-}
-
-impl DiffMap {
-    pub fn snapshot(&self) -> DiffMapSnapshot {
-        self.snapshot.clone()
-    }
-
-    pub fn add_change_set(
-        &mut self,
-        change_set: Model<BufferChangeSet>,
-        cx: &mut ViewContext<Editor>,
-    ) {
-        let buffer_id = change_set.read(cx).buffer_id;
-        self.snapshot
-            .0
-            .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
-        self.diff_bases.insert(
-            buffer_id,
-            DiffBaseState {
-                last_version: None,
-                _subscription: cx.observe(&change_set, move |editor, change_set, 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, cx);
-                }),
-                change_set,
-            },
-        );
-        Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
-    }
-
-    pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
-        self.hunks
-            .iter()
-            .filter(move |hunk| include_folded || !hunk.folded)
-    }
-}
-
-impl DiffMapSnapshot {
-    pub fn is_empty(&self) -> bool {
-        self.0.values().all(|diff| diff.is_empty())
-    }
-
-    pub fn diff_hunks<'a>(
-        &'a self,
-        buffer_snapshot: &'a MultiBufferSnapshot,
-    ) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
-        self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot)
-    }
-
-    pub fn diff_hunks_in_range<'a, T: ToOffset>(
-        &'a self,
-        range: Range<T>,
-        buffer_snapshot: &'a MultiBufferSnapshot,
-    ) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
-        let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
-        buffer_snapshot
-            .excerpts_for_range(range.clone())
-            .filter_map(move |excerpt| {
-                let buffer = excerpt.buffer();
-                let buffer_id = buffer.remote_id();
-                let diff = self.0.get(&buffer_id)?;
-                let buffer_range = excerpt.map_range_to_buffer(range.clone());
-                let buffer_range =
-                    buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
-                Some(
-                    diff.hunks_intersecting_range(buffer_range, excerpt.buffer())
-                        .map(move |hunk| {
-                            let start =
-                                excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0));
-                            let end =
-                                excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
-                            MultiBufferDiffHunk {
-                                row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
-                                buffer_id,
-                                buffer_range: hunk.buffer_range.clone(),
-                                diff_base_byte_range: hunk.diff_base_byte_range.clone(),
-                            }
-                        }),
-                )
-            })
-            .flatten()
-    }
-
-    pub fn diff_hunks_in_range_rev<'a, T: ToOffset>(
-        &'a self,
-        range: Range<T>,
-        buffer_snapshot: &'a MultiBufferSnapshot,
-    ) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
-        let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
-        buffer_snapshot
-            .excerpts_for_range_rev(range.clone())
-            .filter_map(move |excerpt| {
-                let buffer = excerpt.buffer();
-                let buffer_id = buffer.remote_id();
-                let diff = self.0.get(&buffer_id)?;
-                let buffer_range = excerpt.map_range_to_buffer(range.clone());
-                let buffer_range =
-                    buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
-                Some(
-                    diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer())
-                        .map(move |hunk| {
-                            let start_row = excerpt
-                                .map_point_from_buffer(Point::new(hunk.row_range.start, 0))
-                                .row;
-                            let end_row = excerpt
-                                .map_point_from_buffer(Point::new(hunk.row_range.end, 0))
-                                .row;
-                            MultiBufferDiffHunk {
-                                row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
-                                buffer_id,
-                                buffer_range: hunk.buffer_range.clone(),
-                                diff_base_byte_range: hunk.diff_base_byte_range.clone(),
-                            }
-                        }),
-                )
-            })
-            .flatten()
-    }
-}
-
-impl Editor {
-    pub fn set_expand_all_diff_hunks(&mut self) {
-        self.diff_map.expand_all = true;
-    }
-
-    pub(super) fn toggle_hovered_hunk(
-        &mut self,
-        hovered_hunk: &HoveredHunk,
-        cx: &mut ViewContext<Editor>,
-    ) {
-        let editor_snapshot = self.snapshot(cx);
-        if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) {
-            self.toggle_hunks_expanded(vec![diff_hunk], cx);
-            self.change_selections(None, cx, |selections| selections.refresh());
-        }
-    }
-
-    pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
-        let snapshot = self.snapshot(cx);
-        let selections = self.selections.all(cx);
-        self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx);
-    }
-
-    pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
-        let snapshot = self.snapshot(cx);
-        let display_rows_with_expanded_hunks = self
-            .diff_map
-            .hunks(false)
-            .map(|hunk| &hunk.hunk_range)
-            .map(|anchor_range| {
-                (
-                    anchor_range
-                        .start
-                        .to_display_point(&snapshot.display_snapshot)
-                        .row(),
-                    anchor_range
-                        .end
-                        .to_display_point(&snapshot.display_snapshot)
-                        .row(),
-                )
-            })
-            .collect::<HashMap<_, _>>();
-        let hunks = self
-            .diff_map
-            .snapshot
-            .diff_hunks(&snapshot.display_snapshot.buffer_snapshot)
-            .filter(|hunk| {
-                let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
-                    .to_display_point(&snapshot.display_snapshot)
-                    ..Point::new(hunk.row_range.end.0, 0)
-                        .to_display_point(&snapshot.display_snapshot);
-                let row_range_end =
-                    display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
-                row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row())
-            });
-        self.toggle_hunks_expanded(hunks.collect(), cx);
-    }
-
-    fn toggle_hunks_expanded(
-        &mut self,
-        hunks_to_toggle: Vec<MultiBufferDiffHunk>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        if self.diff_map.expand_all {
-            return;
-        }
-
-        let previous_toggle_task = self.diff_map.hunk_update_tasks.remove(&None);
-        let new_toggle_task = cx.spawn(move |editor, mut cx| async move {
-            if let Some(task) = previous_toggle_task {
-                task.await;
-            }
-
-            editor
-                .update(&mut cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable();
-                    let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len());
-                    let mut blocks_to_remove = HashSet::default();
-                    let mut hunks_to_expand = Vec::new();
-                    editor.diff_map.hunks.retain(|expanded_hunk| {
-                        if expanded_hunk.folded {
-                            return true;
-                        }
-                        let expanded_hunk_row_range = expanded_hunk
-                            .hunk_range
-                            .start
-                            .to_display_point(&snapshot)
-                            .row()
-                            ..expanded_hunk
-                                .hunk_range
-                                .end
-                                .to_display_point(&snapshot)
-                                .row();
-                        let mut retain = true;
-                        while let Some(hunk_to_toggle) = hunks_to_toggle.peek() {
-                            match diff_hunk_to_display(hunk_to_toggle, &snapshot) {
-                                DisplayDiffHunk::Folded { .. } => {
-                                    hunks_to_toggle.next();
-                                    continue;
-                                }
-                                DisplayDiffHunk::Unfolded {
-                                    diff_base_byte_range,
-                                    display_row_range,
-                                    multi_buffer_range,
-                                    status,
-                                } => {
-                                    let hunk_to_toggle_row_range = display_row_range;
-                                    if hunk_to_toggle_row_range.start > expanded_hunk_row_range.end
-                                    {
-                                        break;
-                                    } else if expanded_hunk_row_range == hunk_to_toggle_row_range {
-                                        highlights_to_remove.push(expanded_hunk.hunk_range.clone());
-                                        blocks_to_remove
-                                            .extend(expanded_hunk.blocks.iter().copied());
-                                        hunks_to_toggle.next();
-                                        retain = false;
-                                        break;
-                                    } else {
-                                        hunks_to_expand.push(HoveredHunk {
-                                            status,
-                                            multi_buffer_range,
-                                            diff_base_byte_range,
-                                        });
-                                        hunks_to_toggle.next();
-                                        continue;
-                                    }
-                                }
-                            }
-                        }
-
-                        retain
-                    });
-                    for hunk in hunks_to_toggle {
-                        let remaining_hunk_point_range = Point::new(hunk.row_range.start.0, 0)
-                            ..Point::new(hunk.row_range.end.0, 0);
-                        let hunk_start = snapshot
-                            .buffer_snapshot
-                            .anchor_before(remaining_hunk_point_range.start);
-                        let hunk_end = snapshot
-                            .buffer_snapshot
-                            .anchor_in_excerpt(hunk_start.excerpt_id, hunk.buffer_range.end)
-                            .unwrap();
-                        hunks_to_expand.push(HoveredHunk {
-                            status: hunk_status(&hunk),
-                            multi_buffer_range: hunk_start..hunk_end,
-                            diff_base_byte_range: hunk.diff_base_byte_range.clone(),
-                        });
-                    }
-
-                    editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
-                    editor.remove_blocks(blocks_to_remove, None, cx);
-                    for hunk in hunks_to_expand {
-                        editor.expand_diff_hunk(None, &hunk, cx);
-                    }
-                    cx.notify();
-                })
-                .ok();
-        });
-
-        self.diff_map
-            .hunk_update_tasks
-            .insert(None, cx.background_executor().spawn(new_toggle_task));
-    }
-
-    pub(super) fn expand_diff_hunk(
-        &mut self,
-        diff_base_buffer: Option<Model<Buffer>>,
-        hunk: &HoveredHunk,
-        cx: &mut ViewContext<Editor>,
-    ) -> Option<()> {
-        let buffer = self.buffer.clone();
-        let multi_buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let hunk_range = hunk.multi_buffer_range.clone();
-        let buffer_id = hunk_range.start.buffer_id?;
-        let diff_base_buffer = diff_base_buffer.or_else(|| {
-            self.diff_map
-                .diff_bases
-                .get(&buffer_id)?
-                .change_set
-                .read(cx)
-                .base_text
-                .clone()
-        })?;
-
-        let diff_base = diff_base_buffer.read(cx);
-        let diff_start_row = diff_base
-            .offset_to_point(hunk.diff_base_byte_range.start)
-            .row;
-        let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
-        let deleted_text_lines = diff_end_row - diff_start_row;
-
-        let block_insert_index = self
-            .diff_map
-            .hunks
-            .binary_search_by(|probe| {
-                probe
-                    .hunk_range
-                    .start
-                    .cmp(&hunk_range.start, &multi_buffer_snapshot)
-            })
-            .err()?;
-
-        let blocks;
-        match hunk.status {
-            DiffHunkStatus::Removed => {
-                blocks = self.insert_blocks(
-                    [
-                        self.hunk_header_block(&hunk, cx),
-                        Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
-                    ],
-                    None,
-                    cx,
-                );
-            }
-            DiffHunkStatus::Added => {
-                self.highlight_rows::<DiffRowHighlight>(
-                    hunk_range.clone(),
-                    added_hunk_color(cx),
-                    false,
-                    cx,
-                );
-                blocks = self.insert_blocks([self.hunk_header_block(&hunk, cx)], None, cx);
-            }
-            DiffHunkStatus::Modified => {
-                self.highlight_rows::<DiffRowHighlight>(
-                    hunk_range.clone(),
-                    added_hunk_color(cx),
-                    false,
-                    cx,
-                );
-                blocks = self.insert_blocks(
-                    [
-                        self.hunk_header_block(&hunk, cx),
-                        Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
-                    ],
-                    None,
-                    cx,
-                );
-            }
-        };
-        self.diff_map.hunks.insert(
-            block_insert_index,
-            ExpandedHunk {
-                blocks,
-                hunk_range,
-                status: hunk.status,
-                folded: false,
-                diff_base_byte_range: hunk.diff_base_byte_range.clone(),
-            },
-        );
-
-        Some(())
-    }
-
-    fn apply_diff_hunks_in_range(
-        &mut self,
-        range: Range<Anchor>,
-        cx: &mut ViewContext<Editor>,
-    ) -> Option<()> {
-        let multi_buffer = self.buffer.read(cx);
-        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-        let (excerpt, range) = multi_buffer_snapshot
-            .range_to_buffer_ranges(range)
-            .into_iter()
-            .next()?;
-
-        multi_buffer
-            .buffer(excerpt.buffer_id())
-            .unwrap()
-            .update(cx, |branch_buffer, cx| {
-                branch_buffer.merge_into_base(vec![range], cx);
-            });
-
-        if let Some(project) = self.project.clone() {
-            self.save(true, project, cx).detach_and_log_err(cx);
-        }
-
-        None
-    }
-
-    pub(crate) fn apply_all_diff_hunks(
-        &mut self,
-        _: &ApplyAllDiffHunks,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let buffers = self.buffer.read(cx).all_buffers();
-        for branch_buffer in buffers {
-            branch_buffer.update(cx, |branch_buffer, cx| {
-                branch_buffer.merge_into_base(Vec::new(), cx);
-            });
-        }
-
-        if let Some(project) = self.project.clone() {
-            self.save(true, project, cx).detach_and_log_err(cx);
-        }
-    }
-
-    pub(crate) fn apply_selected_diff_hunks(
-        &mut self,
-        _: &ApplyDiffHunk,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let snapshot = self.snapshot(cx);
-        let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx));
-        let mut ranges_by_buffer = HashMap::default();
-        self.transact(cx, |editor, cx| {
-            for hunk in hunks {
-                if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
-                    ranges_by_buffer
-                        .entry(buffer.clone())
-                        .or_insert_with(Vec::new)
-                        .push(hunk.buffer_range.to_offset(buffer.read(cx)));
-                }
-            }
-
-            for (buffer, ranges) in ranges_by_buffer {
-                buffer.update(cx, |buffer, cx| {
-                    buffer.merge_into_base(ranges, cx);
-                });
-            }
-        });
-
-        if let Some(project) = self.project.clone() {
-            self.save(true, project, cx).detach_and_log_err(cx);
-        }
-    }
-
-    fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot);
-        hunks.nth(1).is_some()
-    }
-
-    fn hunk_header_block(
-        &self,
-        hunk: &HoveredHunk,
-        cx: &mut ViewContext<Editor>,
-    ) -> BlockProperties<Anchor> {
-        let is_branch_buffer = self
-            .buffer
-            .read(cx)
-            .point_to_buffer_offset(hunk.multi_buffer_range.start, cx)
-            .map_or(false, |(buffer, _, _)| {
-                buffer.read(cx).base_buffer().is_some()
-            });
-
-        let border_color = cx.theme().colors().border_variant;
-        let bg_color = cx.theme().colors().editor_background;
-        let gutter_color = match hunk.status {
-            DiffHunkStatus::Added => cx.theme().status().created,
-            DiffHunkStatus::Modified => cx.theme().status().modified,
-            DiffHunkStatus::Removed => cx.theme().status().deleted,
-        };
-
-        BlockProperties {
-            placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
-            height: 1,
-            style: BlockStyle::Sticky,
-            priority: 0,
-            render: Arc::new({
-                let editor = cx.view().clone();
-                let hunk = hunk.clone();
-                let has_multiple_hunks = self.has_multiple_hunks(cx);
-
-                move |cx| {
-                    let hunk_controls_menu_handle =
-                        editor.read(cx).hunk_controls_menu_handle.clone();
-
-                    h_flex()
-                        .id(cx.block_id)
-                        .block_mouse_down()
-                        .h(cx.line_height())
-                        .w_full()
-                        .border_t_1()
-                        .border_color(border_color)
-                        .bg(bg_color)
-                        .child(
-                            div()
-                                .id("gutter-strip")
-                                .w(EditorElement::diff_hunk_strip_width(cx.line_height()))
-                                .h_full()
-                                .bg(gutter_color)
-                                .cursor(CursorStyle::PointingHand)
-                                .on_click({
-                                    let editor = editor.clone();
-                                    let hunk = hunk.clone();
-                                    move |_event, cx| {
-                                        editor.update(cx, |editor, cx| {
-                                            editor.toggle_hovered_hunk(&hunk, cx);
-                                        });
-                                    }
-                                }),
-                        )
-                        .child(
-                            h_flex()
-                                .px_6()
-                                .size_full()
-                                .justify_end()
-                                .child(
-                                    h_flex()
-                                        .gap_1()
-                                        .when(!is_branch_buffer, |row| {
-                                            row.child(
-                                                IconButton::new("next-hunk", IconName::ArrowDown)
-                                                    .shape(IconButtonShape::Square)
-                                                    .icon_size(IconSize::Small)
-                                                    .disabled(!has_multiple_hunks)
-                                                    .tooltip({
-                                                        let focus_handle = editor.focus_handle(cx);
-                                                        move |cx| {
-                                                            Tooltip::for_action_in(
-                                                                "Next Hunk",
-                                                                &GoToHunk,
-                                                                &focus_handle,
-                                                                cx,
-                                                            )
-                                                        }
-                                                    })
-                                                    .on_click({
-                                                        let editor = editor.clone();
-                                                        let hunk = hunk.clone();
-                                                        move |_event, cx| {
-                                                            editor.update(cx, |editor, cx| {
-                                                                editor.go_to_subsequent_hunk(
-                                                                    hunk.multi_buffer_range.end,
-                                                                    cx,
-                                                                );
-                                                            });
-                                                        }
-                                                    }),
-                                            )
-                                            .child(
-                                                IconButton::new("prev-hunk", IconName::ArrowUp)
-                                                    .shape(IconButtonShape::Square)
-                                                    .icon_size(IconSize::Small)
-                                                    .disabled(!has_multiple_hunks)
-                                                    .tooltip({
-                                                        let focus_handle = editor.focus_handle(cx);
-                                                        move |cx| {
-                                                            Tooltip::for_action_in(
-                                                                "Previous Hunk",
-                                                                &GoToPrevHunk,
-                                                                &focus_handle,
-                                                                cx,
-                                                            )
-                                                        }
-                                                    })
-                                                    .on_click({
-                                                        let editor = editor.clone();
-                                                        let hunk = hunk.clone();
-                                                        move |_event, cx| {
-                                                            editor.update(cx, |editor, cx| {
-                                                                editor.go_to_preceding_hunk(
-                                                                    hunk.multi_buffer_range.start,
-                                                                    cx,
-                                                                );
-                                                            });
-                                                        }
-                                                    }),
-                                            )
-                                        })
-                                        .child(
-                                            IconButton::new("discard", IconName::Undo)
-                                                .shape(IconButtonShape::Square)
-                                                .icon_size(IconSize::Small)
-                                                .tooltip({
-                                                    let focus_handle = editor.focus_handle(cx);
-                                                    move |cx| {
-                                                        Tooltip::for_action_in(
-                                                            "Discard Hunk",
-                                                            &RevertSelectedHunks,
-                                                            &focus_handle,
-                                                            cx,
-                                                        )
-                                                    }
-                                                })
-                                                .on_click({
-                                                    let editor = editor.clone();
-                                                    let hunk = hunk.clone();
-                                                    move |_event, cx| {
-                                                        editor.update(cx, |editor, cx| {
-                                                            editor.revert_hunk(hunk.clone(), cx);
-                                                        });
-                                                    }
-                                                }),
-                                        )
-                                        .map(|this| {
-                                            if is_branch_buffer {
-                                                this.child(
-                                                    IconButton::new("apply", IconName::Check)
-                                                        .shape(IconButtonShape::Square)
-                                                        .icon_size(IconSize::Small)
-                                                        .tooltip({
-                                                            let focus_handle =
-                                                                editor.focus_handle(cx);
-                                                            move |cx| {
-                                                                Tooltip::for_action_in(
-                                                                    "Apply Hunk",
-                                                                    &ApplyDiffHunk,
-                                                                    &focus_handle,
-                                                                    cx,
-                                                                )
-                                                            }
-                                                        })
-                                                        .on_click({
-                                                            let editor = editor.clone();
-                                                            let hunk = hunk.clone();
-                                                            move |_event, cx| {
-                                                                editor.update(cx, |editor, cx| {
-                                                                    editor
-                                                                        .apply_diff_hunks_in_range(
-                                                                            hunk.multi_buffer_range
-                                                                                .clone(),
-                                                                            cx,
-                                                                        );
-                                                                });
-                                                            }
-                                                        }),
-                                                )
-                                            } else {
-                                                this.child({
-                                                    let focus = editor.focus_handle(cx);
-                                                    PopoverMenu::new("hunk-controls-dropdown")
-                                                        .trigger(
-                                                            IconButton::new(
-                                                                "toggle_editor_selections_icon",
-                                                                IconName::EllipsisVertical,
-                                                            )
-                                                            .shape(IconButtonShape::Square)
-                                                            .icon_size(IconSize::Small)
-                                                            .style(ButtonStyle::Subtle)
-                                                            .toggle_state(
-                                                                hunk_controls_menu_handle
-                                                                    .is_deployed(),
-                                                            )
-                                                            .when(
-                                                                !hunk_controls_menu_handle
-                                                                    .is_deployed(),
-                                                                |this| {
-                                                                    this.tooltip(|cx| {
-                                                                        Tooltip::text(
-                                                                            "Hunk Controls",
-                                                                            cx,
-                                                                        )
-                                                                    })
-                                                                },
-                                                            ),
-                                                        )
-                                                        .anchor(Corner::TopRight)
-                                                        .with_handle(hunk_controls_menu_handle)
-                                                        .menu(move |cx| {
-                                                            let focus = focus.clone();
-                                                            let menu = ContextMenu::build(
-                                                                cx,
-                                                                move |menu, _| {
-                                                                    menu.context(focus.clone())
-                                                                        .action(
-                                                                            "Discard All Hunks",
-                                                                            RevertFile
-                                                                                .boxed_clone(),
-                                                                        )
-                                                                },
-                                                            );
-                                                            Some(menu)
-                                                        })
-                                                })
-                                            }
-                                        }),
-                                )
-                                .when(!is_branch_buffer, |div| {
-                                    div.child(
-                                        IconButton::new("collapse", IconName::Close)
-                                            .shape(IconButtonShape::Square)
-                                            .icon_size(IconSize::Small)
-                                            .tooltip({
-                                                let focus_handle = editor.focus_handle(cx);
-                                                move |cx| {
-                                                    Tooltip::for_action_in(
-                                                        "Collapse Hunk",
-                                                        &ToggleHunkDiff,
-                                                        &focus_handle,
-                                                        cx,
-                                                    )
-                                                }
-                                            })
-                                            .on_click({
-                                                let editor = editor.clone();
-                                                let hunk = hunk.clone();
-                                                move |_event, cx| {
-                                                    editor.update(cx, |editor, cx| {
-                                                        editor.toggle_hovered_hunk(&hunk, cx);
-                                                    });
-                                                }
-                                            }),
-                                    )
-                                }),
-                        )
-                        .into_any_element()
-                }
-            }),
-        }
-    }
-
-    fn deleted_text_block(
-        hunk: &HoveredHunk,
-        diff_base_buffer: Model<Buffer>,
-        deleted_text_height: u32,
-        cx: &mut ViewContext<Editor>,
-    ) -> BlockProperties<Anchor> {
-        let gutter_color = match hunk.status {
-            DiffHunkStatus::Added => unreachable!(),
-            DiffHunkStatus::Modified => cx.theme().status().modified,
-            DiffHunkStatus::Removed => cx.theme().status().deleted,
-        };
-        let deleted_hunk_color = deleted_hunk_color(cx);
-        let (editor_height, editor_with_deleted_text) =
-            editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
-        let editor = cx.view().clone();
-        let hunk = hunk.clone();
-        let height = editor_height.max(deleted_text_height);
-        BlockProperties {
-            placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
-            height,
-            style: BlockStyle::Flex,
-            priority: 0,
-            render: Arc::new(move |cx| {
-                let width = EditorElement::diff_hunk_strip_width(cx.line_height());
-                let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
-
-                h_flex()
-                    .id(cx.block_id)
-                    .block_mouse_down()
-                    .bg(deleted_hunk_color)
-                    .h(height as f32 * cx.line_height())
-                    .w_full()
-                    .child(
-                        h_flex()
-                            .id("gutter")
-                            .max_w(gutter_dimensions.full_width())
-                            .min_w(gutter_dimensions.full_width())
-                            .size_full()
-                            .child(
-                                h_flex()
-                                    .id("gutter hunk")
-                                    .bg(gutter_color)
-                                    .pl(gutter_dimensions.margin
-                                        + gutter_dimensions
-                                            .git_blame_entries_width
-                                            .unwrap_or_default())
-                                    .max_w(width)
-                                    .min_w(width)
-                                    .size_full()
-                                    .cursor(CursorStyle::PointingHand)
-                                    .on_mouse_down(MouseButton::Left, {
-                                        let editor = editor.clone();
-                                        let hunk = hunk.clone();
-                                        move |_event, cx| {
-                                            editor.update(cx, |editor, cx| {
-                                                editor.toggle_hovered_hunk(&hunk, cx);
-                                            });
-                                        }
-                                    }),
-                            ),
-                    )
-                    .child(editor_with_deleted_text.clone())
-                    .into_any_element()
-            }),
-        }
-    }
-
-    pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<Editor>) -> bool {
-        if self.diff_map.expand_all {
-            return false;
-        }
-        self.diff_map.hunk_update_tasks.clear();
-        self.clear_row_highlights::<DiffRowHighlight>();
-        let to_remove = self
-            .diff_map
-            .hunks
-            .drain(..)
-            .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
-            .collect::<HashSet<_>>();
-        if to_remove.is_empty() {
-            false
-        } else {
-            self.remove_blocks(to_remove, None, cx);
-            true
-        }
-    }
-
-    pub(super) fn sync_expanded_diff_hunks(
-        diff_map: &mut DiffMap,
-        buffer_id: BufferId,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id);
-        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_buffer_unchanged = false;
-                }
-                diff_base_buffer = change_set.base_text.clone();
-            })
-        }
-
-        diff_map.hunk_update_tasks.remove(&Some(buffer_id));
-
-        let new_sync_task = cx.spawn(move |editor, mut cx| async move {
-            editor
-                .update(&mut cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let mut recalculated_hunks = snapshot
-                        .diff_map
-                        .diff_hunks(&snapshot.buffer_snapshot)
-                        .filter(|hunk| hunk.buffer_id == buffer_id)
-                        .fuse()
-                        .peekable();
-                    let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len());
-                    let mut blocks_to_remove = HashSet::default();
-                    let mut hunks_to_reexpand = Vec::with_capacity(editor.diff_map.hunks.len());
-                    editor.diff_map.hunks.retain_mut(|expanded_hunk| {
-                        if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) {
-                            return true;
-                        };
-
-                        let mut retain = false;
-                        if diff_base_buffer_unchanged {
-                            let expanded_hunk_display_range = expanded_hunk
-                                .hunk_range
-                                .start
-                                .to_display_point(&snapshot)
-                                .row()
-                                ..expanded_hunk
-                                    .hunk_range
-                                    .end
-                                    .to_display_point(&snapshot)
-                                    .row();
-                            while let Some(buffer_hunk) = recalculated_hunks.peek() {
-                                match diff_hunk_to_display(buffer_hunk, &snapshot) {
-                                    DisplayDiffHunk::Folded { display_row } => {
-                                        recalculated_hunks.next();
-                                        if !expanded_hunk.folded
-                                            && expanded_hunk_display_range
-                                                .to_inclusive()
-                                                .contains(&display_row)
-                                        {
-                                            retain = true;
-                                            expanded_hunk.folded = true;
-                                            highlights_to_remove
-                                                .push(expanded_hunk.hunk_range.clone());
-                                            for block in expanded_hunk.blocks.drain(..) {
-                                                blocks_to_remove.insert(block);
-                                            }
-                                            break;
-                                        } else {
-                                            continue;
-                                        }
-                                    }
-                                    DisplayDiffHunk::Unfolded {
-                                        diff_base_byte_range,
-                                        display_row_range,
-                                        multi_buffer_range,
-                                        status,
-                                    } => {
-                                        let hunk_display_range = display_row_range;
-
-                                        if expanded_hunk_display_range.start
-                                            > hunk_display_range.end
-                                        {
-                                            recalculated_hunks.next();
-                                            if editor.diff_map.expand_all {
-                                                hunks_to_reexpand.push(HoveredHunk {
-                                                    status,
-                                                    multi_buffer_range,
-                                                    diff_base_byte_range,
-                                                });
-                                            }
-                                            continue;
-                                        }
-
-                                        if expanded_hunk_display_range.end
-                                            < hunk_display_range.start
-                                        {
-                                            break;
-                                        }
-
-                                        if !expanded_hunk.folded
-                                            && expanded_hunk_display_range == hunk_display_range
-                                            && expanded_hunk.status == hunk_status(buffer_hunk)
-                                            && expanded_hunk.diff_base_byte_range
-                                                == buffer_hunk.diff_base_byte_range
-                                        {
-                                            recalculated_hunks.next();
-                                            retain = true;
-                                        } else {
-                                            hunks_to_reexpand.push(HoveredHunk {
-                                                status,
-                                                multi_buffer_range,
-                                                diff_base_byte_range,
-                                            });
-                                        }
-                                        break;
-                                    }
-                                }
-                            }
-                        }
-                        if !retain {
-                            blocks_to_remove.extend(expanded_hunk.blocks.drain(..));
-                            highlights_to_remove.push(expanded_hunk.hunk_range.clone());
-                        }
-                        retain
-                    });
-
-                    if editor.diff_map.expand_all {
-                        for hunk in recalculated_hunks {
-                            match diff_hunk_to_display(&hunk, &snapshot) {
-                                DisplayDiffHunk::Folded { .. } => {}
-                                DisplayDiffHunk::Unfolded {
-                                    diff_base_byte_range,
-                                    multi_buffer_range,
-                                    status,
-                                    ..
-                                } => {
-                                    hunks_to_reexpand.push(HoveredHunk {
-                                        status,
-                                        multi_buffer_range,
-                                        diff_base_byte_range,
-                                    });
-                                }
-                            }
-                        }
-                    } else {
-                        drop(recalculated_hunks);
-                    }
-
-                    editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
-                    editor.remove_blocks(blocks_to_remove, None, cx);
-
-                    if let Some(diff_base_buffer) = &diff_base_buffer {
-                        for hunk in hunks_to_reexpand {
-                            editor.expand_diff_hunk(Some(diff_base_buffer.clone()), &hunk, cx);
-                        }
-                    }
-                })
-                .ok();
-        });
-
-        diff_map.hunk_update_tasks.insert(
-            Some(buffer_id),
-            cx.background_executor().spawn(new_sync_task),
-        );
-    }
-
-    fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
-        let snapshot = self.snapshot(cx);
-        let position = position.to_point(&snapshot.buffer_snapshot);
-        if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, cx) {
-            let multi_buffer_start = snapshot
-                .buffer_snapshot
-                .anchor_before(Point::new(hunk.row_range.start.0, 0));
-            let multi_buffer_end = snapshot
-                .buffer_snapshot
-                .anchor_after(Point::new(hunk.row_range.end.0, 0));
-            self.expand_diff_hunk(
-                None,
-                &HoveredHunk {
-                    multi_buffer_range: multi_buffer_start..multi_buffer_end,
-                    status: hunk_status(&hunk),
-                    diff_base_byte_range: hunk.diff_base_byte_range,
-                },
-                cx,
-            );
-        }
-    }
-
-    fn go_to_preceding_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
-        let snapshot = self.snapshot(cx);
-        let position = position.to_point(&snapshot.buffer_snapshot);
-        let hunk = self.go_to_hunk_before_position(&snapshot, position, cx);
-        if let Some(hunk) = hunk {
-            let multi_buffer_start = snapshot
-                .buffer_snapshot
-                .anchor_before(Point::new(hunk.row_range.start.0, 0));
-            let multi_buffer_end = snapshot
-                .buffer_snapshot
-                .anchor_after(Point::new(hunk.row_range.end.0, 0));
-            self.expand_diff_hunk(
-                None,
-                &HoveredHunk {
-                    multi_buffer_range: multi_buffer_start..multi_buffer_end,
-                    status: hunk_status(&hunk),
-                    diff_base_byte_range: hunk.diff_base_byte_range,
-                },
-                cx,
-            );
-        }
-    }
-}
-
-pub(crate) fn to_diff_hunk(
-    hovered_hunk: &HoveredHunk,
-    multi_buffer_snapshot: &MultiBufferSnapshot,
-) -> Option<MultiBufferDiffHunk> {
-    let buffer_id = hovered_hunk
-        .multi_buffer_range
-        .start
-        .buffer_id
-        .or(hovered_hunk.multi_buffer_range.end.buffer_id)?;
-    let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
-        ..hovered_hunk.multi_buffer_range.end.text_anchor;
-    let point_range = hovered_hunk
-        .multi_buffer_range
-        .to_point(multi_buffer_snapshot);
-    Some(MultiBufferDiffHunk {
-        row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
-        buffer_id,
-        buffer_range,
-        diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
-    })
-}
-
-fn added_hunk_color(cx: &AppContext) -> Hsla {
-    let mut created_color = cx.theme().status().git().created;
-    created_color.fade_out(0.7);
-    created_color
-}
-
-fn deleted_hunk_color(cx: &AppContext) -> Hsla {
-    let mut deleted_color = cx.theme().status().deleted;
-    deleted_color.fade_out(0.7);
-    deleted_color
-}
-
-fn editor_with_deleted_text(
-    diff_base_buffer: Model<Buffer>,
-    deleted_color: Hsla,
-    hunk: &HoveredHunk,
-    cx: &mut ViewContext<Editor>,
-) -> (u32, View<Editor>) {
-    let parent_editor = cx.view().downgrade();
-    let editor = cx.new_view(|cx| {
-        let multi_buffer =
-            cx.new_model(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
-        multi_buffer.update(cx, |multi_buffer, cx| {
-            multi_buffer.push_excerpts(
-                diff_base_buffer,
-                Some(ExcerptRange {
-                    context: hunk.diff_base_byte_range.clone(),
-                    primary: None,
-                }),
-                cx,
-            );
-        });
-
-        let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
-        editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
-        editor.set_show_wrap_guides(false, cx);
-        editor.set_show_gutter(false, cx);
-        editor.set_show_line_numbers(false, cx);
-        editor.set_show_scrollbars(false, cx);
-        editor.set_show_runnables(false, cx);
-        editor.set_show_git_diff_gutter(false, cx);
-        editor.set_show_code_actions(false, cx);
-        editor.scroll_manager.set_forbid_vertical_scroll(true);
-        editor.set_read_only(true);
-        editor.set_show_inline_completions(Some(false), cx);
-
-        enum DeletedBlockRowHighlight {}
-        editor.highlight_rows::<DeletedBlockRowHighlight>(
-            Anchor::min()..Anchor::max(),
-            deleted_color,
-            false,
-            cx,
-        );
-        editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
-        editor
-            ._subscriptions
-            .extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
-                editor.change_selections(None, cx, |s| {
-                    s.try_cancel();
-                });
-            })]);
-
-        editor
-            .register_action::<RevertSelectedHunks>({
-                let hunk = hunk.clone();
-                let parent_editor = parent_editor.clone();
-                move |_, cx| {
-                    parent_editor
-                        .update(cx, |editor, cx| editor.revert_hunk(hunk.clone(), cx))
-                        .ok();
-                }
-            })
-            .detach();
-        editor
-            .register_action::<ToggleHunkDiff>({
-                let hunk = hunk.clone();
-                move |_, cx| {
-                    parent_editor
-                        .update(cx, |editor, cx| {
-                            editor.toggle_hovered_hunk(&hunk, cx);
-                        })
-                        .ok();
-                }
-            })
-            .detach();
-        editor
-    });
-
-    let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
-    (editor_height, editor)
-}
-
-impl DisplayDiffHunk {
-    pub fn start_display_row(&self) -> DisplayRow {
-        match self {
-            &DisplayDiffHunk::Folded { display_row } => display_row,
-            DisplayDiffHunk::Unfolded {
-                display_row_range, ..
-            } => display_row_range.start,
-        }
-    }
-
-    pub fn contains_display_row(&self, display_row: DisplayRow) -> bool {
-        let range = match self {
-            &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
-
-            DisplayDiffHunk::Unfolded {
-                display_row_range, ..
-            } => display_row_range.start..=display_row_range.end,
-        };
-
-        range.contains(&display_row)
-    }
-}
-
-pub fn diff_hunk_to_display(
-    hunk: &MultiBufferDiffHunk,
-    snapshot: &DisplaySnapshot,
-) -> DisplayDiffHunk {
-    let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
-    let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
-    let hunk_end_point_sub = Point::new(
-        hunk.row_range
-            .end
-            .0
-            .saturating_sub(1)
-            .max(hunk.row_range.start.0),
-        0,
-    );
-
-    let status = hunk_status(hunk);
-    let is_removal = status == DiffHunkStatus::Removed;
-
-    let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
-    let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
-    let folds_range = folds_start..folds_end;
-
-    let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
-        let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
-        let fold_point_range = fold_point_range.start..=fold_point_range.end;
-
-        let folded_start = fold_point_range.contains(&hunk_start_point);
-        let folded_end = fold_point_range.contains(&hunk_end_point_sub);
-        let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
-
-        (folded_start && folded_end) || (is_removal && folded_start_sub)
-    });
-
-    if let Some(fold) = containing_fold {
-        let row = fold.range.start.to_display_point(snapshot).row();
-        DisplayDiffHunk::Folded { display_row: row }
-    } else {
-        let start = hunk_start_point.to_display_point(snapshot).row();
-
-        let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
-        let hunk_end_point = Point::new(hunk_end_row.0, 0);
-
-        let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
-        let multi_buffer_end = snapshot
-            .buffer_snapshot
-            .anchor_in_excerpt(multi_buffer_start.excerpt_id, hunk.buffer_range.end)
-            .unwrap();
-        let end = hunk_end_point.to_display_point(snapshot).row();
-
-        DisplayDiffHunk::Unfolded {
-            display_row_range: start..end,
-            multi_buffer_range: multi_buffer_start..multi_buffer_end,
-            status,
-            diff_base_byte_range: hunk.diff_base_byte_range.clone(),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{editor_tests::init_test, hunk_status};
-    use gpui::{Context, TestAppContext};
-    use language::Capability::ReadWrite;
-    use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
-    use project::{FakeFs, Project};
-    use unindent::Unindent as _;
-
-    #[gpui::test]
-    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
-        use git::diff::DiffHunkStatus;
-        init_test(cx, |_| {});
-
-        let fs = FakeFs::new(cx.background_executor.clone());
-        let project = Project::test(fs, [], cx).await;
-
-        // buffer has two modified hunks with two rows each
-        let diff_base_1 = "
-            1.zero
-            1.one
-            1.two
-            1.three
-            1.four
-            1.five
-            1.six
-        "
-        .unindent();
-
-        let text_1 = "
-            1.zero
-            1.ONE
-            1.TWO
-            1.three
-            1.FOUR
-            1.FIVE
-            1.six
-        "
-        .unindent();
-
-        // buffer has a deletion hunk and an insertion hunk
-        let diff_base_2 = "
-            2.zero
-            2.one
-            2.one-and-a-half
-            2.two
-            2.three
-            2.four
-            2.six
-        "
-        .unindent();
-
-        let text_2 = "
-            2.zero
-            2.one
-            2.two
-            2.three
-            2.four
-            2.five
-            2.six
-        "
-        .unindent();
-
-        let buffer_1 = project.update(cx, |project, cx| {
-            project.create_local_buffer(text_1.as_str(), None, cx)
-        });
-        let buffer_2 = project.update(cx, |project, cx| {
-            project.create_local_buffer(text_2.as_str(), None, cx)
-        });
-
-        let multibuffer = cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(ReadWrite);
-            multibuffer.push_excerpts(
-                buffer_1.clone(),
-                [
-                    // excerpt ends in the middle of a modified hunk
-                    ExcerptRange {
-                        context: Point::new(0, 0)..Point::new(1, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt begins in the middle of a modified hunk
-                    ExcerptRange {
-                        context: Point::new(5, 0)..Point::new(6, 5),
-                        primary: Default::default(),
-                    },
-                ],
-                cx,
-            );
-            multibuffer.push_excerpts(
-                buffer_2.clone(),
-                [
-                    // excerpt ends at a deletion
-                    ExcerptRange {
-                        context: Point::new(0, 0)..Point::new(1, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt starts at a deletion
-                    ExcerptRange {
-                        context: Point::new(2, 0)..Point::new(2, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt fully contains a deletion hunk
-                    ExcerptRange {
-                        context: Point::new(1, 0)..Point::new(2, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt fully contains an insertion hunk
-                    ExcerptRange {
-                        context: Point::new(4, 0)..Point::new(6, 5),
-                        primary: Default::default(),
-                    },
-                ],
-                cx,
-            );
-            multibuffer
-        });
-
-        let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, false, cx));
-        editor
-            .update(cx, |editor, cx| {
-                for (buffer, diff_base) in [
-                    (buffer_1.clone(), diff_base_1),
-                    (buffer_2.clone(), diff_base_2),
-                ] {
-                    let change_set = cx.new_model(|cx| {
-                        BufferChangeSet::new_with_base_text(
-                            diff_base.to_string(),
-                            buffer.read(cx).text_snapshot(),
-                            cx,
-                        )
-                    });
-                    editor.diff_map.add_change_set(change_set, cx)
-                }
-            })
-            .unwrap();
-        cx.background_executor.run_until_parked();
-
-        let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
-
-        assert_eq!(
-            snapshot.buffer_snapshot.text(),
-            "
-                1.zero
-                1.ONE
-                1.FIVE
-                1.six
-                2.zero
-                2.one
-                2.two
-                2.one
-                2.two
-                2.four
-                2.five
-                2.six"
-                .unindent()
-        );
-
-        let expected = [
-            (
-                DiffHunkStatus::Modified,
-                MultiBufferRow(1)..MultiBufferRow(2),
-            ),
-            (
-                DiffHunkStatus::Modified,
-                MultiBufferRow(2)..MultiBufferRow(3),
-            ),
-            //TODO: Define better when and where removed hunks show up at range extremities
-            (
-                DiffHunkStatus::Removed,
-                MultiBufferRow(6)..MultiBufferRow(6),
-            ),
-            (
-                DiffHunkStatus::Removed,
-                MultiBufferRow(8)..MultiBufferRow(8),
-            ),
-            (
-                DiffHunkStatus::Added,
-                MultiBufferRow(10)..MultiBufferRow(11),
-            ),
-        ];
-
-        assert_eq!(
-            snapshot
-                .diff_map
-                .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot)
-                .map(|hunk| (hunk_status(&hunk), hunk.row_range))
-                .collect::<Vec<_>>(),
-            &expected,
-        );
-
-        assert_eq!(
-            snapshot
-                .diff_map
-                .diff_hunks_in_range_rev(
-                    Point::zero()..Point::new(12, 0),
-                    &snapshot.buffer_snapshot
-                )
-                .map(|hunk| (hunk_status(&hunk), hunk.row_range))
-                .collect::<Vec<_>>(),
-            expected
-                .iter()
-                .rev()
-                .cloned()
-                .collect::<Vec<_>>()
-                .as_slice(),
-        );
-    }
-}

crates/editor/src/indent_guides.rs πŸ”—

@@ -2,17 +2,16 @@ use std::{ops::Range, time::Duration};
 
 use collections::HashSet;
 use gpui::{AppContext, Task};
-use language::{language_settings::language_settings, BufferRow};
-use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
-use text::{BufferId, LineIndent, Point};
+use language::language_settings::language_settings;
+use multi_buffer::{IndentGuide, MultiBufferRow};
+use text::{LineIndent, Point};
 use ui::ViewContext;
 use util::ResultExt;
 
 use crate::{DisplaySnapshot, Editor};
 
 struct ActiveIndentedRange {
-    buffer_id: BufferId,
-    row_range: Range<BufferRow>,
+    row_range: Range<MultiBufferRow>,
     indent: LineIndent,
 }
 
@@ -36,7 +35,7 @@ impl Editor {
         visible_buffer_range: Range<MultiBufferRow>,
         snapshot: &DisplaySnapshot,
         cx: &mut ViewContext<Editor>,
-    ) -> Option<Vec<MultiBufferIndentGuide>> {
+    ) -> Option<Vec<IndentGuide>> {
         let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
             if let Some(buffer) = self.buffer().read(cx).as_singleton() {
                 language_settings(
@@ -66,7 +65,7 @@ impl Editor {
 
     pub fn find_active_indent_guide_indices(
         &mut self,
-        indent_guides: &[MultiBufferIndentGuide],
+        indent_guides: &[IndentGuide],
         snapshot: &DisplaySnapshot,
         cx: &mut ViewContext<Editor>,
     ) -> Option<HashSet<usize>> {
@@ -134,9 +133,7 @@ impl Editor {
             .iter()
             .enumerate()
             .filter(|(_, indent_guide)| {
-                indent_guide.buffer_id == active_indent_range.buffer_id
-                    && indent_guide.indent_level()
-                        == active_indent_range.indent.len(indent_guide.tab_size)
+                indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
             });
 
         let mut matches = HashSet::default();
@@ -158,7 +155,7 @@ pub fn indent_guides_in_range(
     ignore_disabled_for_language: bool,
     snapshot: &DisplaySnapshot,
     cx: &AppContext,
-) -> Vec<MultiBufferIndentGuide> {
+) -> Vec<IndentGuide> {
     let start_anchor = snapshot
         .buffer_snapshot
         .anchor_before(Point::new(visible_buffer_range.start.0, 0));
@@ -169,14 +166,12 @@ pub fn indent_guides_in_range(
     snapshot
         .buffer_snapshot
         .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
-        .into_iter()
         .filter(|indent_guide| {
-            if editor.buffer_folded(indent_guide.buffer_id, cx) {
+            if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
                 return false;
             }
 
-            let start =
-                MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
+            let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
             // Filter out indent guides that are inside a fold
             // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
             // Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
@@ -193,24 +188,11 @@ async fn resolve_indented_range(
     snapshot: DisplaySnapshot,
     buffer_row: MultiBufferRow,
 ) -> Option<ActiveIndentedRange> {
-    let (buffer_row, buffer_snapshot, buffer_id) =
-        if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
-            (buffer_row.0, snapshot, buffer_id)
-        } else {
-            let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
-
-            let buffer_id = snapshot.remote_id();
-            (point.start.row, snapshot, buffer_id)
-        };
-
-    buffer_snapshot
+    snapshot
+        .buffer_snapshot
         .enclosing_indent(buffer_row)
         .await
-        .map(|(row_range, indent)| ActiveIndentedRange {
-            row_range,
-            indent,
-            buffer_id,
-        })
+        .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
 }
 
 fn should_recalculate_indented_range(
@@ -222,23 +204,23 @@ fn should_recalculate_indented_range(
     if prev_row.0 == new_row.0 {
         return false;
     }
-    if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
-        if !current_indent_range.row_range.contains(&new_row.0) {
+    if snapshot.buffer_snapshot.is_singleton() {
+        if !current_indent_range.row_range.contains(&new_row) {
             return true;
         }
 
-        let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
-        let new_line_indent = snapshot.line_indent_for_row(new_row.0);
+        let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
+        let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
 
         if old_line_indent.is_line_empty()
             || new_line_indent.is_line_empty()
             || old_line_indent != new_line_indent
-            || snapshot.max_point().row == new_row.0
+            || snapshot.buffer_snapshot.max_point().row == new_row.0
         {
             return true;
         }
 
-        let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
+        let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
         next_line_indent.is_line_empty() || next_line_indent != old_line_indent
     } else {
         true

crates/editor/src/items.rs πŸ”—

@@ -20,7 +20,6 @@ use language::{
     SelectionGoal,
 };
 use lsp::DiagnosticSeverity;
-use multi_buffer::AnchorRangeExt;
 use project::{
     lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Project,
     ProjectItem as _, ProjectPath,
@@ -528,6 +527,7 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
         excerpt_id,
         text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
         buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
+        diff_base_anchor: None,
     })
 }
 
@@ -1435,59 +1435,34 @@ impl SearchableItem for Editor {
         cx.background_executor().spawn(async move {
             let mut ranges = Vec::new();
 
-            if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
-                let search_within_ranges = if search_within_ranges.is_empty() {
-                    vec![None]
-                } else {
-                    search_within_ranges
-                        .into_iter()
-                        .map(|range| Some(range.to_offset(&buffer)))
-                        .collect::<Vec<_>>()
-                };
-
-                for range in search_within_ranges {
-                    let buffer = &buffer;
-                    ranges.extend(
-                        query
-                            .search(excerpt_buffer, range.clone())
-                            .await
-                            .into_iter()
-                            .map(|matched_range| {
-                                let offset = range.clone().map(|r| r.start).unwrap_or(0);
-                                buffer.anchor_after(matched_range.start + offset)
-                                    ..buffer.anchor_before(matched_range.end + offset)
-                            }),
-                    );
-                }
+            let search_within_ranges = if search_within_ranges.is_empty() {
+                vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
             } else {
-                let search_within_ranges = if search_within_ranges.is_empty() {
-                    vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
-                } else {
-                    search_within_ranges
-                };
-
-                for (excerpt_id, search_buffer, search_range) in
-                    buffer.excerpts_in_ranges(search_within_ranges)
-                {
-                    if !search_range.is_empty() {
-                        ranges.extend(
-                            query
-                                .search(search_buffer, Some(search_range.clone()))
-                                .await
-                                .into_iter()
-                                .map(|match_range| {
-                                    let start = search_buffer
-                                        .anchor_after(search_range.start + match_range.start);
-                                    let end = search_buffer
-                                        .anchor_before(search_range.start + match_range.end);
-                                    buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
-                                        ..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
-                                }),
-                        );
-                    }
-                }
+                search_within_ranges
             };
 
+            for (search_buffer, search_range, excerpt_id) in
+                buffer.ranges_to_buffer_ranges(search_within_ranges.into_iter())
+            {
+                ranges.extend(
+                    query
+                        .search(search_buffer, Some(search_range.clone()))
+                        .await
+                        .into_iter()
+                        .map(|match_range| {
+                            let start =
+                                search_buffer.anchor_after(search_range.start + match_range.start);
+                            let end =
+                                search_buffer.anchor_before(search_range.start + match_range.end);
+                            Anchor::range_in_buffer(
+                                excerpt_id,
+                                search_buffer.remote_id(),
+                                start..end,
+                            )
+                        }),
+                );
+            }
+
             ranges
         })
     }

crates/editor/src/proposed_changes_editor.rs πŸ”—

@@ -61,7 +61,7 @@ impl ProposedChangesEditor {
         let mut this = Self {
             editor: cx.new_view(|cx| {
                 let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
-                editor.set_expand_all_diff_hunks();
+                editor.set_expand_all_diff_hunks(cx);
                 editor.set_completion_provider(None);
                 editor.clear_code_action_providers();
                 editor.set_semantics_provider(
@@ -104,16 +104,10 @@ impl ProposedChangesEditor {
                                     let buffer = buffer.read(cx);
                                     let base_buffer = buffer.base_buffer()?;
                                     let buffer = buffer.text_snapshot();
-                                    let change_set = this.editor.update(cx, |editor, _| {
-                                        Some(
-                                            editor
-                                                .diff_map
-                                                .diff_bases
-                                                .get(&buffer.remote_id())?
-                                                .change_set
-                                                .clone(),
-                                        )
-                                    })?;
+                                    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.read(cx).text(),
@@ -193,7 +187,7 @@ impl ProposedChangesEditor {
             } else {
                 branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
                 new_change_sets.push(cx.new_model(|cx| {
-                    let mut change_set = BufferChangeSet::new(branch_buffer.read(cx));
+                    let mut change_set = BufferChangeSet::new(&branch_buffer, cx);
                     let _ = change_set.set_base_text(
                         location.buffer.read(cx).text(),
                         branch_buffer.read(cx).text_snapshot(),
@@ -223,9 +217,11 @@ impl ProposedChangesEditor {
         self.buffer_entries = buffer_entries;
         self.editor.update(cx, |editor, cx| {
             editor.change_selections(None, cx, |selections| selections.refresh());
-            for change_set in new_change_sets {
-                editor.diff_map.add_change_set(change_set, cx)
-            }
+            editor.buffer.update(cx, |buffer, cx| {
+                for change_set in new_change_sets {
+                    buffer.add_change_set(change_set, cx)
+                }
+            })
         });
     }
 

crates/editor/src/selections_collection.rs πŸ”—

@@ -323,8 +323,7 @@ impl SelectionsCollection {
         self.all(cx).last().unwrap().clone()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
+    pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
         cx: &mut AppContext,
     ) -> Vec<Range<D>> {
@@ -332,9 +331,9 @@ impl SelectionsCollection {
             .iter()
             .map(|s| {
                 if s.reversed {
-                    s.end.clone()..s.start.clone()
+                    s.end..s.start
                 } else {
-                    s.start.clone()..s.end.clone()
+                    s.start..s.end
                 }
             })
             .collect()
@@ -921,7 +920,7 @@ pub(crate) fn resolve_selections<'a, D, I>(
     map: &'a DisplaySnapshot,
 ) -> impl 'a + Iterator<Item = Selection<D>>
 where
-    D: TextDimension + Clone + Ord + Sub<D, Output = D>,
+    D: TextDimension + Ord + Sub<D, Output = D>,
     I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
 {
     let (to_convert, selections) = resolve_selections_display(selections, map).tee();

crates/editor/src/signature_help.rs πŸ”—

@@ -5,6 +5,7 @@ use crate::actions::ShowSignatureHelp;
 use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
 use gpui::{AppContext, ViewContext};
 use language::markdown::parse_markdown;
+use language::BufferSnapshot;
 use multi_buffer::{Anchor, ToOffset};
 use settings::Settings;
 use std::ops::Range;
@@ -94,13 +95,14 @@ impl Editor {
             (a, b) if b <= buffer_snapshot.len() => a - 1..b,
             (a, b) => a - 1..b - 1,
         };
-        let not_quote_like_brackets = |start: Range<usize>, end: Range<usize>| {
-            let text = buffer_snapshot.text();
-            let (text_start, text_end) = (text.get(start), text.get(end));
-            QUOTE_PAIRS
-                .into_iter()
-                .all(|(start, end)| text_start != Some(start) && text_end != Some(end))
-        };
+        let not_quote_like_brackets =
+            |buffer: &BufferSnapshot, start: Range<usize>, end: Range<usize>| {
+                let text_start = buffer.text_for_range(start).collect::<String>();
+                let text_end = buffer.text_for_range(end).collect::<String>();
+                QUOTE_PAIRS
+                    .into_iter()
+                    .all(|(start, end)| text_start != start && text_end != end)
+            };
 
         let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
         let previous_brackets_range = bracket_range(previous_position);

crates/editor/src/tasks.rs πŸ”—

@@ -15,7 +15,7 @@ fn task_context_with_editor(
     };
     let (selection, buffer, editor_snapshot) = {
         let selection = editor.selections.newest_adjusted(cx);
-        let Some((buffer, _, _)) = editor
+        let Some((buffer, _)) = editor
             .buffer()
             .read(cx)
             .point_to_buffer_offset(selection.start, cx)

crates/editor/src/test/editor_lsp_test_context.rs πŸ”—

@@ -67,6 +67,13 @@ pub(crate) fn rust_lang() -> Arc<Language> {
             ("<" @open ">" @close)
             ("\"" @open "\"" @close)
             (closure_parameters "|" @open "|" @close)"#})),
+        text_objects: Some(Cow::from(indoc! {r#"
+            (function_item
+                body: (_
+                    "{"
+                    (_)* @function.inside
+                    "}" )) @function.around
+        "#})),
         ..Default::default()
     })
     .expect("Could not parse queries");

crates/editor/src/test/editor_test_context.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
-    display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DiffRowHighlight, DisplayPoint,
-    Editor, MultiBuffer, RowExt,
+    display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
+    RowExt,
 };
 use collections::BTreeMap;
 use futures::Future;
@@ -11,7 +11,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{Buffer, BufferSnapshot, LanguageRegistry};
-use multi_buffer::{ExcerptRange, ToPoint};
+use multi_buffer::{ExcerptRange, MultiBufferRow};
 use parking_lot::RwLock;
 use project::{FakeFs, Project};
 use std::{
@@ -333,85 +333,8 @@ impl EditorTestContext {
     ///
     /// Diff hunks are indicated by lines starting with `+` and `-`.
     #[track_caller]
-    pub fn assert_state_with_diff(&mut self, expected_diff: String) {
-        let has_diff_markers = expected_diff
-            .lines()
-            .any(|line| line.starts_with("+") || line.starts_with("-"));
-        let expected_diff_text = expected_diff
-            .split('\n')
-            .map(|line| {
-                let trimmed = line.trim();
-                if trimmed.is_empty() {
-                    String::new()
-                } else if has_diff_markers {
-                    line.to_string()
-                } else {
-                    format!("  {line}")
-                }
-            })
-            .join("\n");
-
-        let actual_selections = self.editor_selections();
-        let actual_marked_text =
-            generate_marked_text(&self.buffer_text(), &actual_selections, true);
-
-        // Read the actual diff from the editor's row highlights and block
-        // decorations.
-        let actual_diff = self.editor.update(&mut self.cx, |editor, cx| {
-            let snapshot = editor.snapshot(cx);
-            let insertions = editor
-                .highlighted_rows::<DiffRowHighlight>()
-                .map(|(range, _)| {
-                    let start = range.start.to_point(&snapshot.buffer_snapshot);
-                    let end = range.end.to_point(&snapshot.buffer_snapshot);
-                    start.row..end.row
-                })
-                .collect::<Vec<_>>();
-            let deletions = editor
-                .diff_map
-                .hunks
-                .iter()
-                .filter_map(|hunk| {
-                    if hunk.blocks.is_empty() {
-                        return None;
-                    }
-                    let row = hunk
-                        .hunk_range
-                        .start
-                        .to_point(&snapshot.buffer_snapshot)
-                        .row;
-                    let (_, buffer, _) = editor
-                        .buffer()
-                        .read(cx)
-                        .excerpt_containing(hunk.hunk_range.start, cx)
-                        .expect("no excerpt for expanded buffer's hunk start");
-                    let buffer_id = buffer.read(cx).remote_id();
-                    let change_set = &editor
-                        .diff_map
-                        .diff_bases
-                        .get(&buffer_id)
-                        .expect("should have a diff base for expanded hunk")
-                        .change_set;
-                    let deleted_text = change_set
-                        .read(cx)
-                        .base_text
-                        .as_ref()
-                        .expect("no base text for expanded hunk")
-                        .read(cx)
-                        .as_rope()
-                        .slice(hunk.diff_base_byte_range.clone())
-                        .to_string();
-                    if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status {
-                        Some((row, deleted_text))
-                    } else {
-                        None
-                    }
-                })
-                .collect::<Vec<_>>();
-            format_diff(actual_marked_text, deletions, insertions)
-        });
-
-        pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
+    pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
+        assert_state_with_diff(&self.editor, &mut self.cx, &expected_diff_text);
     }
 
     /// Make an assertion about the editor's text and the ranges and directions
@@ -504,44 +427,49 @@ impl EditorTestContext {
     }
 }
 
-fn format_diff(
-    text: String,
-    actual_deletions: Vec<(u32, String)>,
-    actual_insertions: Vec<Range<u32>>,
-) -> String {
-    let mut diff = String::new();
-    for (row, line) in text.split('\n').enumerate() {
-        let row = row as u32;
-        if row > 0 {
-            diff.push('\n');
-        }
-        if let Some(text) = actual_deletions
-            .iter()
-            .find_map(|(deletion_row, deleted_text)| {
-                if *deletion_row == row {
-                    Some(deleted_text)
-                } else {
-                    None
-                }
-            })
-        {
-            for line in text.lines() {
-                diff.push('-');
-                if !line.is_empty() {
-                    diff.push(' ');
-                    diff.push_str(line);
+#[track_caller]
+pub fn assert_state_with_diff(
+    editor: &View<Editor>,
+    cx: &mut VisualTestContext,
+    expected_diff_text: &str,
+) {
+    let (snapshot, selections) = editor.update(cx, |editor, cx| {
+        (
+            editor.snapshot(cx).buffer_snapshot.clone(),
+            editor.selections.ranges::<usize>(cx),
+        )
+    });
+
+    let actual_marked_text = generate_marked_text(&snapshot.text(), &selections, true);
+
+    // Read the actual diff.
+    let line_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
+    let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
+    let actual_diff = actual_marked_text
+        .split('\n')
+        .zip(line_infos)
+        .map(|(line, info)| {
+            let mut marker = match info.diff_status {
+                Some(DiffHunkStatus::Added) => "+ ",
+                Some(DiffHunkStatus::Removed) => "- ",
+                Some(DiffHunkStatus::Modified) => unreachable!(),
+                None => {
+                    if has_diff {
+                        "  "
+                    } else {
+                        ""
+                    }
                 }
-                diff.push('\n');
+            };
+            if line.is_empty() {
+                marker = marker.trim();
             }
-        }
-        let marker = if actual_insertions.iter().any(|range| range.contains(&row)) {
-            "+ "
-        } else {
-            "  "
-        };
-        diff.push_str(format!("{marker}{line}").trim_end());
-    }
-    diff
+            format!("{marker}{line}")
+        })
+        .collect::<Vec<_>>()
+        .join("\n");
+
+    pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
 }
 
 impl Deref for EditorTestContext {

crates/file_finder/src/file_finder.rs πŸ”—

@@ -9,7 +9,7 @@ use futures::future::join_all;
 pub use open_path_prompt::OpenPathDelegate;
 
 use collections::HashMap;
-use editor::{scroll::Autoscroll, Bias, Editor};
+use editor::Editor;
 use file_finder_settings::{FileFinderSettings, FileFinderWidth};
 use file_icons::FileIcons;
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
@@ -1162,13 +1162,7 @@ impl PickerDelegate for FileFinderDelegate {
                             active_editor
                                 .downgrade()
                                 .update(&mut cx, |editor, cx| {
-                                    let snapshot = editor.snapshot(cx).display_snapshot;
-                                    let point = snapshot
-                                        .buffer_snapshot
-                                        .clip_point(Point::new(row, col), Bias::Left);
-                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-                                        s.select_ranges([point..point])
-                                    });
+                                    editor.go_to_singleton_buffer_point(Point::new(row, col), cx);
                                 })
                                 .log_err();
                         }

crates/git/src/diff.rs πŸ”—

@@ -74,7 +74,7 @@ impl BufferDiff {
         }
     }
 
-    pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
+    pub fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
         let mut tree = SumTree::new(buffer);
 
         let buffer_text = buffer.as_rope().to_string();
@@ -119,32 +119,38 @@ impl BufferDiff {
                 !before_start && !after_end
             });
 
-        let anchor_iter = std::iter::from_fn(move || {
+        let anchor_iter = iter::from_fn(move || {
             cursor.next(buffer);
             cursor.item()
         })
         .flat_map(move |hunk| {
             [
-                (&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
-                (&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
+                (
+                    &hunk.buffer_range.start,
+                    (hunk.buffer_range.start, hunk.diff_base_byte_range.start),
+                ),
+                (
+                    &hunk.buffer_range.end,
+                    (hunk.buffer_range.end, hunk.diff_base_byte_range.end),
+                ),
             ]
-            .into_iter()
         });
 
         let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
         iter::from_fn(move || {
-            let (start_point, start_base) = summaries.next()?;
-            let (mut end_point, end_base) = summaries.next()?;
+            let (start_point, (start_anchor, start_base)) = summaries.next()?;
+            let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
 
             if end_point.column > 0 {
                 end_point.row += 1;
                 end_point.column = 0;
+                end_anchor = buffer.anchor_before(end_point);
             }
 
             Some(DiffHunk {
                 row_range: start_point.row..end_point.row,
                 diff_base_byte_range: start_base..end_base,
-                buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point),
+                buffer_range: start_anchor..end_anchor,
             })
         })
     }
@@ -162,7 +168,7 @@ impl BufferDiff {
                 !before_start && !after_end
             });
 
-        std::iter::from_fn(move || {
+        iter::from_fn(move || {
             cursor.prev(buffer);
 
             let hunk = cursor.item()?;
@@ -186,8 +192,8 @@ impl BufferDiff {
         self.tree = SumTree::new(buffer);
     }
 
-    pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
-        *self = Self::build(&diff_base.to_string(), buffer).await;
+    pub fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
+        *self = Self::build(&diff_base.to_string(), buffer);
     }
 
     #[cfg(test)]
@@ -346,7 +352,7 @@ mod tests {
 
         let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
         let mut diff = BufferDiff::new(&buffer);
-        smol::block_on(diff.update(&diff_base_rope, &buffer));
+        diff.update(&diff_base_rope, &buffer);
         assert_hunks(
             diff.hunks(&buffer),
             &buffer,
@@ -355,7 +361,7 @@ mod tests {
         );
 
         buffer.edit([(0..0, "point five\n")]);
-        smol::block_on(diff.update(&diff_base_rope, &buffer));
+        diff.update(&diff_base_rope, &buffer);
         assert_hunks(
             diff.hunks(&buffer),
             &buffer,
@@ -407,7 +413,7 @@ mod tests {
 
         let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
         let mut diff = BufferDiff::new(&buffer);
-        smol::block_on(diff.update(&diff_base_rope, &buffer));
+        diff.update(&diff_base_rope, &buffer);
         assert_eq!(diff.hunks(&buffer).count(), 8);
 
         assert_hunks(

crates/go_to_line/Cargo.toml πŸ”—

@@ -16,6 +16,7 @@ doctest = false
 anyhow.workspace = true
 editor.workspace = true
 gpui.workspace = true
+language.workspace = true
 menu.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/go_to_line/src/cursor_position.rs πŸ”—

@@ -20,7 +20,7 @@ pub(crate) struct SelectionStats {
 }
 
 pub struct CursorPosition {
-    position: Option<Point>,
+    position: Option<(Point, bool)>,
     selected_count: SelectionStats,
     context: Option<FocusHandle>,
     workspace: WeakView<Workspace>,
@@ -97,8 +97,11 @@ impl CursorPosition {
                                         }
                                     }
                                 }
-                                cursor_position.position =
-                                    last_selection.map(|s| s.head().to_point(&buffer));
+                                cursor_position.position = last_selection.and_then(|s| {
+                                    buffer
+                                        .point_to_buffer_point(s.head().to_point(&buffer))
+                                        .map(|(_, point, is_main_buffer)| (point, is_main_buffer))
+                                });
                                 cursor_position.context = Some(editor.focus_handle(cx));
                             }
                         }
@@ -163,9 +166,10 @@ impl CursorPosition {
 
 impl Render for CursorPosition {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        div().when_some(self.position, |el, position| {
+        div().when_some(self.position, |el, (position, is_main_buffer)| {
             let mut text = format!(
-                "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+                "{}{}{FILE_ROW_COLUMN_DELIMITER}{}",
+                if is_main_buffer { "" } else { "(deleted) " },
                 position.row + 1,
                 position.column + 1
             );
@@ -183,8 +187,12 @@ impl Render for CursorPosition {
                                     .active_item(cx)
                                     .and_then(|item| item.act_as::<Editor>(cx))
                                 {
-                                    workspace
-                                        .toggle_modal(cx, |cx| crate::GoToLine::new(editor, cx))
+                                    if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
+                                    {
+                                        workspace.toggle_modal(cx, |cx| {
+                                            crate::GoToLine::new(editor, buffer, cx)
+                                        })
+                                    }
                                 }
                             });
                         }

crates/go_to_line/src/go_to_line.rs πŸ”—

@@ -1,13 +1,15 @@
 pub mod cursor_position;
 
 use cursor_position::LineIndicatorFormat;
-use editor::{scroll::Autoscroll, Editor};
+use editor::{scroll::Autoscroll, Anchor, Editor, MultiBuffer, ToPoint};
 use gpui::{
     div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
-    FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+    FocusableView, Model, Render, SharedString, Styled, Subscription, View, ViewContext,
+    VisualContext,
 };
+use language::Buffer;
 use settings::Settings;
-use text::{Bias, Point};
+use text::Point;
 use theme::ActiveTheme;
 use ui::prelude::*;
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
@@ -21,6 +23,7 @@ pub fn init(cx: &mut AppContext) {
 pub struct GoToLine {
     line_editor: View<Editor>,
     active_editor: View<Editor>,
+    active_buffer: Model<Buffer>,
     current_text: SharedString,
     prev_scroll_position: Option<gpui::Point<f32>>,
     _subscriptions: Vec<Subscription>,
@@ -42,22 +45,43 @@ impl GoToLine {
         let handle = cx.view().downgrade();
         editor
             .register_action(move |_: &editor::actions::ToggleGoToLine, cx| {
-                let Some(editor) = handle.upgrade() else {
+                let Some(editor_handle) = handle.upgrade() else {
                     return;
                 };
-                let Some(workspace) = editor.read(cx).workspace() else {
+                let Some(workspace) = editor_handle.read(cx).workspace() else {
+                    return;
+                };
+                let editor = editor_handle.read(cx);
+                let Some((_, buffer, _)) = editor.active_excerpt(cx) else {
                     return;
                 };
                 workspace.update(cx, |workspace, cx| {
-                    workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
+                    workspace.toggle_modal(cx, move |cx| GoToLine::new(editor_handle, buffer, cx));
                 })
             })
             .detach();
     }
 
-    pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
-        let cursor =
-            active_editor.update(cx, |editor, cx| editor.selections.last::<Point>(cx).head());
+    pub fn new(
+        active_editor: View<Editor>,
+        active_buffer: Model<Buffer>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let (cursor, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
+            let cursor = editor.selections.last::<Point>(cx).head();
+            let snapshot = active_buffer.read(cx).snapshot();
+
+            let last_line = editor
+                .buffer()
+                .read(cx)
+                .excerpts_for_buffer(&active_buffer, cx)
+                .into_iter()
+                .map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row)
+                .max()
+                .unwrap_or(0);
+
+            (cursor, last_line, editor.scroll_position(cx))
+        });
 
         let line = cursor.row + 1;
         let column = cursor.column + 1;
@@ -69,15 +93,17 @@ impl GoToLine {
         });
         let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event);
 
-        let editor = active_editor.read(cx);
-        let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row;
-        let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx));
-
-        let current_text = format!("{} of {} (column {})", line, last_line + 1, column);
+        let current_text = format!(
+            "Current Line: {} of {} (column {})",
+            line,
+            last_line + 1,
+            column
+        );
 
         Self {
             line_editor,
             active_editor,
+            active_buffer,
             current_text: current_text.into(),
             prev_scroll_position: Some(scroll_position),
             _subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
@@ -113,35 +139,40 @@ impl GoToLine {
     }
 
     fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(point) = self.point_from_query(cx) {
-            self.active_editor.update(cx, |active_editor, cx| {
-                let snapshot = active_editor.snapshot(cx).display_snapshot;
-                let start = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
-                let end = start + Point::new(1, 0);
-                let start = snapshot.buffer_snapshot.anchor_before(start);
-                let end = snapshot.buffer_snapshot.anchor_after(end);
-                active_editor.clear_row_highlights::<GoToLineRowHighlights>();
-                active_editor.highlight_rows::<GoToLineRowHighlights>(
-                    start..end,
-                    cx.theme().colors().editor_highlighted_line_background,
-                    true,
-                    cx,
-                );
-                active_editor.request_autoscroll(Autoscroll::center(), cx);
-            });
-            cx.notify();
-        }
+        self.active_editor.update(cx, |editor, cx| {
+            editor.clear_row_highlights::<GoToLineRowHighlights>();
+            let multibuffer = editor.buffer().read(cx);
+            let snapshot = multibuffer.snapshot(cx);
+            let Some(start) = self.anchor_from_query(&multibuffer, cx) else {
+                return;
+            };
+            let start_point = start.to_point(&snapshot);
+            let end_point = start_point + Point::new(1, 0);
+            let end = snapshot.anchor_after(end_point);
+            editor.highlight_rows::<GoToLineRowHighlights>(
+                start..end,
+                cx.theme().colors().editor_highlighted_line_background,
+                true,
+                cx,
+            );
+            editor.request_autoscroll(Autoscroll::center(), cx);
+        });
+        cx.notify();
     }
 
-    fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
-        let (row, column) = self.line_column_from_query(cx);
-        Some(Point::new(
-            row?.saturating_sub(1),
-            column.unwrap_or(0).saturating_sub(1),
-        ))
+    fn anchor_from_query(
+        &self,
+        multibuffer: &MultiBuffer,
+        cx: &ViewContext<Editor>,
+    ) -> Option<Anchor> {
+        let (Some(row), column) = self.line_column_from_query(cx) else {
+            return None;
+        };
+        let point = Point::new(row.saturating_sub(1), column.unwrap_or(0).saturating_sub(1));
+        multibuffer.buffer_point_to_anchor(&self.active_buffer, point, cx)
     }
 
-    fn line_column_from_query(&self, cx: &ViewContext<Self>) -> (Option<u32>, Option<u32>) {
+    fn line_column_from_query(&self, cx: &AppContext) -> (Option<u32>, Option<u32>) {
         let input = self.line_editor.read(cx).text(cx);
         let mut components = input
             .splitn(2, FILE_ROW_COLUMN_DELIMITER)
@@ -157,18 +188,18 @@ impl GoToLine {
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        if let Some(point) = self.point_from_query(cx) {
-            self.active_editor.update(cx, |editor, cx| {
-                let snapshot = editor.snapshot(cx).display_snapshot;
-                let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
-                editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-                    s.select_ranges([point..point])
-                });
-                editor.focus(cx);
-                cx.notify();
+        self.active_editor.update(cx, |editor, cx| {
+            let multibuffer = editor.buffer().read(cx);
+            let Some(start) = self.anchor_from_query(&multibuffer, cx) else {
+                return;
+            };
+            editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+                s.select_anchor_ranges([start..start])
             });
-            self.prev_scroll_position.take();
-        }
+            editor.focus(cx);
+            cx.notify()
+        });
+        self.prev_scroll_position.take();
 
         cx.emit(DismissEvent);
     }
@@ -205,7 +236,6 @@ impl Render for GoToLine {
                     .px_2()
                     .py_1()
                     .gap_1()
-                    .child(Label::new("Current Line:").color(Color::Muted))
                     .child(Label::new(help_text).color(Color::Muted)),
             )
     }

crates/language/src/buffer.rs πŸ”—

@@ -6,7 +6,7 @@ pub use crate::{
 };
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
-    language_settings::{language_settings, IndentGuideSettings, LanguageSettings},
+    language_settings::{language_settings, LanguageSettings},
     markdown::parse_markdown,
     outline::OutlineItem,
     syntax_map::{
@@ -144,7 +144,7 @@ struct BufferBranchState {
 /// An immutable, cheaply cloneable representation of a fixed
 /// state of a buffer.
 pub struct BufferSnapshot {
-    text: text::BufferSnapshot,
+    pub text: text::BufferSnapshot,
     pub(crate) syntax: SyntaxSnapshot,
     file: Option<Arc<dyn File>>,
     diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
@@ -587,22 +587,6 @@ pub struct Runnable {
     pub buffer: BufferId,
 }
 
-#[derive(Clone, Debug, PartialEq)]
-pub struct IndentGuide {
-    pub buffer_id: BufferId,
-    pub start_row: BufferRow,
-    pub end_row: BufferRow,
-    pub depth: u32,
-    pub tab_size: u32,
-    pub settings: IndentGuideSettings,
-}
-
-impl IndentGuide {
-    pub fn indent_level(&self) -> u32 {
-        self.depth * self.tab_size
-    }
-}
-
 #[derive(Clone)]
 pub struct EditPreview {
     applied_edits_snapshot: text::BufferSnapshot,
@@ -937,6 +921,36 @@ impl Buffer {
         }
     }
 
+    pub fn build_snapshot(
+        text: Rope,
+        language: Option<Arc<Language>>,
+        language_registry: Option<Arc<LanguageRegistry>>,
+        cx: &mut AppContext,
+    ) -> impl Future<Output = BufferSnapshot> {
+        let entity_id = cx.reserve_model::<Self>().entity_id();
+        let buffer_id = entity_id.as_non_zero_u64().into();
+        async move {
+            let text =
+                TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
+            let mut syntax = SyntaxMap::new(&text).snapshot();
+            if let Some(language) = language.clone() {
+                let text = text.clone();
+                let language = language.clone();
+                let language_registry = language_registry.clone();
+                syntax.reparse(&text, language_registry, language);
+            }
+            BufferSnapshot {
+                text,
+                syntax,
+                file: None,
+                diagnostics: Default::default(),
+                remote_selections: Default::default(),
+                language,
+                non_text_state_update_count: 0,
+            }
+        }
+    }
+
     /// Retrieve a snapshot of the buffer's current state. This is computationally
     /// cheap, and allows reading from the buffer on a background thread.
     pub fn snapshot(&self) -> BufferSnapshot {
@@ -2633,7 +2647,8 @@ impl Buffer {
             last_end = Some(range.end);
 
             let new_text_len = rng.gen_range(0..10);
-            let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect();
+            let mut new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect();
+            new_text = new_text.to_uppercase();
 
             edits.push((range, new_text));
         }
@@ -3730,10 +3745,8 @@ impl BufferSnapshot {
 
     pub fn runnable_ranges(
         &self,
-        range: Range<Anchor>,
+        offset_range: Range<usize>,
     ) -> impl Iterator<Item = RunnableRange> + '_ {
-        let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
-
         let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
             grammar.runnable_config.as_ref().map(|config| &config.query)
         });
@@ -3833,245 +3846,6 @@ impl BufferSnapshot {
         })
     }
 
-    pub fn indent_guides_in_range(
-        &self,
-        range: Range<Anchor>,
-        ignore_disabled_for_language: bool,
-        cx: &AppContext,
-    ) -> Vec<IndentGuide> {
-        let language_settings =
-            language_settings(self.language().map(|l| l.name()), self.file.as_ref(), cx);
-        let settings = language_settings.indent_guides;
-        if !ignore_disabled_for_language && !settings.enabled {
-            return Vec::new();
-        }
-        let tab_size = language_settings.tab_size.get() as u32;
-
-        let start_row = range.start.to_point(self).row;
-        let end_row = range.end.to_point(self).row;
-        let row_range = start_row..end_row + 1;
-
-        let mut row_indents = self.line_indents_in_row_range(row_range.clone());
-
-        let mut result_vec = Vec::new();
-        let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
-
-        while let Some((first_row, mut line_indent)) = row_indents.next() {
-            let current_depth = indent_stack.len() as u32;
-
-            // When encountering empty, continue until found useful line indent
-            // then add to the indent stack with the depth found
-            let mut found_indent = false;
-            let mut last_row = first_row;
-            if line_indent.is_line_empty() {
-                let mut trailing_row = end_row;
-                while !found_indent {
-                    let (target_row, new_line_indent) =
-                        if let Some(display_row) = row_indents.next() {
-                            display_row
-                        } else {
-                            // This means we reached the end of the given range and found empty lines at the end.
-                            // We need to traverse further until we find a non-empty line to know if we need to add
-                            // an indent guide for the last visible indent.
-                            trailing_row += 1;
-
-                            const TRAILING_ROW_SEARCH_LIMIT: u32 = 25;
-                            if trailing_row > self.max_point().row
-                                || trailing_row > end_row + TRAILING_ROW_SEARCH_LIMIT
-                            {
-                                break;
-                            }
-                            let new_line_indent = self.line_indent_for_row(trailing_row);
-                            (trailing_row, new_line_indent)
-                        };
-
-                    if new_line_indent.is_line_empty() {
-                        continue;
-                    }
-                    last_row = target_row.min(end_row);
-                    line_indent = new_line_indent;
-                    found_indent = true;
-                    break;
-                }
-            } else {
-                found_indent = true
-            }
-
-            let depth = if found_indent {
-                line_indent.len(tab_size) / tab_size
-                    + ((line_indent.len(tab_size) % tab_size) > 0) as u32
-            } else {
-                current_depth
-            };
-
-            match depth.cmp(&current_depth) {
-                Ordering::Less => {
-                    for _ in 0..(current_depth - depth) {
-                        let mut indent = indent_stack.pop().unwrap();
-                        if last_row != first_row {
-                            // In this case, we landed on an empty row, had to seek forward,
-                            // and discovered that the indent we where on is ending.
-                            // This means that the last display row must
-                            // be on line that ends this indent range, so we
-                            // should display the range up to the first non-empty line
-                            indent.end_row = first_row.saturating_sub(1);
-                        }
-
-                        result_vec.push(indent)
-                    }
-                }
-                Ordering::Greater => {
-                    for next_depth in current_depth..depth {
-                        indent_stack.push(IndentGuide {
-                            buffer_id: self.remote_id(),
-                            start_row: first_row,
-                            end_row: last_row,
-                            depth: next_depth,
-                            tab_size,
-                            settings,
-                        });
-                    }
-                }
-                _ => {}
-            }
-
-            for indent in indent_stack.iter_mut() {
-                indent.end_row = last_row;
-            }
-        }
-
-        result_vec.extend(indent_stack);
-
-        result_vec
-    }
-
-    pub async fn enclosing_indent(
-        &self,
-        mut buffer_row: BufferRow,
-    ) -> Option<(Range<BufferRow>, LineIndent)> {
-        let max_row = self.max_point().row;
-        if buffer_row >= max_row {
-            return None;
-        }
-
-        let mut target_indent = self.line_indent_for_row(buffer_row);
-
-        // If the current row is at the start of an indented block, we want to return this
-        // block as the enclosing indent.
-        if !target_indent.is_line_empty() && buffer_row < max_row {
-            let next_line_indent = self.line_indent_for_row(buffer_row + 1);
-            if !next_line_indent.is_line_empty()
-                && target_indent.raw_len() < next_line_indent.raw_len()
-            {
-                target_indent = next_line_indent;
-                buffer_row += 1;
-            }
-        }
-
-        const SEARCH_ROW_LIMIT: u32 = 25000;
-        const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500;
-        const YIELD_INTERVAL: u32 = 100;
-
-        let mut accessed_row_counter = 0;
-
-        // If there is a blank line at the current row, search for the next non indented lines
-        if target_indent.is_line_empty() {
-            let start = buffer_row.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT);
-            let end = (max_row + 1).min(buffer_row + SEARCH_WHITESPACE_ROW_LIMIT);
-
-            let mut non_empty_line_above = None;
-            for (row, indent) in self
-                .text
-                .reversed_line_indents_in_row_range(start..buffer_row)
-            {
-                accessed_row_counter += 1;
-                if accessed_row_counter == YIELD_INTERVAL {
-                    accessed_row_counter = 0;
-                    yield_now().await;
-                }
-                if !indent.is_line_empty() {
-                    non_empty_line_above = Some((row, indent));
-                    break;
-                }
-            }
-
-            let mut non_empty_line_below = None;
-            for (row, indent) in self.text.line_indents_in_row_range((buffer_row + 1)..end) {
-                accessed_row_counter += 1;
-                if accessed_row_counter == YIELD_INTERVAL {
-                    accessed_row_counter = 0;
-                    yield_now().await;
-                }
-                if !indent.is_line_empty() {
-                    non_empty_line_below = Some((row, indent));
-                    break;
-                }
-            }
-
-            let (row, indent) = match (non_empty_line_above, non_empty_line_below) {
-                (Some((above_row, above_indent)), Some((below_row, below_indent))) => {
-                    if above_indent.raw_len() >= below_indent.raw_len() {
-                        (above_row, above_indent)
-                    } else {
-                        (below_row, below_indent)
-                    }
-                }
-                (Some(above), None) => above,
-                (None, Some(below)) => below,
-                _ => return None,
-            };
-
-            target_indent = indent;
-            buffer_row = row;
-        }
-
-        let start = buffer_row.saturating_sub(SEARCH_ROW_LIMIT);
-        let end = (max_row + 1).min(buffer_row + SEARCH_ROW_LIMIT);
-
-        let mut start_indent = None;
-        for (row, indent) in self
-            .text
-            .reversed_line_indents_in_row_range(start..buffer_row)
-        {
-            accessed_row_counter += 1;
-            if accessed_row_counter == YIELD_INTERVAL {
-                accessed_row_counter = 0;
-                yield_now().await;
-            }
-            if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() {
-                start_indent = Some((row, indent));
-                break;
-            }
-        }
-        let (start_row, start_indent_size) = start_indent?;
-
-        let mut end_indent = (end, None);
-        for (row, indent) in self.text.line_indents_in_row_range((buffer_row + 1)..end) {
-            accessed_row_counter += 1;
-            if accessed_row_counter == YIELD_INTERVAL {
-                accessed_row_counter = 0;
-                yield_now().await;
-            }
-            if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() {
-                end_indent = (row.saturating_sub(1), Some(indent));
-                break;
-            }
-        }
-        let (end_row, end_indent_size) = end_indent;
-
-        let indent = if let Some(end_indent_size) = end_indent_size {
-            if start_indent_size.raw_len() > end_indent_size.raw_len() {
-                start_indent_size
-            } else {
-                end_indent_size
-            }
-        } else {
-            start_indent_size
-        };
-
-        Some((start_row..end_row, indent))
-    }
-
     /// Returns selections for remote peers intersecting the given range.
     #[allow(clippy::type_complexity)]
     pub fn selections_in_range(
@@ -4395,6 +4169,10 @@ impl<'a> BufferChunks<'a> {
         self.range.start
     }
 
+    pub fn range(&self) -> Range<usize> {
+        self.range.clone()
+    }
+
     fn update_diagnostic_depths(&mut self, endpoint: DiagnosticEndpoint) {
         let depth = match endpoint.severity {
             DiagnosticSeverity::ERROR => &mut self.error_depth,

crates/language/src/buffer_tests.rs πŸ”—

@@ -21,7 +21,7 @@ use std::{
 };
 use syntax_map::TreeSitterOptions;
 use text::network::Network;
-use text::{BufferId, LineEnding, LineIndent};
+use text::{BufferId, LineEnding};
 use text::{Point, ToPoint};
 use unindent::Unindent as _;
 use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
@@ -2475,92 +2475,6 @@ fn test_serialization(cx: &mut gpui::AppContext) {
     assert_eq!(buffer2.read(cx).text(), "abcDF");
 }
 
-#[gpui::test]
-async fn test_find_matching_indent(cx: &mut TestAppContext) {
-    cx.update(|cx| init_settings(cx, |_| {}));
-
-    async fn enclosing_indent(
-        text: impl Into<String>,
-        buffer_row: u32,
-        cx: &mut TestAppContext,
-    ) -> Option<(Range<u32>, LineIndent)> {
-        let buffer = cx.new_model(|cx| Buffer::local(text, cx));
-        let snapshot = cx.read(|cx| buffer.read(cx).snapshot());
-        snapshot.enclosing_indent(buffer_row).await
-    }
-
-    assert_eq!(
-        enclosing_indent(
-            "
-        fn b() {
-            if c {
-                let d = 2;
-            }
-        }"
-            .unindent(),
-            1,
-            cx,
-        )
-        .await,
-        Some((
-            1..2,
-            LineIndent {
-                tabs: 0,
-                spaces: 4,
-                line_blank: false,
-            }
-        ))
-    );
-
-    assert_eq!(
-        enclosing_indent(
-            "
-        fn b() {
-            if c {
-                let d = 2;
-            }
-        }"
-            .unindent(),
-            2,
-            cx,
-        )
-        .await,
-        Some((
-            1..2,
-            LineIndent {
-                tabs: 0,
-                spaces: 4,
-                line_blank: false,
-            }
-        ))
-    );
-
-    assert_eq!(
-        enclosing_indent(
-            "
-        fn b() {
-            if c {
-                let d = 2;
-
-                let e = 5;
-            }
-        }"
-            .unindent(),
-            3,
-            cx,
-        )
-        .await,
-        Some((
-            1..4,
-            LineIndent {
-                tabs: 0,
-                spaces: 4,
-                line_blank: false,
-            }
-        ))
-    );
-}
-
 #[gpui::test]
 fn test_branch_and_merge(cx: &mut TestAppContext) {
     cx.update(|cx| init_settings(cx, |_| {}));

crates/language_tools/src/syntax_tree_view.rs πŸ”—

@@ -131,15 +131,15 @@ impl SyntaxTreeView {
         let snapshot = editor_state
             .editor
             .update(cx, |editor, cx| editor.snapshot(cx));
-        let (excerpt, buffer, range) = editor_state.editor.update(cx, |editor, cx| {
+        let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
             let selection_range = editor.selections.last::<usize>(cx).range();
             let multi_buffer = editor.buffer().read(cx);
-            let (excerpt, range) = snapshot
+            let (buffer, range, excerpt_id) = snapshot
                 .buffer_snapshot
                 .range_to_buffer_ranges(selection_range)
                 .pop()?;
-            let buffer = multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone();
-            Some((excerpt, buffer, range))
+            let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone();
+            Some((buffer, range, excerpt_id))
         })?;
 
         // If the cursor has moved into a different excerpt, retrieve a new syntax layer
@@ -148,16 +148,16 @@ impl SyntaxTreeView {
             .active_buffer
             .get_or_insert_with(|| BufferState {
                 buffer: buffer.clone(),
-                excerpt_id: excerpt.id(),
+                excerpt_id,
                 active_layer: None,
             });
         let mut prev_layer = None;
         if did_reparse {
             prev_layer = buffer_state.active_layer.take();
         }
-        if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt.id() {
+        if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
             buffer_state.buffer = buffer.clone();
-            buffer_state.excerpt_id = excerpt.id();
+            buffer_state.excerpt_id = excerpt_id;
             buffer_state.active_layer = None;
         }
 

crates/multi_buffer/Cargo.toml πŸ”—

@@ -27,12 +27,16 @@ collections.workspace = true
 ctor.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
 settings.workspace = true
 serde.workspace = true
 smallvec.workspace = true
@@ -45,7 +49,10 @@ util.workspace = true
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
+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 πŸ”—

@@ -12,14 +12,38 @@ pub struct Anchor {
     pub buffer_id: Option<BufferId>,
     pub excerpt_id: ExcerptId,
     pub text_anchor: text::Anchor,
+    pub diff_base_anchor: Option<text::Anchor>,
 }
 
 impl Anchor {
+    pub fn in_buffer(
+        excerpt_id: ExcerptId,
+        buffer_id: BufferId,
+        text_anchor: text::Anchor,
+    ) -> Self {
+        Self {
+            buffer_id: Some(buffer_id),
+            excerpt_id,
+            text_anchor,
+            diff_base_anchor: None,
+        }
+    }
+
+    pub fn range_in_buffer(
+        excerpt_id: ExcerptId,
+        buffer_id: BufferId,
+        range: Range<text::Anchor>,
+    ) -> Range<Self> {
+        Self::in_buffer(excerpt_id, buffer_id, range.start)
+            ..Self::in_buffer(excerpt_id, buffer_id, range.end)
+    }
+
     pub fn min() -> Self {
         Self {
             buffer_id: None,
             excerpt_id: ExcerptId::min(),
             text_anchor: text::Anchor::MIN,
+            diff_base_anchor: None,
         }
     }
 
@@ -28,22 +52,47 @@ impl Anchor {
             buffer_id: None,
             excerpt_id: ExcerptId::max(),
             text_anchor: text::Anchor::MAX,
+            diff_base_anchor: None,
         }
     }
 
     pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
         let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
-        if excerpt_id_cmp.is_eq() {
-            if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
-                Ordering::Equal
-            } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
-                self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
-            } else {
-                Ordering::Equal
+        if excerpt_id_cmp.is_ne() {
+            return excerpt_id_cmp;
+        }
+        if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
+            return Ordering::Equal;
+        }
+        if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
+            let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer);
+            if text_cmp.is_ne() {
+                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));
+                    return match (self_anchor, other_anchor) {
+                        (Some(a), Some(b)) => a.cmp(&b, &diff_base.base_text),
+                        (Some(_), None) => match other.text_anchor.bias {
+                            Bias::Left => Ordering::Greater,
+                            Bias::Right => Ordering::Less,
+                        },
+                        (None, Some(_)) => match self.text_anchor.bias {
+                            Bias::Left => Ordering::Less,
+                            Bias::Right => Ordering::Greater,
+                        },
+                        (None, None) => Ordering::Equal,
+                    };
+                }
             }
-        } else {
-            excerpt_id_cmp
         }
+        Ordering::Equal
     }
 
     pub fn bias(&self) -> Bias {
@@ -57,6 +106,14 @@ impl Anchor {
                     buffer_id: self.buffer_id,
                     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);
+                            }
+                        }
+                        a
+                    }),
                 };
             }
         }
@@ -70,6 +127,14 @@ impl Anchor {
                     buffer_id: self.buffer_id,
                     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);
+                            }
+                        }
+                        a
+                    }),
                 };
             }
         }

crates/multi_buffer/src/multi_buffer.rs πŸ”—

@@ -1,27 +1,34 @@
 mod anchor;
 #[cfg(test)]
 mod multi_buffer_tests;
+mod position;
 
 pub use anchor::{Anchor, AnchorRangeExt, Offset};
+pub use position::{TypedOffset, TypedPoint, TypedRow};
+
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet};
 use futures::{channel::mpsc, SinkExt};
+use git::diff::DiffHunkStatus;
 use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext, Task};
 use itertools::Itertools;
 use language::{
-    language_settings::{language_settings, LanguageSettings},
+    language_settings::{language_settings, IndentGuideSettings, LanguageSettings},
     AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
-    CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuide, IndentSize,
-    Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16,
-    Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _,
-    TransactionId, Unclipped,
+    CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentSize, Language,
+    LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection,
+    TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions,
+    Unclipped,
 };
+use project::buffer_store::BufferChangeSet;
+use rope::DimensionPair;
 use smallvec::SmallVec;
+use smol::future::yield_now;
 use std::{
     any::type_name,
     borrow::Cow,
-    cell::{Ref, RefCell},
+    cell::{Ref, RefCell, RefMut},
     cmp, fmt,
     future::Future,
     io,
@@ -32,14 +39,13 @@ use std::{
     sync::Arc,
     time::{Duration, Instant},
 };
-use sum_tree::{Bias, Cursor, SumTree};
+use sum_tree::{Bias, Cursor, SumTree, TreeMap};
 use text::{
     locator::Locator,
     subscription::{Subscription, Topic},
-    BufferId, Edit, TextSummary,
+    BufferId, Edit, LineIndent, TextSummary,
 };
 use theme::SyntaxTheme;
-
 use util::post_inc;
 
 #[cfg(any(test, feature = "test-support"))]
@@ -50,12 +56,6 @@ const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
 #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
 pub struct ExcerptId(usize);
 
-impl From<ExcerptId> for EntityId {
-    fn from(id: ExcerptId) -> Self {
-        EntityId::from(id.0 as u64)
-    }
-}
-
 /// One or more [`Buffers`](Buffer) being edited in a single view.
 ///
 /// See <https://zed.dev/features#multi-buffers>
@@ -65,6 +65,8 @@ pub struct MultiBuffer {
     snapshot: RefCell<MultiBufferSnapshot>,
     /// Contains the state of the buffers being edited
     buffers: RefCell<HashMap<BufferId, BufferState>>,
+    diff_bases: HashMap<BufferId, ChangeSetState>,
+    all_diff_hunks_expanded: bool,
     subscriptions: Topic,
     /// If true, the multi-buffer only contains a single [`Buffer`] and a single [`Excerpt`]
     singleton: bool,
@@ -119,11 +121,27 @@ pub struct MultiBufferDiffHunk {
     pub buffer_id: BufferId,
     /// The range of the underlying buffer that this hunk corresponds to.
     pub buffer_range: Range<text::Anchor>,
+    /// The excerpt that contains the diff hunk.
+    pub excerpt_id: ExcerptId,
     /// The range within the buffer's diff base that this hunk corresponds to.
     pub diff_base_byte_range: Range<usize>,
 }
 
+impl MultiBufferDiffHunk {
+    pub fn status(&self) -> DiffHunkStatus {
+        if self.buffer_range.start == self.buffer_range.end {
+            DiffHunkStatus::Removed
+        } else if self.diff_base_byte_range.is_empty() {
+            DiffHunkStatus::Added
+        } else {
+            DiffHunkStatus::Modified
+        }
+    }
+}
+
 pub type MultiBufferPoint = Point;
+type ExcerptOffset = TypedOffset<Excerpt>;
+type ExcerptPoint = TypedPoint<Excerpt>;
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)]
 #[serde(transparent)]
@@ -134,6 +152,14 @@ impl MultiBufferRow {
     pub const MAX: Self = Self(u32::MAX);
 }
 
+impl std::ops::Add<usize> for MultiBufferRow {
+    type Output = Self;
+
+    fn add(self, rhs: usize) -> Self::Output {
+        MultiBufferRow(self.0 + rhs as u32)
+    }
+}
+
 #[derive(Clone)]
 struct History {
     next_transaction_id: TransactionId,
@@ -176,12 +202,19 @@ struct BufferState {
     _subscriptions: [gpui::Subscription; 2],
 }
 
+struct ChangeSetState {
+    change_set: Model<BufferChangeSet>,
+    _subscription: gpui::Subscription,
+}
+
 /// The contents of a [`MultiBuffer`] at a single point in time.
 #[derive(Clone, Default)]
 pub struct MultiBufferSnapshot {
     singleton: bool,
     excerpts: SumTree<Excerpt>,
     excerpt_ids: SumTree<ExcerptIdMapping>,
+    diffs: TreeMap<BufferId, DiffSnapshot>,
+    pub diff_transforms: SumTree<DiffTransform>,
     trailing_excerpt_update_count: usize,
     non_text_state_update_count: usize,
     edit_count: usize,
@@ -191,13 +224,34 @@ pub struct MultiBufferSnapshot {
     show_headers: bool,
 }
 
+#[derive(Debug, Clone)]
+pub enum DiffTransform {
+    BufferContent {
+        summary: TextSummary,
+        inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>,
+    },
+    DeletedHunk {
+        summary: TextSummary,
+        buffer_id: BufferId,
+        hunk_anchor: (ExcerptId, text::Anchor),
+        base_text_byte_range: Range<usize>,
+        has_trailing_newline: bool,
+    },
+}
+
+#[derive(Clone)]
+struct DiffSnapshot {
+    diff: git::diff::BufferDiff,
+    base_text: language::BufferSnapshot,
+}
+
 #[derive(Clone)]
 pub struct ExcerptInfo {
     pub id: ExcerptId,
     pub buffer: BufferSnapshot,
     pub buffer_id: BufferId,
     pub range: ExcerptRange<text::Anchor>,
-    pub text_summary: TextSummary,
+    pub end_row: MultiBufferRow,
 }
 
 impl std::fmt::Debug for ExcerptInfo {
@@ -230,6 +284,13 @@ impl ExcerptBoundary {
     }
 }
 
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub struct RowInfo {
+    pub buffer_row: Option<u32>,
+    pub multibuffer_row: Option<MultiBufferRow>,
+    pub diff_status: Option<git::diff::DiffHunkStatus>,
+}
+
 /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`].
 #[derive(Clone)]
 struct Excerpt {
@@ -257,8 +318,11 @@ struct Excerpt {
 #[derive(Clone)]
 pub struct MultiBufferExcerpt<'a> {
     excerpt: &'a Excerpt,
-    excerpt_offset: usize,
-    excerpt_position: Point,
+    diff_transforms:
+        sum_tree::Cursor<'a, DiffTransform, (OutputDimension<usize>, ExcerptDimension<usize>)>,
+    offset: usize,
+    excerpt_offset: ExcerptDimension<usize>,
+    buffer_offset: usize,
 }
 
 #[derive(Clone, Debug)]
@@ -287,50 +351,92 @@ pub struct ExcerptSummary {
     text: TextSummary,
 }
 
+#[derive(Debug, Clone)]
+pub struct DiffTransformSummary {
+    input: TextSummary,
+    output: TextSummary,
+}
+
 #[derive(Clone)]
 pub struct MultiBufferRows<'a> {
-    buffer_row_range: Range<u32>,
-    excerpts: Cursor<'a, Excerpt, Point>,
+    point: Point,
+    is_empty: bool,
+    cursor: MultiBufferCursor<'a, Point>,
 }
 
 pub struct MultiBufferChunks<'a> {
+    excerpts: Cursor<'a, Excerpt, ExcerptOffset>,
+    diff_transforms: Cursor<'a, DiffTransform, (usize, ExcerptOffset)>,
+    diffs: &'a TreeMap<BufferId, DiffSnapshot>,
+    diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>,
+    buffer_chunk: Option<Chunk<'a>>,
     range: Range<usize>,
-    excerpts: Cursor<'a, Excerpt, usize>,
+    excerpt_offset_range: Range<ExcerptOffset>,
     excerpt_chunks: Option<ExcerptChunks<'a>>,
     language_aware: bool,
 }
 
+pub struct ReversedMultiBufferChunks<'a> {
+    cursor: MultiBufferCursor<'a, usize>,
+    current_chunks: Option<rope::Chunks<'a>>,
+    start: usize,
+    offset: usize,
+}
+
 pub struct MultiBufferBytes<'a> {
     range: Range<usize>,
-    excerpts: Cursor<'a, Excerpt, usize>,
-    excerpt_bytes: Option<ExcerptBytes<'a>>,
+    cursor: MultiBufferCursor<'a, usize>,
+    excerpt_bytes: Option<text::Bytes<'a>>,
+    has_trailing_newline: bool,
     chunk: &'a [u8],
 }
 
 pub struct ReversedMultiBufferBytes<'a> {
     range: Range<usize>,
-    excerpts: Cursor<'a, Excerpt, usize>,
-    excerpt_bytes: Option<ExcerptBytes<'a>>,
+    chunks: ReversedMultiBufferChunks<'a>,
     chunk: &'a [u8],
 }
 
+#[derive(Clone)]
+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>,
+    cached_region: Option<MultiBufferRegion<'a, D>>,
+}
+
+#[derive(Clone)]
+struct MultiBufferRegion<'a, D: TextDimension> {
+    buffer: &'a BufferSnapshot,
+    is_main_buffer: bool,
+    is_inserted_hunk: bool,
+    excerpt: &'a Excerpt,
+    buffer_range: Range<D>,
+    range: Range<D>,
+    has_trailing_newline: bool,
+}
+
 struct ExcerptChunks<'a> {
     excerpt_id: ExcerptId,
     content_chunks: BufferChunks<'a>,
     footer_height: usize,
 }
 
-struct ExcerptBytes<'a> {
-    content_bytes: text::Bytes<'a>,
-    padding_height: usize,
-    reversed: bool,
-}
-
+#[derive(Debug)]
 struct BufferEdit {
     range: Range<usize>,
     new_text: Arc<str>,
     is_insertion: bool,
     original_indent_column: u32,
+    excerpt_id: ExcerptId,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum DiffChangeKind {
+    BufferEdited,
+    ExcerptsChanged,
+    DiffUpdated { base_changed: bool },
+    ExpandOrCollapseHunks { expand: bool },
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -359,16 +465,18 @@ impl ExpandExcerptDirection {
 }
 
 #[derive(Clone, Debug, PartialEq)]
-pub struct MultiBufferIndentGuide {
-    pub multibuffer_row_range: Range<MultiBufferRow>,
-    pub buffer: IndentGuide,
+pub struct IndentGuide {
+    pub buffer_id: BufferId,
+    pub start_row: MultiBufferRow,
+    pub end_row: MultiBufferRow,
+    pub depth: u32,
+    pub tab_size: u32,
+    pub settings: IndentGuideSettings,
 }
 
-impl std::ops::Deref for MultiBufferIndentGuide {
-    type Target = IndentGuide;
-
-    fn deref(&self) -> &Self::Target {
-        &self.buffer
+impl IndentGuide {
+    pub fn indent_level(&self) -> u32 {
+        self.depth * self.tab_size
     }
 }
 
@@ -380,6 +488,8 @@ impl MultiBuffer {
                 ..MultiBufferSnapshot::default()
             }),
             buffers: RefCell::default(),
+            diff_bases: HashMap::default(),
+            all_diff_hunks_expanded: false,
             subscriptions: Topic::default(),
             singleton: false,
             capability,
@@ -398,6 +508,8 @@ impl MultiBuffer {
         Self {
             snapshot: Default::default(),
             buffers: Default::default(),
+            diff_bases: HashMap::default(),
+            all_diff_hunks_expanded: false,
             subscriptions: Default::default(),
             singleton: false,
             capability,
@@ -429,9 +541,22 @@ 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 {
+                    _subscription: new_cx
+                        .observe(&change_set_state.change_set, Self::buffer_diff_changed),
+                    change_set: change_set_state.change_set.clone(),
+                },
+            );
+        }
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
             buffers: RefCell::new(buffers),
+            diff_bases,
+            all_diff_hunks_expanded: self.all_diff_hunks_expanded,
             subscriptions: Default::default(),
             singleton: self.singleton,
             capability: self.capability,
@@ -566,16 +691,6 @@ impl MultiBuffer {
                 return;
             }
 
-            if let Some(buffer) = this.as_singleton() {
-                buffer.update(cx, |buffer, cx| {
-                    buffer.edit(edits, autoindent_mode, cx);
-                });
-                cx.emit(Event::ExcerptsEdited {
-                    ids: this.excerpt_ids(),
-                });
-                return;
-            }
-
             let original_indent_columns = match &mut autoindent_mode {
                 Some(AutoindentMode::Block {
                     original_indent_columns,
@@ -588,7 +703,7 @@ impl MultiBuffer {
             drop(snapshot);
 
             for (buffer_id, mut edits) in buffer_edits {
-                edits.sort_unstable_by_key(|edit| edit.range.start);
+                edits.sort_by_key(|edit| edit.range.start);
                 this.buffers.borrow()[&buffer_id]
                     .buffer
                     .update(cx, |buffer, cx| {
@@ -599,20 +714,26 @@ impl MultiBuffer {
                         let empty_str: Arc<str> = Arc::default();
                         while let Some(BufferEdit {
                             mut range,
-                            new_text,
+                            mut new_text,
                             mut is_insertion,
                             original_indent_column,
+                            excerpt_id,
                         }) = edits.next()
                         {
                             while let Some(BufferEdit {
                                 range: next_range,
                                 is_insertion: next_is_insertion,
+                                new_text: next_new_text,
+                                excerpt_id: next_excerpt_id,
                                 ..
                             }) = edits.peek()
                             {
                                 if range.end >= next_range.start {
                                     range.end = cmp::max(next_range.end, range.end);
                                     is_insertion |= *next_is_insertion;
+                                    if excerpt_id == *next_excerpt_id {
+                                        new_text = format!("{new_text}{next_new_text}").into();
+                                    }
                                     edits.next();
                                 } else {
                                     break;
@@ -671,96 +792,113 @@ impl MultiBuffer {
     ) -> (HashMap<BufferId, Vec<BufferEdit>>, Vec<ExcerptId>) {
         let mut buffer_edits: HashMap<BufferId, Vec<BufferEdit>> = Default::default();
         let mut edited_excerpt_ids = Vec::new();
-        let mut cursor = snapshot.excerpts.cursor::<usize>(&());
+        let mut cursor = snapshot.cursor::<usize>();
         for (ix, (range, new_text)) in edits.into_iter().enumerate() {
             let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0);
-            cursor.seek(&range.start, Bias::Right, &());
-            if cursor.item().is_none() && range.start == *cursor.start() {
-                cursor.prev(&());
+
+            cursor.seek_forward(&range.start);
+            let mut start_region = cursor.region().expect("start offset out of bounds");
+            if !start_region.is_main_buffer {
+                cursor.next();
+                if let Some(region) = cursor.region() {
+                    start_region = region;
+                } else {
+                    continue;
+                }
             }
-            let start_excerpt = cursor.item().expect("start offset out of bounds");
-            let start_overshoot = range.start - cursor.start();
-            let buffer_start = start_excerpt
-                .range
-                .context
-                .start
-                .to_offset(&start_excerpt.buffer)
-                + start_overshoot;
-            edited_excerpt_ids.push(start_excerpt.id);
 
-            cursor.seek(&range.end, Bias::Right, &());
-            if cursor.item().is_none() && range.end == *cursor.start() {
-                cursor.prev(&());
+            if range.end < start_region.range.start {
+                continue;
             }
-            let end_excerpt = cursor.item().expect("end offset out of bounds");
-            let end_overshoot = range.end - cursor.start();
-            let buffer_end = end_excerpt
-                .range
-                .context
-                .start
-                .to_offset(&end_excerpt.buffer)
-                + end_overshoot;
 
-            if start_excerpt.id == end_excerpt.id {
-                buffer_edits
-                    .entry(start_excerpt.buffer_id)
-                    .or_default()
-                    .push(BufferEdit {
-                        range: buffer_start..buffer_end,
-                        new_text,
-                        is_insertion: true,
-                        original_indent_column,
-                    });
-            } else {
-                edited_excerpt_ids.push(end_excerpt.id);
-                let start_excerpt_range = buffer_start
-                    ..start_excerpt
-                        .range
-                        .context
-                        .end
-                        .to_offset(&start_excerpt.buffer);
-                let end_excerpt_range = end_excerpt
-                    .range
-                    .context
-                    .start
-                    .to_offset(&end_excerpt.buffer)
-                    ..buffer_end;
-                buffer_edits
-                    .entry(start_excerpt.buffer_id)
-                    .or_default()
-                    .push(BufferEdit {
-                        range: start_excerpt_range,
-                        new_text: new_text.clone(),
-                        is_insertion: true,
-                        original_indent_column,
-                    });
-                buffer_edits
-                    .entry(end_excerpt.buffer_id)
-                    .or_default()
-                    .push(BufferEdit {
-                        range: end_excerpt_range,
-                        new_text: new_text.clone(),
-                        is_insertion: false,
-                        original_indent_column,
-                    });
+            if range.end > start_region.range.end {
+                cursor.seek_forward(&range.end);
+            }
+            let mut end_region = cursor.region().expect("end offset out of bounds");
+            if !end_region.is_main_buffer {
+                cursor.prev();
+                if let Some(region) = cursor.region() {
+                    end_region = region;
+                } else {
+                    continue;
+                }
+            }
 
-                cursor.seek(&range.start, Bias::Right, &());
-                cursor.next(&());
-                while let Some(excerpt) = cursor.item() {
-                    if excerpt.id == end_excerpt.id {
-                        break;
-                    }
+            if range.start > end_region.range.end {
+                continue;
+            }
+
+            let start_overshoot = range.start.saturating_sub(start_region.range.start);
+            let end_overshoot = range.end.saturating_sub(end_region.range.start);
+            let buffer_start = (start_region.buffer_range.start + start_overshoot)
+                .min(start_region.buffer_range.end);
+            let buffer_end =
+                (end_region.buffer_range.start + end_overshoot).min(end_region.buffer_range.end);
+
+            if start_region.excerpt.id == end_region.excerpt.id {
+                if start_region.is_main_buffer {
+                    edited_excerpt_ids.push(start_region.excerpt.id);
                     buffer_edits
-                        .entry(excerpt.buffer_id)
+                        .entry(start_region.buffer.remote_id())
                         .or_default()
                         .push(BufferEdit {
-                            range: excerpt.range.context.to_offset(&excerpt.buffer),
+                            range: buffer_start..buffer_end,
+                            new_text,
+                            is_insertion: true,
+                            original_indent_column,
+                            excerpt_id: start_region.excerpt.id,
+                        });
+                }
+            } else {
+                let start_excerpt_range = buffer_start..start_region.buffer_range.end;
+                let end_excerpt_range = end_region.buffer_range.start..buffer_end;
+                if start_region.is_main_buffer {
+                    edited_excerpt_ids.push(start_region.excerpt.id);
+                    buffer_edits
+                        .entry(start_region.buffer.remote_id())
+                        .or_default()
+                        .push(BufferEdit {
+                            range: start_excerpt_range,
+                            new_text: new_text.clone(),
+                            is_insertion: true,
+                            original_indent_column,
+                            excerpt_id: start_region.excerpt.id,
+                        });
+                }
+                if end_region.is_main_buffer {
+                    edited_excerpt_ids.push(end_region.excerpt.id);
+                    buffer_edits
+                        .entry(end_region.buffer.remote_id())
+                        .or_default()
+                        .push(BufferEdit {
+                            range: end_excerpt_range,
                             new_text: new_text.clone(),
                             is_insertion: false,
                             original_indent_column,
+                            excerpt_id: end_region.excerpt.id,
                         });
-                    edited_excerpt_ids.push(excerpt.id);
-                    cursor.next(&());
+                }
+
+                cursor.seek(&range.start);
+                cursor.next_excerpt();
+                while let Some(region) = cursor.region() {
+                    if region.excerpt.id == end_region.excerpt.id {
+                        break;
+                    }
+                    if region.is_main_buffer {
+                        edited_excerpt_ids.push(region.excerpt.id);
+                        buffer_edits
+                            .entry(region.buffer.remote_id())
+                            .or_default()
+                            .push(BufferEdit {
+                                range: region.buffer_range,
+                                new_text: new_text.clone(),
+                                is_insertion: false,
+                                original_indent_column,
+                                excerpt_id: region.excerpt.id,
+                            });
+                    }
+                    cursor.next_excerpt();
                 }
             }
         }
@@ -797,16 +935,6 @@ impl MultiBuffer {
                 return;
             }
 
-            if let Some(buffer) = this.as_singleton() {
-                buffer.update(cx, |buffer, cx| {
-                    buffer.autoindent_ranges(edits.into_iter().map(|e| e.0), cx);
-                });
-                cx.emit(Event::ExcerptsEdited {
-                    ids: this.excerpt_ids(),
-                });
-                return;
-            }
-
             let (buffer_edits, edited_excerpt_ids) =
                 this.convert_edits_to_buffer_edits(edits, &snapshot, &[]);
             drop(snapshot);
@@ -849,20 +977,13 @@ impl MultiBuffer {
         cx: &mut ModelContext<Self>,
     ) -> Point {
         let multibuffer_point = position.to_point(&self.read(cx));
-        if let Some(buffer) = self.as_singleton() {
-            buffer.update(cx, |buffer, cx| {
-                buffer.insert_empty_line(multibuffer_point, space_above, space_below, cx)
-            })
-        } else {
-            let (buffer, buffer_point, _) =
-                self.point_to_buffer_point(multibuffer_point, cx).unwrap();
-            self.start_transaction(cx);
-            let empty_line_start = buffer.update(cx, |buffer, cx| {
-                buffer.insert_empty_line(buffer_point, space_above, space_below, cx)
-            });
-            self.end_transaction(cx);
-            multibuffer_point + (empty_line_start - buffer_point)
-        }
+        let (buffer, buffer_point, _) = self.point_to_buffer_point(multibuffer_point, cx).unwrap();
+        self.start_transaction(cx);
+        let empty_line_start = buffer.update(cx, |buffer, cx| {
+            buffer.insert_empty_line(buffer_point, space_above, space_below, cx)
+        });
+        self.end_transaction(cx);
+        multibuffer_point + (empty_line_start - buffer_point)
     }
 
     pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
@@ -922,13 +1043,6 @@ impl MultiBuffer {
     where
         D: TextDimension + Ord + Sub<D, Output = D>,
     {
-        if let Some(buffer) = self.as_singleton() {
-            return buffer
-                .read(cx)
-                .edited_ranges_for_transaction_id(transaction_id)
-                .collect::<Vec<_>>();
-        }
-
         let Some(transaction) = self.history.transaction(transaction_id) else {
             return Vec::new();
         };
@@ -952,14 +1066,14 @@ impl MultiBuffer {
                             let excerpt_buffer_start =
                                 excerpt.range.context.start.summary::<D>(buffer);
                             let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer);
-                            let excerpt_range = excerpt_buffer_start.clone()..excerpt_buffer_end;
+                            let excerpt_range = excerpt_buffer_start..excerpt_buffer_end;
                             if excerpt_range.contains(&range.start)
                                 && excerpt_range.contains(&range.end)
                             {
                                 let excerpt_start = D::from_text_summary(&cursor.start().text);
 
-                                let mut start = excerpt_start.clone();
-                                start.add_assign(&(range.start - excerpt_buffer_start.clone()));
+                                let mut start = excerpt_start;
+                                start.add_assign(&(range.start - excerpt_buffer_start));
                                 let mut end = excerpt_start;
                                 end.add_assign(&(range.end - excerpt_buffer_start));
 
@@ -972,7 +1086,7 @@ impl MultiBuffer {
             }
         }
 
-        ranges.sort_by_key(|range| range.start.clone());
+        ranges.sort_by_key(|range| range.start);
         ranges
     }
 
@@ -1258,11 +1372,13 @@ impl MultiBuffer {
                     buffer_id: Some(buffer_id),
                     excerpt_id,
                     text_anchor: buffer_snapshot.anchor_after(range.start),
+                    diff_base_anchor: None,
                 };
                 let end = Anchor {
                     buffer_id: Some(buffer_id),
                     excerpt_id,
                     text_anchor: buffer_snapshot.anchor_after(range.end),
+                    diff_base_anchor: None,
                 };
                 start..end
             }))
@@ -1339,11 +1455,13 @@ impl MultiBuffer {
                                 buffer_id: Some(buffer_id),
                                 excerpt_id,
                                 text_anchor: range.start,
+                                diff_base_anchor: None,
                             };
                             let end = Anchor {
                                 buffer_id: Some(buffer_id),
                                 excerpt_id,
                                 text_anchor: range.end,
+                                diff_base_anchor: None,
                             };
                             multi_buffer_ranges.push(start..end);
                         }
@@ -1425,7 +1543,7 @@ impl MultiBuffer {
         let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &());
         prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone();
 
-        let edit_start = new_excerpts.summary().text.len;
+        let edit_start = ExcerptOffset::new(new_excerpts.summary().text.len);
         new_excerpts.update_last(
             |excerpt| {
                 excerpt.has_trailing_newline = true;
@@ -1471,7 +1589,7 @@ impl MultiBuffer {
             new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
         }
 
-        let edit_end = new_excerpts.summary().text.len;
+        let edit_end = ExcerptOffset::new(new_excerpts.summary().text.len);
 
         let suffix = cursor.suffix(&());
         let changed_trailing_excerpt = suffix.is_empty();
@@ -1483,10 +1601,14 @@ impl MultiBuffer {
             snapshot.trailing_excerpt_update_count += 1;
         }
 
-        self.subscriptions.publish_mut([Edit {
-            old: edit_start..edit_start,
-            new: edit_start..edit_end,
-        }]);
+        self.sync_diff_transforms(
+            snapshot,
+            vec![Edit {
+                old: edit_start..edit_start,
+                new: edit_start..edit_end,
+            }],
+            DiffChangeKind::ExcerptsChanged,
+        );
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
             edited_buffer: None,
@@ -1504,17 +1626,22 @@ impl MultiBuffer {
         let ids = self.excerpt_ids();
         self.buffers.borrow_mut().clear();
         let mut snapshot = self.snapshot.borrow_mut();
-        let prev_len = snapshot.len();
+        let start = ExcerptOffset::new(0);
+        let prev_len = ExcerptOffset::new(snapshot.excerpts.summary().text.len);
         snapshot.excerpts = Default::default();
         snapshot.trailing_excerpt_update_count += 1;
         snapshot.is_dirty = false;
         snapshot.has_deleted_file = false;
         snapshot.has_conflict = false;
 
-        self.subscriptions.publish_mut([Edit {
-            old: 0..prev_len,
-            new: 0..0,
-        }]);
+        self.sync_diff_transforms(
+            snapshot,
+            vec![Edit {
+                old: start..prev_len,
+                new: start..start,
+            }],
+            DiffChangeKind::ExcerptsChanged,
+        );
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
             edited_buffer: None,
@@ -1556,24 +1683,39 @@ impl MultiBuffer {
     ) -> Vec<Range<Point>> {
         let snapshot = self.read(cx);
         let buffers = self.buffers.borrow();
-        let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&());
-        buffers
+        let mut excerpts = snapshot
+            .excerpts
+            .cursor::<(Option<&Locator>, ExcerptDimension<Point>)>(&());
+        let mut diff_transforms = snapshot
+            .diff_transforms
+            .cursor::<(ExcerptDimension<Point>, OutputDimension<Point>)>(&());
+        diff_transforms.next(&());
+        let locators = buffers
             .get(&buffer_id)
             .into_iter()
-            .flat_map(|state| &state.excerpts)
-            .filter_map(move |locator| {
-                cursor.seek_forward(&Some(locator), Bias::Left, &());
-                cursor.item().and_then(|excerpt| {
-                    if excerpt.locator == *locator {
-                        let excerpt_start = cursor.start().1;
-                        let excerpt_end = excerpt_start + excerpt.text_summary.lines;
-                        Some(excerpt_start..excerpt_end)
-                    } else {
-                        None
-                    }
-                })
-            })
-            .collect()
+            .flat_map(|state| &state.excerpts);
+        let mut result = Vec::new();
+        for locator in locators {
+            excerpts.seek_forward(&Some(locator), Bias::Left, &());
+            if let Some(excerpt) = excerpts.item() {
+                if excerpt.locator == *locator {
+                    let excerpt_start = excerpts.start().1.clone();
+                    let excerpt_end =
+                        ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines);
+
+                    diff_transforms.seek_forward(&excerpt_start, Bias::Left, &());
+                    let overshoot = excerpt_start.0 - diff_transforms.start().0 .0;
+                    let start = diff_transforms.start().1 .0 + overshoot;
+
+                    diff_transforms.seek_forward(&excerpt_end, Bias::Right, &());
+                    let overshoot = excerpt_end.0 - diff_transforms.start().0 .0;
+                    let end = diff_transforms.start().1 .0 + overshoot;
+
+                    result.push(start..end)
+                }
+            }
+        }
+        result
     }
 
     pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
@@ -1600,12 +1742,12 @@ impl MultiBuffer {
         cx: &AppContext,
     ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
         let snapshot = self.read(cx);
-        let position = position.to_offset(&snapshot);
+        let offset = position.to_offset(&snapshot);
 
-        let mut cursor = snapshot.excerpts.cursor::<usize>(&());
-        cursor.seek(&position, Bias::Right, &());
+        let mut cursor = snapshot.cursor::<usize>();
+        cursor.seek(&offset);
         cursor
-            .item()
+            .excerpt()
             .or_else(|| snapshot.excerpts.last())
             .map(|excerpt| {
                 (
@@ -1626,22 +1768,17 @@ impl MultiBuffer {
         &self,
         point: T,
         cx: &AppContext,
-    ) -> Option<(Model<Buffer>, usize, ExcerptId)> {
+    ) -> Option<(Model<Buffer>, usize)> {
         let snapshot = self.read(cx);
-        let offset = point.to_offset(&snapshot);
-        let mut cursor = snapshot.excerpts.cursor::<usize>(&());
-        cursor.seek(&offset, Bias::Right, &());
-        if cursor.item().is_none() {
-            cursor.prev(&());
-        }
-
-        cursor.item().map(|excerpt| {
-            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
-            let buffer_point = excerpt_start + offset - *cursor.start();
-            let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
-
-            (buffer, buffer_point, excerpt.id)
-        })
+        let (buffer, offset) = snapshot.point_to_buffer_offset(point)?;
+        Some((
+            self.buffers
+                .borrow()
+                .get(&buffer.remote_id())?
+                .buffer
+                .clone(),
+            offset,
+        ))
     }
 
     // If point is at the end of the buffer, the last excerpt is returned
@@ -1652,18 +1789,49 @@ impl MultiBuffer {
     ) -> Option<(Model<Buffer>, Point, ExcerptId)> {
         let snapshot = self.read(cx);
         let point = point.to_point(&snapshot);
-        let mut cursor = snapshot.excerpts.cursor::<Point>(&());
-        cursor.seek(&point, Bias::Right, &());
-        if cursor.item().is_none() {
-            cursor.prev(&());
-        }
+        let mut cursor = snapshot.cursor::<Point>();
+        cursor.seek(&point);
+
+        cursor.region().and_then(|region| {
+            if !region.is_main_buffer {
+                return None;
+            }
+
+            let overshoot = point - region.range.start;
+            let buffer_point = region.buffer_range.start + overshoot;
+            let buffer = self.buffers.borrow()[&region.buffer.remote_id()]
+                .buffer
+                .clone();
+            Some((buffer, buffer_point, region.excerpt.id))
+        })
+    }
 
-        cursor.item().map(|excerpt| {
-            let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
-            let buffer_point = excerpt_start + point - *cursor.start();
-            let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
+    pub fn buffer_point_to_anchor(
+        &self,
+        buffer: &Model<Buffer>,
+        point: Point,
+        cx: &AppContext,
+    ) -> Option<Anchor> {
+        let mut found = None;
+        let snapshot = buffer.read(cx).snapshot();
+        for (excerpt_id, range) in self.excerpts_for_buffer(buffer, cx) {
+            let start = range.context.start.to_point(&snapshot);
+            let end = range.context.end.to_point(&snapshot);
+            if start <= point && point < end {
+                found = Some((snapshot.clip_point(point, Bias::Left), excerpt_id));
+                break;
+            }
+            if point < start {
+                found = Some((start, excerpt_id));
+            }
+            if point > end {
+                found = Some((end, excerpt_id));
+            }
+        }
 
-            (buffer, buffer_point, excerpt.id)
+        found.map(|(point, excerpt_id)| {
+            let text_anchor = snapshot.anchor_after(point);
+            Anchor::in_buffer(excerpt_id, snapshot.remote_id(), text_anchor)
         })
     }
 
@@ -1681,7 +1849,9 @@ impl MultiBuffer {
         let mut buffers = self.buffers.borrow_mut();
         let mut snapshot = self.snapshot.borrow_mut();
         let mut new_excerpts = SumTree::default();
-        let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(&());
+        let mut cursor = snapshot
+            .excerpts
+            .cursor::<(Option<&Locator>, ExcerptOffset)>(&());
         let mut edits = Vec::new();
         let mut excerpt_ids = ids.iter().copied().peekable();
 
@@ -1723,14 +1893,14 @@ impl MultiBuffer {
 
                 // When removing the last excerpt, remove the trailing newline from
                 // the previous excerpt.
-                if cursor.item().is_none() && old_start > 0 {
-                    old_start -= 1;
+                if cursor.item().is_none() && old_start.value > 0 {
+                    old_start.value -= 1;
                     new_excerpts.update_last(|e| e.has_trailing_newline = false, &());
                 }
 
                 // Push an edit for the removal of this run of excerpts.
                 let old_end = cursor.start().1;
-                let new_start = new_excerpts.summary().text.len;
+                let new_start = ExcerptOffset::new(new_excerpts.summary().text.len);
                 edits.push(Edit {
                     old: old_start..old_end,
                     new: new_start..new_start,
@@ -1747,7 +1917,7 @@ impl MultiBuffer {
             snapshot.trailing_excerpt_update_count += 1;
         }
 
-        self.subscriptions.publish_mut(edits);
+        self.sync_diff_transforms(snapshot, edits, DiffChangeKind::ExcerptsChanged);
         cx.emit(Event::Edited {
             singleton_buffer_edited: false,
             edited_buffer: None,

crates/multi_buffer/src/multi_buffer_tests.rs πŸ”—

@@ -1,5 +1,7 @@
 use super::*;
+use git::diff::DiffHunkStatus;
 use gpui::{AppContext, Context, TestAppContext};
+use indoc::indoc;
 use language::{Buffer, Rope};
 use parking_lot::RwLock;
 use rand::prelude::*;
@@ -14,6 +16,22 @@ fn init_logger() {
     }
 }
 
+#[gpui::test]
+fn test_empty_singleton(cx: &mut AppContext) {
+    let buffer = cx.new_model(|cx| Buffer::local("", cx));
+    let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
+    let snapshot = multibuffer.read(cx).snapshot(cx);
+    assert_eq!(snapshot.text(), "");
+    assert_eq!(
+        snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>(),
+        [RowInfo {
+            buffer_row: Some(0),
+            multibuffer_row: Some(MultiBufferRow(0)),
+            diff_status: None
+        }]
+    );
+}
+
 #[gpui::test]
 fn test_singleton(cx: &mut AppContext) {
     let buffer = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx));
@@ -23,22 +41,30 @@ fn test_singleton(cx: &mut AppContext) {
     assert_eq!(snapshot.text(), buffer.read(cx).text());
 
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(0)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(0))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         (0..buffer.read(cx).row_count())
             .map(Some)
             .collect::<Vec<_>>()
     );
+    assert_consistent_line_numbers(&snapshot);
 
     buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx));
     let snapshot = multibuffer.read(cx).snapshot(cx);
 
     assert_eq!(snapshot.text(), buffer.read(cx).text());
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(0)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(0))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         (0..buffer.read(cx).row_count())
             .map(Some)
             .collect::<Vec<_>>()
     );
+    assert_consistent_line_numbers(&snapshot);
 }
 
 #[gpui::test]
@@ -154,28 +180,41 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) {
     let snapshot = multibuffer.read(cx).snapshot(cx);
     assert_eq!(
         snapshot.text(),
-        concat!(
-            "bbbb\n",  // Preserve newlines
-            "ccccc\n", //
-            "ddd\n",   //
-            "eeee\n",  //
-            "jj"       //
-        )
+        indoc!(
+            "
+            bbbb
+            ccccc
+            ddd
+            eeee
+            jj"
+        ),
     );
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(0)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(0))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         [Some(1), Some(2), Some(3), Some(4), Some(3)]
     );
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(2)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(2))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         [Some(3), Some(4), Some(3)]
     );
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(4)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(4))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         [Some(3)]
     );
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(5)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(5))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         []
     );
 
@@ -314,6 +353,312 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) {
     }
 }
 
+#[gpui::test]
+fn test_diff_boundary_anchors(cx: &mut AppContext) {
+    let base_text = "one\ntwo\nthree\n";
+    let text = "one\nthree\n";
+    let buffer = cx.new_model(|cx| Buffer::local(text, cx));
+    let snapshot = buffer.read(cx).snapshot();
+    let change_set = cx.new_model(|cx| {
+        let mut change_set = BufferChangeSet::new(&buffer, cx);
+        change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx);
+        change_set
+    });
+    let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.add_change_set(change_set, cx)
+    });
+
+    let (before, after) = multibuffer.update(cx, |multibuffer, cx| {
+        let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0));
+        let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0));
+        multibuffer.set_all_diff_hunks_expanded(cx);
+        (before, after)
+    });
+    cx.background_executor().run_until_parked();
+
+    let snapshot = multibuffer.read(cx).snapshot(cx);
+    let actual_text = snapshot.text();
+    let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
+    let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default());
+    pretty_assertions::assert_eq!(
+        actual_diff,
+        indoc! {
+            "  one
+             - two
+               three
+             "
+        },
+    );
+
+    multibuffer.update(cx, |multibuffer, cx| {
+        let snapshot = multibuffer.snapshot(cx);
+        assert_eq!(before.to_point(&snapshot), Point::new(1, 0));
+        assert_eq!(after.to_point(&snapshot), Point::new(2, 0));
+        assert_eq!(
+            vec![Point::new(1, 0), Point::new(2, 0),],
+            snapshot.summaries_for_anchors::<Point, _>(&[before, after]),
+        )
+    })
+}
+
+#[gpui::test]
+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_model(|cx| Buffer::local(text, cx));
+    let change_set = cx.new_model(|cx| {
+        let mut change_set = BufferChangeSet::new(&buffer, cx);
+        let snapshot = buffer.read(cx).snapshot();
+        change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx);
+        change_set
+    });
+    let multibuffer = cx.new_model(|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.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
+    });
+
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "  one
+             - two
+             - three
+               four
+             - five
+             - six
+               seven
+             - eight
+            "
+        },
+    );
+
+    assert_eq!(
+        snapshot
+            .diff_hunks_in_range(Point::new(1, 0)..Point::MAX)
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
+            .collect::<Vec<_>>(),
+        vec![1..3, 4..6, 7..8]
+    );
+
+    assert_eq!(
+        snapshot
+            .diff_hunk_before(Point::new(1, 1))
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
+        None,
+    );
+    assert_eq!(
+        snapshot
+            .diff_hunk_before(Point::new(7, 0))
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
+        Some(4..6)
+    );
+    assert_eq!(
+        snapshot
+            .diff_hunk_before(Point::new(4, 0))
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
+        Some(1..3)
+    );
+
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
+    });
+
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+            one
+            four
+            seven
+            "
+        },
+    );
+
+    assert_eq!(
+        snapshot
+            .diff_hunk_before(Point::new(2, 0))
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
+        Some(1..1),
+    );
+    assert_eq!(
+        snapshot
+            .diff_hunk_before(Point::new(4, 0))
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0),
+        Some(2..2)
+    );
+}
+
+#[gpui::test]
+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_model(|cx| Buffer::local(text, cx));
+    let change_set = cx.new_model(|cx| {
+        let mut change_set = BufferChangeSet::new(&buffer, cx);
+        let snapshot = buffer.read(cx).snapshot();
+        change_set.recalculate_diff_sync(base_text.into(), snapshot.text, true, cx);
+        change_set
+    });
+    let multibuffer = cx.new_model(|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.snapshot(cx), multibuffer.subscribe())
+    });
+
+    cx.executor().run_until_parked();
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.set_all_diff_hunks_expanded(cx);
+    });
+
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+              one
+              two
+            + THREE
+              four
+              five
+            - six
+              seven
+            "
+        },
+    );
+
+    // Insert a newline within an insertion hunk
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx);
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+              one
+              two
+            + __
+            + __THREE
+              four
+              five
+            - six
+              seven
+            "
+        },
+    );
+
+    // Delete the newline before a deleted hunk.
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx);
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+              one
+              two
+            + __
+            + __THREE
+              four
+              fiveseven
+            "
+        },
+    );
+
+    multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx));
+    change_set.update(cx, |change_set, cx| {
+        change_set.recalculate_diff_sync(
+            base_text.into(),
+            buffer.read(cx).text_snapshot(),
+            true,
+            cx,
+        );
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+              one
+              two
+            + __
+            + __THREE
+              four
+              five
+            - six
+              seven
+            "
+        },
+    );
+
+    // Cannot (yet) insert at the beginning of a deleted hunk.
+    // (because it would put the newline in the wrong place)
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx);
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+              one
+              two
+            + __
+            + __THREE
+              four
+              five
+            - six
+              seven
+            "
+        },
+    );
+
+    // Replace a range that ends in a deleted hunk.
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx);
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc! {
+            "
+              one
+              two
+            + __
+            + __THREE
+              four
+              fifty-seven
+            "
+        },
+    );
+}
+
 #[gpui::test]
 fn test_excerpt_events(cx: &mut AppContext) {
     let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(10, 3, 'a'), cx));
@@ -633,11 +978,17 @@ fn test_empty_multibuffer(cx: &mut AppContext) {
     let snapshot = multibuffer.read(cx).snapshot(cx);
     assert_eq!(snapshot.text(), "");
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(0)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(0))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         &[Some(0)]
     );
     assert_eq!(
-        snapshot.buffer_rows(MultiBufferRow(1)).collect::<Vec<_>>(),
+        snapshot
+            .row_infos(MultiBufferRow(1))
+            .map(|info| info.buffer_row)
+            .collect::<Vec<_>>(),
         &[]
     );
 }
@@ -851,493 +1202,393 @@ fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) {
     );
 }
 
-#[gpui::test(iterations = 100)]
-fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) {
-    let operations = env::var("OPERATIONS")
-        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-        .unwrap_or(10);
+#[gpui::test]
+fn test_basic_diff_hunks(cx: &mut TestAppContext) {
+    let text = indoc!(
+        "
+        ZERO
+        one
+        TWO
+        three
+        six
+        "
+    );
+    let base_text = indoc!(
+        "
+        one
+        two
+        three
+        four
+        five
+        six
+        "
+    );
 
-    let mut buffers: Vec<Model<Buffer>> = Vec::new();
-    let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
-    let mut excerpt_ids = Vec::<ExcerptId>::new();
-    let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::new();
-    let mut anchors = Vec::new();
-    let mut old_versions = Vec::new();
+    let buffer = cx.new_model(|cx| Buffer::local(text, cx));
+    let change_set =
+        cx.new_model(|cx| BufferChangeSet::new_with_base_text(base_text.to_string(), &buffer, cx));
+    cx.run_until_parked();
 
-    for _ in 0..operations {
-        match rng.gen_range(0..100) {
-            0..=14 if !buffers.is_empty() => {
-                let buffer = buffers.choose(&mut rng).unwrap();
-                buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx));
-            }
-            15..=19 if !expected_excerpts.is_empty() => {
-                multibuffer.update(cx, |multibuffer, cx| {
-                    let ids = multibuffer.excerpt_ids();
-                    let mut excerpts = HashSet::default();
-                    for _ in 0..rng.gen_range(0..ids.len()) {
-                        excerpts.extend(ids.choose(&mut rng).copied());
-                    }
+    let multibuffer = cx.new_model(|cx| {
+        let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
+        multibuffer.add_change_set(change_set.clone(), cx);
+        multibuffer
+    });
 
-                    let line_count = rng.gen_range(0..5);
+    let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
+        (multibuffer.snapshot(cx), multibuffer.subscribe())
+    });
+    assert_eq!(
+        snapshot.text(),
+        indoc!(
+            "
+            ZERO
+            one
+            TWO
+            three
+            six
+            "
+        ),
+    );
 
-                    let excerpt_ixs = excerpts
-                        .iter()
-                        .map(|id| excerpt_ids.iter().position(|i| i == id).unwrap())
-                        .collect::<Vec<_>>();
-                    log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines");
-                    multibuffer.expand_excerpts(
-                        excerpts.iter().cloned(),
-                        line_count,
-                        ExpandExcerptDirection::UpAndDown,
-                        cx,
-                    );
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
+    });
 
-                    if line_count > 0 {
-                        for id in excerpts {
-                            let excerpt_ix = excerpt_ids.iter().position(|&i| i == id).unwrap();
-                            let (buffer, range) = &mut expected_excerpts[excerpt_ix];
-                            let snapshot = buffer.read(cx).snapshot();
-                            let mut point_range = range.to_point(&snapshot);
-                            point_range.start =
-                                Point::new(point_range.start.row.saturating_sub(line_count), 0);
-                            point_range.end = snapshot.clip_point(
-                                Point::new(point_range.end.row + line_count, 0),
-                                Bias::Left,
-                            );
-                            point_range.end.column = snapshot.line_len(point_range.end.row);
-                            *range = snapshot.anchor_before(point_range.start)
-                                ..snapshot.anchor_after(point_range.end);
-                        }
-                    }
-                });
-            }
-            20..=29 if !expected_excerpts.is_empty() => {
-                let mut ids_to_remove = vec![];
-                for _ in 0..rng.gen_range(1..=3) {
-                    if expected_excerpts.is_empty() {
-                        break;
-                    }
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+            + ZERO
+              one
+            - two
+            + TWO
+              three
+            - four
+            - five
+              six
+            "
+        ),
+    );
 
-                    let ix = rng.gen_range(0..expected_excerpts.len());
-                    ids_to_remove.push(excerpt_ids.remove(ix));
-                    let (buffer, range) = expected_excerpts.remove(ix);
-                    let buffer = buffer.read(cx);
-                    log::info!(
-                        "Removing excerpt {}: {:?}",
-                        ix,
-                        buffer
-                            .text_for_range(range.to_offset(buffer))
-                            .collect::<String>(),
-                    );
-                }
-                let snapshot = multibuffer.read(cx).read(cx);
-                ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
-                drop(snapshot);
-                multibuffer.update(cx, |multibuffer, cx| {
-                    multibuffer.remove_excerpts(ids_to_remove, cx)
-                });
-            }
-            30..=39 if !expected_excerpts.is_empty() => {
-                let multibuffer = multibuffer.read(cx).read(cx);
-                let offset =
-                    multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left);
-                let bias = if rng.gen() { Bias::Left } else { Bias::Right };
-                log::info!("Creating anchor at {} with bias {:?}", offset, bias);
-                anchors.push(multibuffer.anchor_at(offset, bias));
-                anchors.sort_by(|a, b| a.cmp(b, &multibuffer));
-            }
-            40..=44 if !anchors.is_empty() => {
-                let multibuffer = multibuffer.read(cx).read(cx);
-                let prev_len = anchors.len();
-                anchors = multibuffer
-                    .refresh_anchors(&anchors)
-                    .into_iter()
-                    .map(|a| a.1)
-                    .collect();
+    assert_eq!(
+        snapshot
+            .row_infos(MultiBufferRow(0))
+            .map(|info| (info.buffer_row, info.diff_status))
+            .collect::<Vec<_>>(),
+        vec![
+            (Some(0), Some(DiffHunkStatus::Added)),
+            (Some(1), None),
+            (Some(1), Some(DiffHunkStatus::Removed)),
+            (Some(2), Some(DiffHunkStatus::Added)),
+            (Some(3), None),
+            (Some(3), Some(DiffHunkStatus::Removed)),
+            (Some(4), Some(DiffHunkStatus::Removed)),
+            (Some(4), None),
+            (Some(5), None)
+        ]
+    );
 
-                // Ensure the newly-refreshed anchors point to a valid excerpt and don't
-                // overshoot its boundaries.
-                assert_eq!(anchors.len(), prev_len);
-                for anchor in &anchors {
-                    if anchor.excerpt_id == ExcerptId::min()
-                        || anchor.excerpt_id == ExcerptId::max()
-                    {
-                        continue;
-                    }
+    assert_chunks_in_ranges(&snapshot);
+    assert_consistent_line_numbers(&snapshot);
+    assert_position_translation(&snapshot);
+    assert_line_indents(&snapshot);
 
-                    let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
-                    assert_eq!(excerpt.id, anchor.excerpt_id);
-                    assert!(excerpt.contains(anchor));
-                }
-            }
-            _ => {
-                let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
-                    let base_text = util::RandomCharIter::new(&mut rng)
-                        .take(25)
-                        .collect::<String>();
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx)
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+            ZERO
+            one
+            TWO
+            three
+            six
+            "
+        ),
+    );
 
-                    buffers.push(cx.new_model(|cx| Buffer::local(base_text, cx)));
-                    buffers.last().unwrap()
-                } else {
-                    buffers.choose(&mut rng).unwrap()
-                };
+    assert_chunks_in_ranges(&snapshot);
+    assert_consistent_line_numbers(&snapshot);
+    assert_position_translation(&snapshot);
+    assert_line_indents(&snapshot);
 
-                let buffer = buffer_handle.read(cx);
-                let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right);
-                let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
-                let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
-                let prev_excerpt_ix = rng.gen_range(0..=expected_excerpts.len());
-                let prev_excerpt_id = excerpt_ids
-                    .get(prev_excerpt_ix)
-                    .cloned()
-                    .unwrap_or_else(ExcerptId::max);
-                let excerpt_ix = (prev_excerpt_ix + 1).min(expected_excerpts.len());
+    // Expand the first diff hunk
+    multibuffer.update(cx, |multibuffer, cx| {
+        let position = multibuffer.read(cx).anchor_before(Point::new(2, 2));
+        multibuffer.expand_diff_hunks(vec![position..position], cx)
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+              ZERO
+              one
+            - two
+            + TWO
+              three
+              six
+            "
+        ),
+    );
 
-                log::info!(
-                    "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
-                    excerpt_ix,
-                    expected_excerpts.len(),
-                    buffer_handle.read(cx).remote_id(),
-                    buffer.text(),
-                    start_ix..end_ix,
-                    &buffer.text()[start_ix..end_ix]
-                );
+    // Expand the second diff hunk
+    multibuffer.update(cx, |multibuffer, cx| {
+        let start = multibuffer.read(cx).anchor_before(Point::new(4, 0));
+        let end = multibuffer.read(cx).anchor_before(Point::new(5, 0));
+        multibuffer.expand_diff_hunks(vec![start..end], cx)
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+              ZERO
+              one
+            - two
+            + TWO
+              three
+            - four
+            - five
+              six
+            "
+        ),
+    );
 
-                let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
-                    multibuffer
-                        .insert_excerpts_after(
-                            prev_excerpt_id,
-                            buffer_handle.clone(),
-                            [ExcerptRange {
-                                context: start_ix..end_ix,
-                                primary: None,
-                            }],
-                            cx,
-                        )
-                        .pop()
-                        .unwrap()
-                });
-
-                excerpt_ids.insert(excerpt_ix, excerpt_id);
-                expected_excerpts.insert(excerpt_ix, (buffer_handle.clone(), anchor_range));
-            }
-        }
-
-        if rng.gen_bool(0.3) {
-            multibuffer.update(cx, |multibuffer, cx| {
-                old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe()));
-            })
-        }
-
-        let snapshot = multibuffer.read(cx).snapshot(cx);
-
-        let mut excerpt_starts = Vec::new();
-        let mut expected_text = String::new();
-        let mut expected_buffer_rows = Vec::new();
-        for (buffer, range) in &expected_excerpts {
-            let buffer = buffer.read(cx);
-            let buffer_range = range.to_offset(buffer);
-
-            excerpt_starts.push(TextSummary::from(expected_text.as_str()));
-            expected_text.extend(buffer.text_for_range(buffer_range.clone()));
-            expected_text.push('\n');
-
-            let buffer_row_range = buffer.offset_to_point(buffer_range.start).row
-                ..=buffer.offset_to_point(buffer_range.end).row;
-            for row in buffer_row_range {
-                expected_buffer_rows.push(Some(row));
-            }
-        }
-        // Remove final trailing newline.
-        if !expected_excerpts.is_empty() {
-            expected_text.pop();
-        }
-
-        // Always report one buffer row
-        if expected_buffer_rows.is_empty() {
-            expected_buffer_rows.push(Some(0));
-        }
+    assert_chunks_in_ranges(&snapshot);
+    assert_consistent_line_numbers(&snapshot);
+    assert_position_translation(&snapshot);
+    assert_line_indents(&snapshot);
 
-        assert_eq!(snapshot.text(), expected_text);
-        log::info!("MultiBuffer text: {:?}", expected_text);
-
-        assert_eq!(
-            snapshot.buffer_rows(MultiBufferRow(0)).collect::<Vec<_>>(),
-            expected_buffer_rows,
-        );
-
-        for _ in 0..5 {
-            let start_row = rng.gen_range(0..=expected_buffer_rows.len());
-            assert_eq!(
-                snapshot
-                    .buffer_rows(MultiBufferRow(start_row as u32))
-                    .collect::<Vec<_>>(),
-                &expected_buffer_rows[start_row..],
-                "buffer_rows({})",
-                start_row
-            );
-        }
-
-        assert_eq!(
-            snapshot.widest_line_number(),
-            expected_buffer_rows.into_iter().flatten().max().unwrap() + 1
+    // Edit the buffer before the first hunk
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit_via_marked_text(
+            indoc!(
+                "
+                ZERO
+                oneΒ« hundred
+                  thousandΒ»
+                TWO
+                three
+                six
+                "
+            ),
+            None,
+            cx,
         );
+    });
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+              ZERO
+              one hundred
+                thousand
+            - two
+            + TWO
+              three
+            - four
+            - five
+              six
+            "
+        ),
+    );
 
-        let mut excerpt_starts = excerpt_starts.into_iter();
-        for (buffer, range) in &expected_excerpts {
-            let buffer = buffer.read(cx);
-            let buffer_id = buffer.remote_id();
-            let buffer_range = range.to_offset(buffer);
-            let buffer_start_point = buffer.offset_to_point(buffer_range.start);
-            let buffer_start_point_utf16 =
-                buffer.text_summary_for_range::<PointUtf16, _>(0..buffer_range.start);
-
-            let excerpt_start = excerpt_starts.next().unwrap();
-            let mut offset = excerpt_start.len;
-            let mut buffer_offset = buffer_range.start;
-            let mut point = excerpt_start.lines;
-            let mut buffer_point = buffer_start_point;
-            let mut point_utf16 = excerpt_start.lines_utf16();
-            let mut buffer_point_utf16 = buffer_start_point_utf16;
-            for ch in buffer
-                .snapshot()
-                .chunks(buffer_range.clone(), false)
-                .flat_map(|c| c.text.chars())
-            {
-                for _ in 0..ch.len_utf8() {
-                    let left_offset = snapshot.clip_offset(offset, Bias::Left);
-                    let right_offset = snapshot.clip_offset(offset, Bias::Right);
-                    let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left);
-                    let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right);
-                    assert_eq!(
-                        left_offset,
-                        excerpt_start.len + (buffer_left_offset - buffer_range.start),
-                        "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}",
-                        offset,
-                        buffer_id,
-                        buffer_offset,
-                    );
-                    assert_eq!(
-                        right_offset,
-                        excerpt_start.len + (buffer_right_offset - buffer_range.start),
-                        "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}",
-                        offset,
-                        buffer_id,
-                        buffer_offset,
-                    );
-
-                    let left_point = snapshot.clip_point(point, Bias::Left);
-                    let right_point = snapshot.clip_point(point, Bias::Right);
-                    let buffer_left_point = buffer.clip_point(buffer_point, Bias::Left);
-                    let buffer_right_point = buffer.clip_point(buffer_point, Bias::Right);
-                    assert_eq!(
-                        left_point,
-                        excerpt_start.lines + (buffer_left_point - buffer_start_point),
-                        "clip_point({:?}, Left). buffer: {:?}, buffer point: {:?}",
-                        point,
-                        buffer_id,
-                        buffer_point,
-                    );
-                    assert_eq!(
-                        right_point,
-                        excerpt_start.lines + (buffer_right_point - buffer_start_point),
-                        "clip_point({:?}, Right). buffer: {:?}, buffer point: {:?}",
-                        point,
-                        buffer_id,
-                        buffer_point,
-                    );
-
-                    assert_eq!(
-                        snapshot.point_to_offset(left_point),
-                        left_offset,
-                        "point_to_offset({:?})",
-                        left_point,
-                    );
-                    assert_eq!(
-                        snapshot.offset_to_point(left_offset),
-                        left_point,
-                        "offset_to_point({:?})",
-                        left_offset,
-                    );
-
-                    offset += 1;
-                    buffer_offset += 1;
-                    if ch == '\n' {
-                        point += Point::new(1, 0);
-                        buffer_point += Point::new(1, 0);
-                    } else {
-                        point += Point::new(0, 1);
-                        buffer_point += Point::new(0, 1);
-                    }
-                }
-
-                for _ in 0..ch.len_utf16() {
-                    let left_point_utf16 =
-                        snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left);
-                    let right_point_utf16 =
-                        snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right);
-                    let buffer_left_point_utf16 =
-                        buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left);
-                    let buffer_right_point_utf16 =
-                        buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right);
-                    assert_eq!(
-                        left_point_utf16,
-                        excerpt_start.lines_utf16()
-                            + (buffer_left_point_utf16 - buffer_start_point_utf16),
-                        "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}",
-                        point_utf16,
-                        buffer_id,
-                        buffer_point_utf16,
-                    );
-                    assert_eq!(
-                        right_point_utf16,
-                        excerpt_start.lines_utf16()
-                            + (buffer_right_point_utf16 - buffer_start_point_utf16),
-                        "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}",
-                        point_utf16,
-                        buffer_id,
-                        buffer_point_utf16,
-                    );
-
-                    if ch == '\n' {
-                        point_utf16 += PointUtf16::new(1, 0);
-                        buffer_point_utf16 += PointUtf16::new(1, 0);
-                    } else {
-                        point_utf16 += PointUtf16::new(0, 1);
-                        buffer_point_utf16 += PointUtf16::new(0, 1);
-                    }
-                }
-            }
-        }
-
-        for (row, line) in expected_text.split('\n').enumerate() {
-            assert_eq!(
-                snapshot.line_len(MultiBufferRow(row as u32)),
-                line.len() as u32,
-                "line_len({}).",
-                row
-            );
-        }
+    assert_chunks_in_ranges(&snapshot);
+    assert_consistent_line_numbers(&snapshot);
+    assert_position_translation(&snapshot);
+    assert_line_indents(&snapshot);
 
-        let text_rope = Rope::from(expected_text.as_str());
-        for _ in 0..10 {
-            let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
-            let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
+    // Recalculate the diff, changing the first diff hunk.
+    let _ = change_set.update(cx, |change_set, cx| {
+        change_set.recalculate_diff(buffer.read(cx).text_snapshot(), cx)
+    });
+    cx.run_until_parked();
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+              ZERO
+              one hundred
+                thousand
+              TWO
+              three
+            - four
+            - five
+              six
+            "
+        ),
+    );
 
-            let text_for_range = snapshot
-                .text_for_range(start_ix..end_ix)
-                .collect::<String>();
-            assert_eq!(
-                text_for_range,
-                &expected_text[start_ix..end_ix],
-                "incorrect text for range {:?}",
-                start_ix..end_ix
-            );
+    assert_eq!(
+        snapshot
+            .diff_hunks_in_range(0..snapshot.len())
+            .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0)
+            .collect::<Vec<_>>(),
+        &[0..4, 5..7]
+    );
+}
 
-            let snapshot = multibuffer.read(cx).snapshot(cx);
-            let excerpted_buffer_ranges = snapshot.range_to_buffer_ranges(start_ix..end_ix);
-            let excerpted_buffers_text = excerpted_buffer_ranges
-                .iter()
-                .map(|(excerpt, buffer_range)| {
-                    excerpt
-                        .buffer()
-                        .text_for_range(buffer_range.clone())
-                        .collect::<String>()
-                })
-                .collect::<Vec<_>>()
-                .join("\n");
-            assert_eq!(excerpted_buffers_text, text_for_range);
-            if !expected_excerpts.is_empty() {
-                assert!(!excerpted_buffer_ranges.is_empty());
-            }
+#[gpui::test]
+fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
+    let text = indoc!(
+        "
+        one
+        TWO
+        THREE
+        four
+        FIVE
+        six
+        "
+    );
+    let base_text = indoc!(
+        "
+        one
+        four
+        six
+        "
+    );
 
-            let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]);
-            assert_eq!(
-                snapshot.text_summary_for_range::<TextSummary, _>(start_ix..end_ix),
-                expected_summary,
-                "incorrect summary for range {:?}",
-                start_ix..end_ix
-            );
-        }
+    let buffer = cx.new_model(|cx| Buffer::local(text, cx));
+    let change_set =
+        cx.new_model(|cx| BufferChangeSet::new_with_base_text(base_text.to_string(), &buffer, cx));
+    cx.run_until_parked();
 
-        // Anchor resolution
-        let summaries = snapshot.summaries_for_anchors::<usize, _>(&anchors);
-        assert_eq!(anchors.len(), summaries.len());
-        for (anchor, resolved_offset) in anchors.iter().zip(summaries) {
-            assert!(resolved_offset <= snapshot.len());
-            assert_eq!(
-                snapshot.summary_for_anchor::<usize>(anchor),
-                resolved_offset
-            );
-        }
+    let multibuffer = cx.new_model(|cx| {
+        let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
+        multibuffer.add_change_set(change_set.clone(), cx);
+        multibuffer
+    });
 
-        for _ in 0..10 {
-            let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
-            assert_eq!(
-                snapshot.reversed_chars_at(end_ix).collect::<String>(),
-                expected_text[..end_ix].chars().rev().collect::<String>(),
-            );
-        }
+    let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
+        (multibuffer.snapshot(cx), multibuffer.subscribe())
+    });
 
-        for _ in 0..10 {
-            let end_ix = rng.gen_range(0..=text_rope.len());
-            let start_ix = rng.gen_range(0..=end_ix);
-            assert_eq!(
-                snapshot
-                    .bytes_in_range(start_ix..end_ix)
-                    .flatten()
-                    .copied()
-                    .collect::<Vec<_>>(),
-                expected_text.as_bytes()[start_ix..end_ix].to_vec(),
-                "bytes_in_range({:?})",
-                start_ix..end_ix,
-            );
-        }
-    }
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
+    });
 
-    let snapshot = multibuffer.read(cx).snapshot(cx);
-    for (old_snapshot, subscription) in old_versions {
-        let edits = subscription.consume().into_inner();
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+              one
+            + TWO
+            + THREE
+              four
+            + FIVE
+              six
+            "
+        ),
+    );
 
-        log::info!(
-            "applying subscription edits to old text: {:?}: {:?}",
-            old_snapshot.text(),
-            edits,
+    // Regression test: expanding diff hunks that are already expanded should not change anything.
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.expand_diff_hunks(
+            vec![
+                snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)),
+            ],
+            cx,
         );
+    });
 
-        let mut text = old_snapshot.text();
-        for edit in edits {
-            let new_text: String = snapshot.text_for_range(edit.new.clone()).collect();
-            text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text);
-        }
-        assert_eq!(text.to_string(), snapshot.text());
-    }
+    assert_new_snapshot(
+        &multibuffer,
+        &mut snapshot,
+        &mut subscription,
+        cx,
+        indoc!(
+            "
+              one
+            + TWO
+            + THREE
+              four
+            + FIVE
+              six
+            "
+        ),
+    );
 }
 
 #[gpui::test]
-fn test_history(cx: &mut AppContext) {
-    let test_settings = SettingsStore::test(cx);
-    cx.set_global(test_settings);
-    let group_interval: Duration = Duration::from_millis(1);
-    let buffer_1 = cx.new_model(|cx| {
-        let mut buf = Buffer::local("1234", cx);
-        buf.set_group_interval(group_interval);
-        buf
-    });
-    let buffer_2 = cx.new_model(|cx| {
-        let mut buf = Buffer::local("5678", cx);
-        buf.set_group_interval(group_interval);
-        buf
+fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
+    let base_text_1 = indoc!(
+        "
+        one
+        two
+            three
+        four
+        five
+        six
+        "
+    );
+    let text_1 = indoc!(
+        "
+        ZERO
+        one
+        TWO
+            three
+        six
+        "
+    );
+    let base_text_2 = indoc!(
+        "
+        seven
+          eight
+        nine
+        ten
+        eleven
+        twelve
+        "
+    );
+    let text_2 = indoc!(
+        "
+          eight
+        nine
+        eleven
+        THIRTEEN
+        FOURTEEN
+        "
+    );
+
+    let buffer_1 = cx.new_model(|cx| Buffer::local(text_1, cx));
+    let buffer_2 = cx.new_model(|cx| Buffer::local(text_2, cx));
+    let change_set_1 = cx.new_model(|cx| {
+        BufferChangeSet::new_with_base_text(base_text_1.to_string(), &buffer_1, cx)
     });
-    let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
-    multibuffer.update(cx, |this, _| {
-        this.history.group_interval = group_interval;
+    let change_set_2 = cx.new_model(|cx| {
+        BufferChangeSet::new_with_base_text(base_text_2.to_string(), &buffer_2, cx)
     });
-    multibuffer.update(cx, |multibuffer, cx| {
+    cx.run_until_parked();
+
+    let multibuffer = cx.new_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
         multibuffer.push_excerpts(
             buffer_1.clone(),
             [ExcerptRange {
-                context: 0..buffer_1.read(cx).len(),
+                context: text::Anchor::MIN..text::Anchor::MAX,
                 primary: None,
             }],
             cx,

crates/multi_buffer/src/position.rs πŸ”—

@@ -0,0 +1,264 @@
+use std::{
+    fmt::{Debug, Display},
+    marker::PhantomData,
+    ops::{Add, AddAssign, Sub, SubAssign},
+};
+use text::Point;
+
+#[repr(transparent)]
+pub struct TypedOffset<T> {
+    pub value: usize,
+    _marker: PhantomData<T>,
+}
+
+#[repr(transparent)]
+pub struct TypedPoint<T> {
+    pub value: Point,
+    _marker: PhantomData<T>,
+}
+
+#[repr(transparent)]
+pub struct TypedRow<T> {
+    pub value: u32,
+    _marker: PhantomData<T>,
+}
+
+impl<T> TypedOffset<T> {
+    pub fn new(offset: usize) -> Self {
+        Self {
+            value: offset,
+            _marker: PhantomData,
+        }
+    }
+
+    pub fn saturating_sub(self, n: TypedOffset<T>) -> Self {
+        Self {
+            value: self.value.saturating_sub(n.value),
+            _marker: PhantomData,
+        }
+    }
+
+    pub fn zero() -> Self {
+        Self::new(0)
+    }
+
+    pub fn is_zero(&self) -> bool {
+        self.value == 0
+    }
+}
+
+impl<T> TypedPoint<T> {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self {
+            value: Point::new(row, column),
+            _marker: PhantomData,
+        }
+    }
+
+    pub fn wrap(point: Point) -> Self {
+        Self {
+            value: point,
+            _marker: PhantomData,
+        }
+    }
+
+    pub fn row(&self) -> u32 {
+        self.value.row
+    }
+
+    pub fn column(&self) -> u32 {
+        self.value.column
+    }
+
+    pub fn zero() -> Self {
+        Self::wrap(Point::zero())
+    }
+
+    pub fn is_zero(&self) -> bool {
+        self.value.is_zero()
+    }
+}
+
+impl<T> TypedRow<T> {
+    pub fn new(row: u32) -> Self {
+        Self {
+            value: row,
+            _marker: PhantomData,
+        }
+    }
+}
+
+impl<T> Copy for TypedOffset<T> {}
+impl<T> Copy for TypedPoint<T> {}
+impl<T> Copy for TypedRow<T> {}
+
+impl<T> Clone for TypedOffset<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+impl<T> Clone for TypedPoint<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+impl<T> Clone for TypedRow<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+impl<T> Default for TypedOffset<T> {
+    fn default() -> Self {
+        Self::new(0)
+    }
+}
+impl<T> Default for TypedPoint<T> {
+    fn default() -> Self {
+        Self::wrap(Point::default())
+    }
+}
+impl<T> Default for TypedRow<T> {
+    fn default() -> Self {
+        Self::new(0)
+    }
+}
+
+impl<T> PartialOrd for TypedOffset<T> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.value.cmp(&other.value))
+    }
+}
+impl<T> PartialOrd for TypedPoint<T> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.value.cmp(&other.value))
+    }
+}
+impl<T> PartialOrd for TypedRow<T> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.value.cmp(&other.value))
+    }
+}
+
+impl<T> Ord for TypedOffset<T> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.value.cmp(&other.value)
+    }
+}
+impl<T> Ord for TypedPoint<T> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.value.cmp(&other.value)
+    }
+}
+impl<T> Ord for TypedRow<T> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.value.cmp(&other.value)
+    }
+}
+
+impl<T> PartialEq for TypedOffset<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.value == other.value
+    }
+}
+impl<T> PartialEq for TypedPoint<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.value == other.value
+    }
+}
+impl<T> PartialEq for TypedRow<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.value == other.value
+    }
+}
+
+impl<T> Eq for TypedOffset<T> {}
+impl<T> Eq for TypedPoint<T> {}
+impl<T> Eq for TypedRow<T> {}
+
+impl<T> Debug for TypedOffset<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}Offset({})", type_name::<T>(), self.value)
+    }
+}
+impl<T> Debug for TypedPoint<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}Point({}, {})",
+            type_name::<T>(),
+            self.value.row,
+            self.value.column
+        )
+    }
+}
+impl<T> Debug for TypedRow<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}Row({})", type_name::<T>(), self.value)
+    }
+}
+
+impl<T> Display for TypedOffset<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(&self.value, f)
+    }
+}
+impl<T> Display for TypedRow<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(&self.value, f)
+    }
+}
+
+fn type_name<T>() -> &'static str {
+    std::any::type_name::<T>().split("::").last().unwrap()
+}
+
+impl<T> Add<TypedOffset<T>> for TypedOffset<T> {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self {
+        TypedOffset::new(self.value + other.value)
+    }
+}
+impl<T> Add<TypedPoint<T>> for TypedPoint<T> {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self {
+        TypedPoint::wrap(self.value + other.value)
+    }
+}
+
+impl<T> Sub<TypedOffset<T>> for TypedOffset<T> {
+    type Output = Self;
+    fn sub(self, other: Self) -> Self {
+        TypedOffset::new(self.value - other.value)
+    }
+}
+impl<T> Sub<TypedPoint<T>> for TypedPoint<T> {
+    type Output = Self;
+    fn sub(self, other: Self) -> Self {
+        TypedPoint::wrap(self.value - other.value)
+    }
+}
+
+impl<T> AddAssign<TypedOffset<T>> for TypedOffset<T> {
+    fn add_assign(&mut self, other: Self) {
+        self.value += other.value;
+    }
+}
+impl<T> AddAssign<TypedPoint<T>> for TypedPoint<T> {
+    fn add_assign(&mut self, other: Self) {
+        self.value += other.value;
+    }
+}
+
+impl<T> SubAssign<Self> for TypedOffset<T> {
+    fn sub_assign(&mut self, other: Self) {
+        self.value -= other.value;
+    }
+}
+impl<T> SubAssign<Self> for TypedRow<T> {
+    fn sub_assign(&mut self, other: Self) {
+        self.value -= other.value;
+    }
+}

crates/outline_panel/src/outline_panel.rs πŸ”—

@@ -1042,7 +1042,7 @@ impl OutlinePanel {
                         .show_excerpt_controls();
                     let expand_excerpt_control_height = 1.0;
                     if let Some(buffer_id) = scroll_to_buffer {
-                        let current_folded = active_editor.read(cx).buffer_folded(buffer_id, cx);
+                        let current_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
                         if current_folded {
                             if show_excerpt_controls {
                                 let previous_buffer_id = self
@@ -1059,7 +1059,9 @@ impl OutlinePanel {
                                     .skip_while(|id| *id != buffer_id)
                                     .nth(1);
                                 if let Some(previous_buffer_id) = previous_buffer_id {
-                                    if !active_editor.read(cx).buffer_folded(previous_buffer_id, cx)
+                                    if !active_editor
+                                        .read(cx)
+                                        .is_buffer_folded(previous_buffer_id, cx)
                                     {
                                         offset.y += expand_excerpt_control_height;
                                     }
@@ -1418,7 +1420,7 @@ impl OutlinePanel {
             };
 
             active_editor.update(cx, |editor, cx| {
-                buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx));
+                buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
             });
             self.select_entry(selected_entry, true, cx);
             if buffers_to_unfold.is_empty() {
@@ -1504,7 +1506,7 @@ impl OutlinePanel {
 
         if collapsed {
             active_editor.update(cx, |editor, cx| {
-                buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx));
+                buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
             });
             self.select_entry(selected_entry, true, cx);
             if buffers_to_fold.is_empty() {
@@ -1569,7 +1571,7 @@ impl OutlinePanel {
         self.collapsed_entries
             .retain(|entry| !expanded_entries.contains(entry));
         active_editor.update(cx, |editor, cx| {
-            buffers_to_unfold.retain(|buffer_id| editor.buffer_folded(*buffer_id, cx));
+            buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx));
         });
         if buffers_to_unfold.is_empty() {
             self.update_cached_entries(None, cx);
@@ -1617,7 +1619,7 @@ impl OutlinePanel {
         self.collapsed_entries.extend(new_entries);
 
         active_editor.update(cx, |editor, cx| {
-            buffers_to_fold.retain(|buffer_id| !editor.buffer_folded(*buffer_id, cx));
+            buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx));
         });
         if buffers_to_fold.is_empty() {
             self.update_cached_entries(None, cx);
@@ -1707,7 +1709,7 @@ impl OutlinePanel {
 
         active_editor.update(cx, |editor, cx| {
             buffers_to_toggle.retain(|buffer_id| {
-                let folded = editor.buffer_folded(*buffer_id, cx);
+                let folded = editor.is_buffer_folded(*buffer_id, cx);
                 if fold {
                     !folded
                 } else {
@@ -2471,7 +2473,7 @@ impl OutlinePanel {
                         let worktree = file.map(|file| file.worktree.read(cx).snapshot());
                         let is_new = new_entries.contains(&excerpt_id)
                             || !outline_panel.excerpts.contains_key(&buffer_id);
-                        let is_folded = active_editor.read(cx).buffer_folded(buffer_id, cx);
+                        let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
                         buffer_excerpts
                             .entry(buffer_id)
                             .or_insert_with(|| (is_new, is_folded, Vec::new(), entry_id, worktree))
@@ -2875,7 +2877,7 @@ impl OutlinePanel {
             .excerpt_containing(selection, cx)?;
         let buffer_id = buffer.read(cx).remote_id();
 
-        if editor.read(cx).buffer_folded(buffer_id, cx) {
+        if editor.read(cx).is_buffer_folded(buffer_id, cx) {
             return self
                 .fs_entries
                 .iter()
@@ -3593,7 +3595,7 @@ impl OutlinePanel {
                                     None
                                 };
                             if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
-                                if !active_editor.read(cx).buffer_folded(buffer_id, cx) {
+                                if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
                                     outline_panel.add_excerpt_entries(
                                         &mut generation_state,
                                         buffer_id,
@@ -4004,12 +4006,12 @@ impl OutlinePanel {
             .filter(|(match_range, _)| {
                 let editor = active_editor.read(cx);
                 if let Some(buffer_id) = match_range.start.buffer_id {
-                    if editor.buffer_folded(buffer_id, cx) {
+                    if editor.is_buffer_folded(buffer_id, cx) {
                         return false;
                     }
                 }
                 if let Some(buffer_id) = match_range.start.buffer_id {
-                    if editor.buffer_folded(buffer_id, cx) {
+                    if editor.is_buffer_folded(buffer_id, cx) {
                         return false;
                     }
                 }
@@ -4883,7 +4885,7 @@ fn subscribe_for_editor_events(
                                     }
                                 })
                                 .map(|buffer_id| {
-                                    if editor.read(cx).buffer_folded(*buffer_id, cx) {
+                                    if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
                                         latest_folded_buffer_id = Some(*buffer_id);
                                         false
                                     } else {

crates/project/src/buffer_store.rs πŸ”—

@@ -21,7 +21,7 @@ use language::{
         deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
         split_operations,
     },
-    Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation,
+    Buffer, BufferEvent, Capability, DiskState, File as _, Language, LanguageRegistry, Operation,
 };
 use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
 use serde::Deserialize;
@@ -60,14 +60,14 @@ struct SharedBuffer {
     lsp_handle: Option<OpenLspBufferHandle>,
 }
 
-#[derive(Debug)]
 pub struct BufferChangeSet {
     pub buffer_id: BufferId,
-    pub base_text: Option<Model<Buffer>>,
+    pub base_text: Option<language::BufferSnapshot>,
+    pub language: Option<Arc<Language>>,
     pub diff_to_buffer: git::diff::BufferDiff,
     pub recalculate_diff_task: Option<Task<Result<()>>>,
     pub diff_updated_futures: Vec<oneshot::Sender<()>>,
-    pub base_text_version: usize,
+    pub language_registry: Option<Arc<LanguageRegistry>>,
 }
 
 enum BufferStoreState {
@@ -1080,9 +1080,9 @@ impl BufferStore {
             Ok(text) => text,
         };
 
-        let change_set = buffer.update(&mut cx, |buffer, cx| {
-            cx.new_model(|_| BufferChangeSet::new(buffer))
-        })?;
+        let change_set = cx
+            .new_model(|cx| BufferChangeSet::new(&buffer, cx))
+            .unwrap();
 
         if let Some(text) = text {
             change_set
@@ -1976,11 +1976,8 @@ impl BufferStore {
                 shared.unstaged_changes = Some(change_set.clone());
             }
         })?;
-        let staged_text = change_set.read_with(&cx, |change_set, cx| {
-            change_set
-                .base_text
-                .as_ref()
-                .map(|buffer| buffer.read(cx).text())
+        let staged_text = change_set.read_with(&cx, |change_set, _| {
+            change_set.base_text.as_ref().map(|buffer| buffer.text())
         })?;
         Ok(proto::GetStagedTextResponse { staged_text })
     }
@@ -2225,25 +2222,51 @@ impl BufferStore {
 }
 
 impl BufferChangeSet {
-    pub fn new(buffer: &text::BufferSnapshot) -> Self {
+    pub fn new(buffer: &Model<Buffer>, cx: &mut ModelContext<Self>) -> Self {
+        cx.subscribe(buffer, |this, buffer, event, cx| match event {
+            BufferEvent::LanguageChanged => {
+                this.language = buffer.read(cx).language().cloned();
+                if let Some(base_text) = &this.base_text {
+                    let snapshot = language::Buffer::build_snapshot(
+                        base_text.as_rope().clone(),
+                        this.language.clone(),
+                        this.language_registry.clone(),
+                        cx,
+                    );
+                    this.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
+                        let base_text = cx.background_executor().spawn(snapshot).await;
+                        this.update(&mut cx, |this, cx| {
+                            this.base_text = Some(base_text);
+                            cx.notify();
+                        })
+                    }));
+                }
+            }
+            _ => {}
+        })
+        .detach();
+
+        let buffer = buffer.read(cx);
+
         Self {
             buffer_id: buffer.remote_id(),
             base_text: None,
             diff_to_buffer: git::diff::BufferDiff::new(buffer),
             recalculate_diff_task: None,
             diff_updated_futures: Vec::new(),
-            base_text_version: 0,
+            language: buffer.language().cloned(),
+            language_registry: buffer.language_registry(),
         }
     }
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn new_with_base_text(
         base_text: String,
-        buffer: text::BufferSnapshot,
+        buffer: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let mut this = Self::new(&buffer);
-        let _ = this.set_base_text(base_text, buffer, cx);
+        let mut this = Self::new(&buffer, cx);
+        let _ = this.set_base_text(base_text, buffer.read(cx).text_snapshot(), cx);
         this
     }
 
@@ -2266,8 +2289,8 @@ impl BufferChangeSet {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn base_text_string(&self, cx: &AppContext) -> Option<String> {
-        self.base_text.as_ref().map(|buffer| buffer.read(cx).text())
+    pub fn base_text_string(&self) -> Option<String> {
+        self.base_text.as_ref().map(|buffer| buffer.text())
     }
 
     pub fn set_base_text(
@@ -2289,7 +2312,6 @@ impl BufferChangeSet {
             self.base_text = None;
             self.diff_to_buffer = BufferDiff::new(&buffer_snapshot);
             self.recalculate_diff_task.take();
-            self.base_text_version += 1;
             cx.notify();
         }
     }
@@ -2300,7 +2322,7 @@ impl BufferChangeSet {
         cx: &mut ModelContext<Self>,
     ) -> oneshot::Receiver<()> {
         if let Some(base_text) = self.base_text.clone() {
-            self.recalculate_diff_internal(base_text.read(cx).text(), buffer_snapshot, false, cx)
+            self.recalculate_diff_internal(base_text.text(), buffer_snapshot, false, cx)
         } else {
             oneshot::channel().1
         }
@@ -2316,19 +2338,30 @@ impl BufferChangeSet {
         let (tx, rx) = oneshot::channel();
         self.diff_updated_futures.push(tx);
         self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
-            let (base_text, diff) = cx
+            let new_base_text = if base_text_changed {
+                let base_text_rope: Rope = base_text.as_str().into();
+                let snapshot = this.update(&mut cx, |this, cx| {
+                    language::Buffer::build_snapshot(
+                        base_text_rope,
+                        this.language.clone(),
+                        this.language_registry.clone(),
+                        cx,
+                    )
+                })?;
+                Some(cx.background_executor().spawn(snapshot).await)
+            } else {
+                None
+            };
+            let diff = cx
                 .background_executor()
-                .spawn(async move {
-                    let diff = BufferDiff::build(&base_text, &buffer_snapshot).await;
-                    (base_text, diff)
+                .spawn({
+                    let buffer_snapshot = buffer_snapshot.clone();
+                    async move { BufferDiff::build(&base_text, &buffer_snapshot) }
                 })
                 .await;
             this.update(&mut cx, |this, cx| {
-                if base_text_changed {
-                    this.base_text_version += 1;
-                    this.base_text = Some(cx.new_model(|cx| {
-                        Buffer::local_normalized(Rope::from(base_text), LineEnding::default(), cx)
-                    }));
+                if let Some(new_base_text) = new_base_text {
+                    this.base_text = Some(new_base_text)
                 }
                 this.diff_to_buffer = diff;
                 this.recalculate_diff_task.take();
@@ -2341,6 +2374,33 @@ impl BufferChangeSet {
         }));
         rx
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn recalculate_diff_sync(
+        &mut self,
+        mut base_text: String,
+        buffer_snapshot: text::BufferSnapshot,
+        base_text_changed: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        LineEnding::normalize(&mut base_text);
+        let diff = BufferDiff::build(&base_text, &buffer_snapshot);
+        if base_text_changed {
+            self.base_text = Some(
+                cx.background_executor()
+                    .clone()
+                    .block(Buffer::build_snapshot(
+                        base_text.into(),
+                        self.language.clone(),
+                        self.language_registry.clone(),
+                        cx,
+                    )),
+            );
+        }
+        self.diff_to_buffer = diff;
+        self.recalculate_diff_task.take();
+        cx.notify();
+    }
 }
 
 impl OpenBuffer {

crates/project/src/lsp_store.rs πŸ”—

@@ -1851,14 +1851,11 @@ impl LocalLspStore {
 
         let edits_since_save = std::cell::LazyCell::new(|| {
             let saved_version = buffer.read(cx).saved_version();
-            Patch::new(
-                snapshot
-                    .edits_since::<Unclipped<PointUtf16>>(saved_version)
-                    .collect(),
-            )
+            Patch::new(snapshot.edits_since::<PointUtf16>(saved_version).collect())
         });
 
         let mut sanitized_diagnostics = Vec::new();
+
         for entry in diagnostics {
             let start;
             let end;
@@ -1866,8 +1863,8 @@ impl LocalLspStore {
                 // Some diagnostics are based on files on disk instead of buffers'
                 // current contents. Adjust these diagnostics' ranges to reflect
                 // any unsaved edits.
-                start = (*edits_since_save).old_to_new(entry.range.start);
-                end = (*edits_since_save).old_to_new(entry.range.end);
+                start = Unclipped((*edits_since_save).old_to_new(entry.range.start.0));
+                end = Unclipped((*edits_since_save).old_to_new(entry.range.end.0));
             } else {
                 start = entry.range.start;
                 end = entry.range.end;

crates/project/src/project_tests.rs πŸ”—

@@ -5651,7 +5651,7 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
         assert_hunks(
             unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &unstaged_changes.base_text.as_ref().unwrap().read(cx).text(),
+            &unstaged_changes.base_text.as_ref().unwrap().text(),
             &[
                 (0..1, "", "// print goodbye\n"),
                 (
@@ -5681,7 +5681,7 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
         assert_hunks(
             unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
             &snapshot,
-            &unstaged_changes.base_text.as_ref().unwrap().read(cx).text(),
+            &unstaged_changes.base_text.as_ref().unwrap().text(),
             &[(2..3, "", "    println!(\"goodbye world\");\n")],
         );
     });

crates/project/src/task_store.rs πŸ”—

@@ -12,7 +12,7 @@ use language::{
 use rpc::{proto, AnyProtoClient, TypedEnvelope};
 use settings::{watch_config_file, SettingsLocation};
 use task::{TaskContext, TaskVariables, VariableName};
-use text::BufferId;
+use text::{BufferId, OffsetRangeExt};
 use util::ResultExt;
 
 use crate::{
@@ -125,12 +125,10 @@ impl TaskStore {
                         .filter_map(|(k, v)| Some((k.parse().log_err()?, v))),
                 );
 
-                for range in location
-                    .buffer
-                    .read(cx)
-                    .snapshot()
-                    .runnable_ranges(location.range.clone())
-                {
+                let snapshot = location.buffer.read(cx).snapshot();
+                let range = location.range.to_offset(&snapshot);
+
+                for range in snapshot.runnable_ranges(range) {
                     for (capture_name, value) in range.extra_captures {
                         variables.insert(VariableName::Custom(capture_name.into()), value);
                     }

crates/remote_server/src/remote_editing_tests.rs πŸ”—

@@ -86,9 +86,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
         .await
         .unwrap();
 
-    change_set.update(cx, |change_set, cx| {
+    change_set.update(cx, |change_set, _| {
         assert_eq!(
-            change_set.base_text_string(cx).unwrap(),
+            change_set.base_text_string().unwrap(),
             "fn one() -> usize { 0 }"
         );
     });
@@ -150,9 +150,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
         &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
     );
     cx.executor().run_until_parked();
-    change_set.update(cx, |change_set, cx| {
+    change_set.update(cx, |change_set, _| {
         assert_eq!(
-            change_set.base_text_string(cx).unwrap(),
+            change_set.base_text_string().unwrap(),
             "fn one() -> usize { 100 }"
         );
     });

crates/rope/src/rope.rs πŸ”—

@@ -4,16 +4,17 @@ mod point;
 mod point_utf16;
 mod unclipped;
 
-use chunk::{Chunk, ChunkSlice};
+use chunk::Chunk;
 use rayon::iter::{IntoParallelIterator, ParallelIterator as _};
 use smallvec::SmallVec;
 use std::{
     cmp, fmt, io, mem,
-    ops::{AddAssign, Range},
+    ops::{self, AddAssign, Range},
     str,
 };
 use sum_tree::{Bias, Dimension, SumTree};
 
+pub use chunk::ChunkSlice;
 pub use offset_utf16::OffsetUtf16;
 pub use point::Point;
 pub use point_utf16::PointUtf16;
@@ -221,7 +222,7 @@ impl Rope {
     }
 
     pub fn summary(&self) -> TextSummary {
-        self.chunks.summary().text.clone()
+        self.chunks.summary().text
     }
 
     pub fn len(&self) -> usize {
@@ -962,7 +963,7 @@ impl sum_tree::Summary for ChunkSummary {
 }
 
 /// Summary of a string of text.
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
 pub struct TextSummary {
     /// Length in UTF-8
     pub len: usize,
@@ -989,6 +990,27 @@ impl TextSummary {
             column: self.last_line_len_utf16,
         }
     }
+
+    pub fn newline() -> Self {
+        Self {
+            len: 1,
+            len_utf16: OffsetUtf16(1),
+            first_line_chars: 0,
+            last_line_chars: 0,
+            last_line_len_utf16: 0,
+            lines: Point::new(1, 0),
+            longest_row: 0,
+            longest_row_chars: 0,
+        }
+    }
+
+    pub fn add_newline(&mut self) {
+        self.len += 1;
+        self.len_utf16 += OffsetUtf16(self.len_utf16.0 + 1);
+        self.last_line_chars = 0;
+        self.last_line_len_utf16 = 0;
+        self.lines += Point::new(1, 0);
+    }
 }
 
 impl<'a> From<&'a str> for TextSummary {
@@ -1048,7 +1070,7 @@ impl sum_tree::Summary for TextSummary {
     }
 }
 
-impl std::ops::Add<Self> for TextSummary {
+impl ops::Add<Self> for TextSummary {
     type Output = Self;
 
     fn add(mut self, rhs: Self) -> Self::Output {
@@ -1057,7 +1079,7 @@ impl std::ops::Add<Self> for TextSummary {
     }
 }
 
-impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
+impl<'a> ops::AddAssign<&'a Self> for TextSummary {
     fn add_assign(&mut self, other: &'a Self) {
         let joined_chars = self.last_line_chars + other.first_line_chars;
         if joined_chars > self.longest_row_chars {
@@ -1087,13 +1109,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
     }
 }
 
-impl std::ops::AddAssign<Self> for TextSummary {
+impl ops::AddAssign<Self> for TextSummary {
     fn add_assign(&mut self, other: Self) {
         *self += &other;
     }
 }
 
-pub trait TextDimension: 'static + for<'a> Dimension<'a, ChunkSummary> {
+pub trait TextDimension:
+    'static + Clone + Copy + Default + for<'a> Dimension<'a, ChunkSummary> + std::fmt::Debug
+{
     fn from_text_summary(summary: &TextSummary) -> Self;
     fn from_chunk(chunk: ChunkSlice) -> Self;
     fn add_assign(&mut self, other: &Self);
@@ -1129,7 +1153,7 @@ impl<'a> sum_tree::Dimension<'a, ChunkSummary> for TextSummary {
 
 impl TextDimension for TextSummary {
     fn from_text_summary(summary: &TextSummary) -> Self {
-        summary.clone()
+        *summary
     }
 
     fn from_chunk(chunk: ChunkSlice) -> Self {
@@ -1240,6 +1264,118 @@ impl TextDimension for PointUtf16 {
     }
 }
 
+/// A pair of text dimensions in which only the first dimension is used for comparison,
+/// but both dimensions are updated during addition and subtraction.
+#[derive(Clone, Copy, Debug)]
+pub struct DimensionPair<K, V> {
+    pub key: K,
+    pub value: Option<V>,
+}
+
+impl<K: Default, V: Default> Default for DimensionPair<K, V> {
+    fn default() -> Self {
+        Self {
+            key: Default::default(),
+            value: Some(Default::default()),
+        }
+    }
+}
+
+impl<K, V> cmp::Ord for DimensionPair<K, V>
+where
+    K: cmp::Ord,
+{
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        self.key.cmp(&other.key)
+    }
+}
+
+impl<K, V> cmp::PartialOrd for DimensionPair<K, V>
+where
+    K: cmp::PartialOrd,
+{
+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+        self.key.partial_cmp(&other.key)
+    }
+}
+
+impl<K, V> cmp::PartialEq for DimensionPair<K, V>
+where
+    K: cmp::PartialEq,
+{
+    fn eq(&self, other: &Self) -> bool {
+        self.key.eq(&other.key)
+    }
+}
+
+impl<K, V> ops::Sub for DimensionPair<K, V>
+where
+    K: ops::Sub<K, Output = K>,
+    V: ops::Sub<V, Output = V>,
+{
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self {
+            key: self.key - rhs.key,
+            value: self.value.zip(rhs.value).map(|(a, b)| a - b),
+        }
+    }
+}
+
+impl<K, V> cmp::Eq for DimensionPair<K, V> where K: cmp::Eq {}
+
+impl<'a, K, V> sum_tree::Dimension<'a, ChunkSummary> for DimensionPair<K, V>
+where
+    K: sum_tree::Dimension<'a, ChunkSummary>,
+    V: sum_tree::Dimension<'a, ChunkSummary>,
+{
+    fn zero(_cx: &()) -> Self {
+        Self {
+            key: K::zero(_cx),
+            value: Some(V::zero(_cx)),
+        }
+    }
+
+    fn add_summary(&mut self, summary: &'a ChunkSummary, _cx: &()) {
+        self.key.add_summary(summary, _cx);
+        if let Some(value) = &mut self.value {
+            value.add_summary(summary, _cx);
+        }
+    }
+}
+
+impl<K, V> TextDimension for DimensionPair<K, V>
+where
+    K: TextDimension,
+    V: TextDimension,
+{
+    fn add_assign(&mut self, other: &Self) {
+        self.key.add_assign(&other.key);
+        if let Some(value) = &mut self.value {
+            if let Some(other_value) = other.value.as_ref() {
+                value.add_assign(other_value);
+            } else {
+                self.value.take();
+            }
+        }
+    }
+
+    fn from_chunk(chunk: ChunkSlice) -> Self {
+        Self {
+            key: K::from_chunk(chunk),
+            value: Some(V::from_chunk(chunk)),
+        }
+    }
+
+    fn from_text_summary(summary: &TextSummary) -> Self {
+        Self {
+            key: K::from_text_summary(summary),
+            value: Some(V::from_text_summary(summary)),
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/rope/src/unclipped.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::{chunk::ChunkSlice, ChunkSummary, TextDimension, TextSummary};
+use crate::ChunkSummary;
 use std::ops::{Add, AddAssign, Sub, SubAssign};
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -22,20 +22,6 @@ impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, Chunk
     }
 }
 
-impl<T: TextDimension> TextDimension for Unclipped<T> {
-    fn from_text_summary(summary: &TextSummary) -> Self {
-        Unclipped(T::from_text_summary(summary))
-    }
-
-    fn from_chunk(chunk: ChunkSlice) -> Self {
-        Unclipped(T::from_chunk(chunk))
-    }
-
-    fn add_assign(&mut self, other: &Self) {
-        TextDimension::add_assign(&mut self.0, &other.0);
-    }
-}
-
 impl<T: Add<T, Output = T>> Add<Unclipped<T>> for Unclipped<T> {
     type Output = Unclipped<T>;
 

crates/sum_tree/src/sum_tree.rs πŸ”—

@@ -115,14 +115,29 @@ impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>> Dimension<'a, T
     }
 }
 
-impl<'a, S: Summary, D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, D2: Dimension<'a, S>>
-    SeekTarget<'a, S, (D1, D2)> for D1
+impl<'a, S, D1, D2> SeekTarget<'a, S, (D1, D2)> for D1
+where
+    S: Summary,
+    D1: SeekTarget<'a, S, D1> + Dimension<'a, S>,
+    D2: Dimension<'a, S>,
 {
     fn cmp(&self, cursor_location: &(D1, D2), cx: &S::Context) -> Ordering {
         self.cmp(&cursor_location.0, cx)
     }
 }
 
+impl<'a, S, D1, D2, D3> SeekTarget<'a, S, ((D1, D2), D3)> for D1
+where
+    S: Summary,
+    D1: SeekTarget<'a, S, D1> + Dimension<'a, S>,
+    D2: Dimension<'a, S>,
+    D3: Dimension<'a, S>,
+{
+    fn cmp(&self, cursor_location: &((D1, D2), D3), cx: &S::Context) -> Ordering {
+        self.cmp(&cursor_location.0 .0, cx)
+    }
+}
+
 struct End<D>(PhantomData<D>);
 
 impl<D> End<D> {

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -5,11 +5,7 @@ pub mod terminal_scrollbar;
 pub mod terminal_tab_tooltip;
 
 use collections::HashSet;
-use editor::{
-    actions::SelectAll,
-    scroll::{Autoscroll, ScrollbarAutoHide},
-    Editor, EditorSettings,
-};
+use editor::{actions::SelectAll, scroll::ScrollbarAutoHide, Editor, EditorSettings};
 use futures::{stream::FuturesUnordered, StreamExt};
 use gpui::{
     anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
@@ -17,7 +13,6 @@ use gpui::{
     MouseDownEvent, Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, View,
     VisualContext, WeakModel, WeakView,
 };
-use language::Bias;
 use persistence::TERMINAL_DB;
 use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
 use schemars::JsonSchema;
@@ -885,19 +880,13 @@ fn subscribe_for_terminal_events(
                                         active_editor
                                             .downgrade()
                                             .update(&mut cx, |editor, cx| {
-                                                let snapshot = editor.snapshot(cx).display_snapshot;
-                                                let point = snapshot.buffer_snapshot.clip_point(
+                                                editor.go_to_singleton_buffer_point(
                                                     language::Point::new(
                                                         row.saturating_sub(1),
                                                         col.saturating_sub(1),
                                                     ),
-                                                    Bias::Left,
-                                                );
-                                                editor.change_selections(
-                                                    Some(Autoscroll::center()),
                                                     cx,
-                                                    |s| s.select_ranges([point..point]),
-                                                );
+                                                )
                                             })
                                             .log_err();
                                     }

crates/text/src/patch.rs πŸ”—

@@ -42,6 +42,7 @@ where
         self.0
     }
 
+    #[must_use]
     pub fn compose(&self, new_edits_iter: impl IntoIterator<Item = Edit<T>>) -> Self {
         let mut old_edits_iter = self.0.iter().cloned().peekable();
         let mut new_edits_iter = new_edits_iter.into_iter().peekable();

crates/text/src/text.rs πŸ”—

@@ -1507,9 +1507,9 @@ impl Buffer {
         let mut rope_cursor = self.visible_text.cursor(0);
         disjoint_ranges.map(move |range| {
             position.add_assign(&rope_cursor.summary(range.start));
-            let start = position.clone();
+            let start = position;
             position.add_assign(&rope_cursor.summary(range.end));
-            let end = position.clone();
+            let end = position;
             start..end
         })
     }
@@ -2029,11 +2029,11 @@ impl BufferSnapshot {
         row_range: Range<u32>,
     ) -> impl Iterator<Item = (u32, LineIndent)> + '_ {
         let start = Point::new(row_range.start, 0).to_offset(self);
-        let end = Point::new(row_range.end - 1, self.line_len(row_range.end - 1)).to_offset(self);
+        let end = Point::new(row_range.end, self.line_len(row_range.end)).to_offset(self);
 
         let mut chunks = self.as_rope().chunks_in_range(start..end);
         let mut row = row_range.start;
-        let mut done = start == end;
+        let mut done = false;
         std::iter::from_fn(move || {
             if done {
                 None
@@ -2071,7 +2071,7 @@ impl BufferSnapshot {
         }
 
         let mut row = end_point.row;
-        let mut done = start == end;
+        let mut done = false;
         std::iter::from_fn(move || {
             if done {
                 None
@@ -2168,7 +2168,7 @@ impl BufferSnapshot {
             }
 
             position.add_assign(&text_cursor.summary(fragment_offset));
-            (position.clone(), payload)
+            (position, payload)
         })
     }
 
@@ -2176,10 +2176,14 @@ impl BufferSnapshot {
     where
         D: TextDimension,
     {
+        self.text_summary_for_range(0..self.offset_for_anchor(anchor))
+    }
+
+    pub fn offset_for_anchor(&self, anchor: &Anchor) -> usize {
         if *anchor == Anchor::MIN {
-            D::zero(&())
+            0
         } else if *anchor == Anchor::MAX {
-            D::from_text_summary(&self.visible_text.summary())
+            self.visible_text.len()
         } else {
             let anchor_key = InsertionFragmentKey {
                 timestamp: anchor.timestamp,
@@ -2217,7 +2221,7 @@ impl BufferSnapshot {
             if fragment.visible {
                 fragment_offset += anchor.offset - insertion.split_offset;
             }
-            self.text_summary_for_range(0..fragment_offset)
+            fragment_offset
         }
     }
 
@@ -2580,16 +2584,16 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo
                 }
 
                 let fragment_summary = self.visible_cursor.summary(visible_end);
-                let mut new_end = self.new_end.clone();
+                let mut new_end = self.new_end;
                 new_end.add_assign(&fragment_summary);
                 if let Some((edit, range)) = pending_edit.as_mut() {
-                    edit.new.end = new_end.clone();
+                    edit.new.end = new_end;
                     range.end = end_anchor;
                 } else {
                     pending_edit = Some((
                         Edit {
-                            old: self.old_end.clone()..self.old_end.clone(),
-                            new: self.new_end.clone()..new_end.clone(),
+                            old: self.old_end..self.old_end,
+                            new: self.new_end..new_end,
                         },
                         start_anchor..end_anchor,
                     ));
@@ -2609,16 +2613,16 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo
                     self.deleted_cursor.seek_forward(cursor.start().deleted);
                 }
                 let fragment_summary = self.deleted_cursor.summary(deleted_end);
-                let mut old_end = self.old_end.clone();
+                let mut old_end = self.old_end;
                 old_end.add_assign(&fragment_summary);
                 if let Some((edit, range)) = pending_edit.as_mut() {
-                    edit.old.end = old_end.clone();
+                    edit.old.end = old_end;
                     range.end = end_anchor;
                 } else {
                     pending_edit = Some((
                         Edit {
-                            old: self.old_end.clone()..old_end.clone(),
-                            new: self.new_end.clone()..self.new_end.clone(),
+                            old: self.old_end..old_end,
+                            new: self.new_end..self.new_end,
                         },
                         start_anchor..end_anchor,
                     ));

crates/vim/src/command.rs πŸ”—

@@ -138,22 +138,27 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     Vim::action(editor, cx, |vim, action: &GoToLine, cx| {
         vim.switch_mode(Mode::Normal, false, cx);
         let result = vim.update_editor(cx, |vim, editor, cx| {
-            action.range.head().buffer_row(vim, editor, cx)
+            let snapshot = editor.snapshot(cx);
+            let buffer_row = action.range.head().buffer_row(vim, editor, cx)?;
+            let current = editor.selections.newest::<Point>(cx);
+            let target = snapshot
+                .buffer_snapshot
+                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges([target..target]);
+            });
+
+            anyhow::Ok(())
         });
-        let buffer_row = match result {
-            None => return,
-            Some(e @ Err(_)) => {
-                let Some(workspace) = vim.workspace(cx) else {
-                    return;
-                };
-                workspace.update(cx, |workspace, cx| {
-                    e.notify_err(workspace, cx);
-                });
+        if let Some(e @ Err(_)) = result {
+            let Some(workspace) = vim.workspace(cx) else {
                 return;
-            }
-            Some(Ok(result)) => result,
-        };
-        vim.move_cursor(Motion::StartOfDocument, Some(buffer_row.0 as usize + 1), cx);
+            };
+            workspace.update(cx, |workspace, cx| {
+                e.notify_err(workspace, cx);
+            });
+            return;
+        }
     });
 
     Vim::action(editor, cx, |vim, action: &YankCommand, cx| {
@@ -462,7 +467,22 @@ impl Position {
     ) -> Result<MultiBufferRow> {
         let snapshot = editor.snapshot(cx);
         let target = match self {
-            Position::Line { row, offset } => row.saturating_add_signed(offset.saturating_sub(1)),
+            Position::Line { row, offset } => {
+                if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| {
+                    editor.buffer().read(cx).buffer_point_to_anchor(
+                        &buffer,
+                        Point::new(row.saturating_sub(1), 0),
+                        cx,
+                    )
+                }) {
+                    anchor
+                        .to_point(&snapshot.buffer_snapshot)
+                        .row
+                        .saturating_add_signed(*offset)
+                } else {
+                    row.saturating_add_signed(offset.saturating_sub(1))
+                }
+            }
             Position::Mark { name, offset } => {
                 let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
                     return Err(anyhow!("mark {} not set", name));
@@ -697,7 +717,8 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
         VimCommand::new(("foldc", "lose"), editor::actions::Fold)
             .bang(editor::actions::FoldRecursive)
             .range(act_on_range),
-        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleHunkDiff).range(act_on_range),
+        VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks)
+            .range(act_on_range),
         VimCommand::new(("rev", "ert"), editor::actions::RevertSelectedHunks).range(act_on_range),
         VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
         VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {

crates/vim/src/motion.rs πŸ”—

@@ -4,7 +4,7 @@ use editor::{
         self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
     },
     scroll::Autoscroll,
-    Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset,
+    Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
 };
 use gpui::{actions, impl_actions, px, ViewContext};
 use language::{CharKind, Point, Selection, SelectionGoal};
@@ -847,7 +847,10 @@ impl Motion {
                 SelectionGoal::None,
             ),
             CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
-            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
+            StartOfDocument => (
+                start_of_document(map, point, maybe_times),
+                SelectionGoal::None,
+            ),
             EndOfDocument => (
                 end_of_document(map, point, maybe_times),
                 SelectionGoal::None,
@@ -1956,25 +1959,96 @@ fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Opti
     Some(map.buffer_snapshot.len())
 }
 
-fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
-    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
-    *new_point.column_mut() = point.column();
-    map.clip_point(new_point, Bias::Left)
+fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
+    let point = map.display_point_to_point(display_point, Bias::Left);
+    let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
+        return display_point;
+    };
+    let offset = excerpt.buffer().point_to_offset(
+        excerpt
+            .buffer()
+            .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
+    );
+    let buffer_range = excerpt.buffer_range();
+    if offset >= buffer_range.start && offset <= buffer_range.end {
+        let point = map
+            .buffer_snapshot
+            .offset_to_point(excerpt.map_offset_from_buffer(offset));
+        return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
+    }
+    let mut last_position = None;
+    for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
+        let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
+            ..language::ToOffset::to_offset(&range.context.end, &buffer);
+        if offset >= excerpt_range.start && offset <= excerpt_range.end {
+            let text_anchor = buffer.anchor_after(offset);
+            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
+            return anchor.to_display_point(map);
+        } else if offset <= excerpt_range.start {
+            let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
+            return anchor.to_display_point(map);
+        } else {
+            last_position = Some(Anchor::in_buffer(
+                excerpt,
+                buffer.remote_id(),
+                range.context.end,
+            ));
+        }
+    }
+
+    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
+    last_point.column = point.column;
+
+    map.clip_point(
+        map.point_to_display_point(
+            map.buffer_snapshot.clip_point(point, Bias::Left),
+            Bias::Left,
+        ),
+        Bias::Left,
+    )
+}
+
+fn start_of_document(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    maybe_times: Option<usize>,
+) -> DisplayPoint {
+    if let Some(times) = maybe_times {
+        return go_to_line(map, display_point, times);
+    }
+
+    let point = map.display_point_to_point(display_point, Bias::Left);
+    let mut first_point = Point::zero();
+    first_point.column = point.column;
+
+    map.clip_point(
+        map.point_to_display_point(
+            map.buffer_snapshot.clip_point(first_point, Bias::Left),
+            Bias::Left,
+        ),
+        Bias::Left,
+    )
 }
 
 fn end_of_document(
     map: &DisplaySnapshot,
-    point: DisplayPoint,
-    line: Option<usize>,
+    display_point: DisplayPoint,
+    maybe_times: Option<usize>,
 ) -> DisplayPoint {
-    let new_row = if let Some(line) = line {
-        (line - 1) as u32
-    } else {
-        map.buffer_snapshot.max_row().0
+    if let Some(times) = maybe_times {
+        return go_to_line(map, display_point, times);
     };
+    let point = map.display_point_to_point(display_point, Bias::Left);
+    let mut last_point = map.buffer_snapshot.max_point();
+    last_point.column = point.column;
 
-    let new_point = Point::new(new_row, point.column());
-    map.clip_point(new_point.to_display_point(map), Bias::Left)
+    map.clip_point(
+        map.point_to_display_point(
+            map.buffer_snapshot.clip_point(last_point, Bias::Left),
+            Bias::Left,
+        ),
+        Bias::Left,
+    )
 }
 
 fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
@@ -2545,7 +2619,7 @@ fn section_motion(
     direction: Direction,
     is_start: bool,
 ) -> DisplayPoint {
-    if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
+    if map.buffer_snapshot.as_singleton().is_some() {
         for _ in 0..times {
             let offset = map
                 .display_point_to_point(display_point, Bias::Left)
@@ -2553,13 +2627,14 @@ fn section_motion(
             let range = if direction == Direction::Prev {
                 0..offset
             } else {
-                offset..buffer.len()
+                offset..map.buffer_snapshot.len()
             };
 
             // we set a max start depth here because we want a section to only be "top level"
             // similar to vim's default of '{' in the first column.
             // (and without it, ]] at the start of editor.rs is -very- slow)
-            let mut possibilities = buffer
+            let mut possibilities = map
+                .buffer_snapshot
                 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
                 .filter(|(_, object)| {
                     matches!(
@@ -2591,7 +2666,7 @@ fn section_motion(
             let offset = if direction == Direction::Prev {
                 possibilities.max().unwrap_or(0)
             } else {
-                possibilities.min().unwrap_or(buffer.len())
+                possibilities.min().unwrap_or(map.buffer_snapshot.len())
             };
 
             let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);

crates/vim/src/object.rs πŸ”—

@@ -494,7 +494,7 @@ pub fn surrounding_html_tag(
 
     let snapshot = &map.buffer_snapshot;
     let offset = head.to_offset(map, Bias::Left);
-    let excerpt = snapshot.excerpt_containing(offset..offset)?;
+    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
     let buffer = excerpt.buffer();
     let offset = excerpt.map_offset_to_buffer(offset);
 
@@ -664,7 +664,7 @@ fn text_object(
     let snapshot = &map.buffer_snapshot;
     let offset = relative_to.to_offset(map, Bias::Left);
 
-    let excerpt = snapshot.excerpt_containing(offset..offset)?;
+    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
     let buffer = excerpt.buffer();
     let offset = excerpt.map_offset_to_buffer(offset);
 
@@ -710,7 +710,7 @@ fn argument(
     let offset = relative_to.to_offset(map, Bias::Left);
 
     // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level
-    let excerpt = snapshot.excerpt_containing(offset..offset)?;
+    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
     let buffer = excerpt.buffer();
 
     fn comma_delimited_range_at(

crates/workspace/src/workspace.rs πŸ”—

@@ -102,6 +102,8 @@ use crate::persistence::{
     SerializedAxis,
 };
 
+pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
+
 static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
     env::var("ZED_WINDOW_SIZE")
         .ok()
@@ -4344,7 +4346,6 @@ impl Workspace {
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
         const CHUNK_SIZE: usize = 200;
-        const THROTTLE_TIME: Duration = Duration::from_millis(200);
 
         let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
 
@@ -4369,7 +4370,9 @@ impl Workspace {
                 }
             }
 
-            cx.background_executor().timer(THROTTLE_TIME).await;
+            cx.background_executor()
+                .timer(SERIALIZATION_THROTTLE_TIME)
+                .await;
         }
 
         Ok(())

crates/zed/src/zed.rs πŸ”—

@@ -1469,7 +1469,7 @@ mod tests {
     use workspace::{
         item::{Item, ItemHandle},
         open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
-        WorkspaceHandle,
+        WorkspaceHandle, SERIALIZATION_THROTTLE_TIME,
     };
 
     #[gpui::test]
@@ -2866,7 +2866,9 @@ mod tests {
             })
             .unwrap();
 
-        cx.run_until_parked();
+        cx.background_executor
+            .advance_clock(SERIALIZATION_THROTTLE_TIME);
+        cx.update(|_| {});
         editor_1.assert_released();
         editor_2.assert_released();
         buffer.assert_released();

crates/zed/src/zed/open_listener.rs πŸ”—

@@ -6,7 +6,6 @@ use cli::{ipc::IpcSender, CliRequest, CliResponse};
 use client::parse_zed_link;
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
-use editor::scroll::Autoscroll;
 use editor::Editor;
 use fs::Fs;
 use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
@@ -14,7 +13,7 @@ use futures::channel::{mpsc, oneshot};
 use futures::future::join_all;
 use futures::{FutureExt, SinkExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
-use language::{Bias, Point};
+use language::Point;
 use recent_projects::{open_ssh_project, SshSettings};
 use remote::SshConnectionOptions;
 use settings::Settings;
@@ -236,11 +235,7 @@ pub async fn open_paths_with_positions(
             workspace
                 .update(cx, |_, cx| {
                     active_editor.update(cx, |editor, cx| {
-                        let snapshot = editor.snapshot(cx).display_snapshot;
-                        let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
-                        editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-                            s.select_ranges([point..point])
-                        });
+                        editor.go_to_singleton_buffer_point(point, cx);
                     });
                 })
                 .log_err();