From 9c889c163daffa070b24f52c04d4ef8297ce2e71 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 6f3bab3133d0192ce6b5af0591ad98a38eafe1a9..053fe3265dadcbbe2c8fba7013e88ed082540a9b 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 020eea6f9677ea9a04cda798d4d1dff7b2f85b85..4401a87195eb13413cfc2fa9c8e7784f737ac3fc 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 7eee1ce7640784fd37efe69b5f6f92b7cbc438ec..2e8896731f2ce1c27f4322f3a58e82c7a284a0e2 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 9b6277285abc1fc6baf24f0f944449407724d332..7507faf0f40db5b252923d61ef44fe78af2a5899 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(); })?;