editor: Batch calls to fold_buffer in ProjectDiff::refresh (#49278) (#49451)

Jakub Konka created

Cherry-pick of #49278 to preview.

Release Notes:

- Improved project diff performance when opening very large
diffs/repositories.

Change summary

crates/editor/src/editor.rs                     | 41 +++++++++++--
crates/editor/src/highlight_matching_bracket.rs | 55 +++++++++---------
crates/git_ui/src/commit_view.rs                |  4 -
crates/git_ui/src/project_diff.rs               | 25 ++++++-
4 files changed, 82 insertions(+), 43 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -3600,7 +3600,7 @@ impl Editor {
             refresh_linked_ranges(self, window, cx);
 
             self.refresh_selected_text_highlights(false, window, cx);
-            self.refresh_matching_bracket_highlights(window, cx);
+            self.refresh_matching_bracket_highlights(&display_map, cx);
             self.refresh_outline_symbols(cx);
             self.update_visible_edit_prediction(window, cx);
             self.edit_prediction_requires_modifier_in_indent_conflict = true;
@@ -20265,22 +20265,46 @@ impl Editor {
     }
 
     pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
-        if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) {
+        self.fold_buffers([buffer_id], cx);
+    }
+
+    pub fn fold_buffers(
+        &mut self,
+        buffer_ids: impl IntoIterator<Item = BufferId>,
+        cx: &mut Context<Self>,
+    ) {
+        if self.buffer().read(cx).is_singleton() {
             return;
         }
 
-        let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx);
+        let ids_to_fold: Vec<BufferId> = buffer_ids
+            .into_iter()
+            .filter(|id| !self.is_buffer_folded(*id, cx))
+            .collect();
+
+        if ids_to_fold.is_empty() {
+            return;
+        }
+
+        let mut all_folded_excerpt_ids = Vec::new();
+        for buffer_id in &ids_to_fold {
+            let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(*buffer_id, cx);
+            all_folded_excerpt_ids.extend(folded_excerpts.into_iter().map(|(id, _)| id));
+        }
+
         self.display_map.update(cx, |display_map, cx| {
-            display_map.fold_buffers([buffer_id], cx)
+            display_map.fold_buffers(ids_to_fold.clone(), cx)
         });
 
         let snapshot = self.display_snapshot(cx);
         self.selections.change_with(&snapshot, |selections| {
-            selections.remove_selections_from_buffer(buffer_id);
+            for buffer_id in ids_to_fold {
+                selections.remove_selections_from_buffer(buffer_id);
+            }
         });
 
         cx.emit(EditorEvent::BufferFoldToggled {
-            ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
+            ids: all_folded_excerpt_ids,
             folded: true,
         });
         cx.notify();
@@ -23856,9 +23880,10 @@ impl Editor {
                 self.refresh_active_diagnostics(cx);
                 self.refresh_code_actions(window, cx);
                 self.refresh_single_line_folds(window, cx);
-                self.refresh_matching_bracket_highlights(window, cx);
+                let snapshot = self.snapshot(window, cx);
+                self.refresh_matching_bracket_highlights(&snapshot, cx);
                 self.refresh_outline_symbols(cx);
-                self.refresh_sticky_headers(&self.snapshot(window, cx), cx);
+                self.refresh_sticky_headers(&snapshot, cx);
                 if self.has_active_edit_prediction() {
                     self.update_visible_edit_prediction(window, cx);
                 }

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -1,5 +1,5 @@
-use crate::{Editor, HighlightKey, RangeToAnchorExt};
-use gpui::{AppContext, Context, HighlightStyle, Window};
+use crate::{Editor, HighlightKey, RangeToAnchorExt, display_map::DisplaySnapshot};
+use gpui::{AppContext, Context, HighlightStyle};
 use language::CursorShape;
 use multi_buffer::MultiBufferOffset;
 use theme::ActiveTheme;
@@ -8,12 +8,11 @@ impl Editor {
     #[ztracing::instrument(skip_all)]
     pub fn refresh_matching_bracket_highlights(
         &mut self,
-        window: &Window,
+        snapshot: &DisplaySnapshot,
         cx: &mut Context<Editor>,
     ) {
         self.clear_highlights(HighlightKey::MatchingBracket, cx);
 
-        let snapshot = self.snapshot(window, cx);
         let newest_selection = self.selections.newest::<MultiBufferOffset>(&snapshot);
         // Don't highlight brackets if the selection isn't empty
         if !newest_selection.is_empty() {
@@ -39,29 +38,31 @@ impl Editor {
             let buffer_snapshot = buffer_snapshot.clone();
             async move { buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None) }
         });
-        self.refresh_matching_bracket_highlights_task = cx.spawn(async move |editor, cx| {
-            if let Some((opening_range, closing_range)) = task.await {
-                let buffer_snapshot = snapshot.buffer_snapshot();
-                editor
-                    .update(cx, |editor, cx| {
-                        editor.highlight_text(
-                            HighlightKey::MatchingBracket,
-                            vec![
-                                opening_range.to_anchors(&buffer_snapshot),
-                                closing_range.to_anchors(&buffer_snapshot),
-                            ],
-                            HighlightStyle {
-                                background_color: Some(
-                                    cx.theme()
-                                        .colors()
-                                        .editor_document_highlight_bracket_background,
-                                ),
-                                ..Default::default()
-                            },
-                            cx,
-                        )
-                    })
-                    .ok();
+        self.refresh_matching_bracket_highlights_task = cx.spawn({
+            let buffer_snapshot = buffer_snapshot.clone();
+            async move |editor, cx| {
+                if let Some((opening_range, closing_range)) = task.await {
+                    editor
+                        .update(cx, |editor, cx| {
+                            editor.highlight_text(
+                                HighlightKey::MatchingBracket,
+                                vec![
+                                    opening_range.to_anchors(&buffer_snapshot),
+                                    closing_range.to_anchors(&buffer_snapshot),
+                                ],
+                                HighlightStyle {
+                                    background_color: Some(
+                                        cx.theme()
+                                            .colors()
+                                            .editor_document_highlight_bracket_background,
+                                    ),
+                                    ..Default::default()
+                                },
+                                cx,
+                            )
+                        })
+                        .ok();
+                }
             }
         });
     }

crates/git_ui/src/commit_view.rs 🔗

@@ -362,9 +362,7 @@ impl CommitView {
                 });
                 if !binary_buffer_ids.is_empty() {
                     this.editor.update(cx, |editor, cx| {
-                        for buffer_id in binary_buffer_ids {
-                            editor.fold_buffer(buffer_id, cx);
-                        }
+                        editor.fold_buffers(binary_buffer_ids, cx);
                     });
                 }
             })?;

crates/git_ui/src/project_diff.rs 🔗

@@ -24,7 +24,7 @@ use gpui::{
     Action, AnyElement, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,
     FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions,
 };
-use language::{Anchor, Buffer, Capability, OffsetRangeExt};
+use language::{Anchor, Buffer, BufferId, Capability, OffsetRangeExt};
 use multi_buffer::{MultiBuffer, PathKey};
 use project::{
     Project, ProjectPath,
@@ -608,7 +608,7 @@ impl ProjectDiff {
         diff: Entity<BufferDiff>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) {
+    ) -> Option<BufferId> {
         let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| {
             this._task = window.spawn(cx, {
                 let this = cx.weak_entity();
@@ -653,6 +653,8 @@ impl ProjectDiff {
             }
         };
 
+        let mut needs_fold = None;
+
         let (was_empty, is_excerpt_newly_added) = self.editor.update(cx, |editor, cx| {
             let was_empty = editor.rhs_editor().read(cx).buffer().read(cx).is_empty();
             let (_, is_newly_added) = editor.set_excerpts_for_path(
@@ -685,7 +687,7 @@ impl ProjectDiff {
                         || (file_status.is_untracked()
                             && GitPanelSettings::get_global(cx).collapse_untracked_diff))
                 {
-                    editor.fold_buffer(snapshot.text.remote_id(), cx)
+                    needs_fold = Some(snapshot.text.remote_id());
                 }
             })
         });
@@ -706,6 +708,8 @@ impl ProjectDiff {
         if self.pending_scroll.as_ref() == Some(&path_key) {
             self.move_to_path(path_key, window, cx);
         }
+
+        needs_fold
     }
 
     #[instrument(skip_all)]
@@ -761,6 +765,8 @@ impl ProjectDiff {
             buffers_to_load
         })?;
 
+        let mut buffers_to_fold = Vec::new();
+
         for (entry, path_key) in buffers_to_load.into_iter().zip(path_keys.into_iter()) {
             if let Some((buffer, diff)) = entry.load.await.log_err() {
                 // We might be lagging behind enough that all future entry.load futures are no longer pending.
@@ -780,14 +786,16 @@ impl ProjectDiff {
                                 RefreshReason::StatusesChanged => false,
                             };
                         if !skip {
-                            this.register_buffer(
+                            if let Some(buffer_id) = this.register_buffer(
                                 path_key,
                                 entry.file_status,
                                 buffer,
                                 diff,
                                 window,
                                 cx,
-                            )
+                            ) {
+                                buffers_to_fold.push(buffer_id);
+                            }
                         }
                     })
                     .ok();
@@ -795,6 +803,13 @@ impl ProjectDiff {
             }
         }
         this.update(cx, |this, cx| {
+            if !buffers_to_fold.is_empty() {
+                this.editor.update(cx, |editor, cx| {
+                    editor
+                        .rhs_editor()
+                        .update(cx, |editor, cx| editor.fold_buffers(buffers_to_fold, cx));
+                });
+            }
             this.pending_scroll.take();
             cx.notify();
         })?;