From 08cee47ed3a10443967cc71701b29400747ebc1b Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Mon, 16 Feb 2026 20:58:30 +0100 Subject: [PATCH] editor: Batch calls to fold_buffer in ProjectDiff::refresh (#49278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change improves performance of project diff in that: * scrolling in split view for very large diffs (think chromium repo with `git reset HEAD~1000`) is now very smooth on macOS and fairly smooth on Linux * switching from split to unified is very smooth on macOS, and fairly smooth on Linux There still remains the case of (severe) hangs when switching from unified to split however, but it will be addressed in a follow-up PR. Anyhow, here's the screenshot of the Instruments.app capture of opening chromium repo in Zed in split view, scrolling a little, moving to unified, scrolling some more, and moving back to split. Prior to this change, split -> unified would cause a severe hang, whereas now it's a hang and thus feels much smoother already (without Instruments profiling is barely visible). Unified -> split severe hangs are still there but don't last as long. Screenshot 2026-02-16 at 5 46 23 PM Release Notes: - Improved project diff performance when opening very large diffs/repositories. --- crates/editor/src/editor.rs | 41 +++++++++++--- .../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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6f3bab3133..053fe3265d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3625,7 +3625,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_at_cursor(cx); self.update_visible_edit_prediction(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; @@ -20304,22 +20304,46 @@ impl Editor { } pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - 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, + cx: &mut Context, + ) { + 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 = 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(); @@ -23906,9 +23930,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_at_cursor(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); } diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 020eea6f96..4401a87195 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/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, ) { self.clear_highlights(HighlightKey::MatchingBracket, cx); - let snapshot = self.snapshot(window, cx); let newest_selection = self.selections.newest::(&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(); + } } }); } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 7eee1ce764..2e8896731f 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/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); }); } })?; diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 9b6277285a..7507faf0f4 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/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, @@ -609,7 +609,7 @@ impl ProjectDiff { diff: Entity, window: &mut Window, cx: &mut Context, - ) { + ) -> Option { let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| { this._task = window.spawn(cx, { let this = cx.weak_entity(); @@ -654,6 +654,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( @@ -686,7 +688,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()); } }) }); @@ -707,6 +709,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)] @@ -762,6 +766,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. @@ -781,14 +787,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(); @@ -796,6 +804,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(); })?;