editor: Batch calls to fold_buffer in ProjectDiff::refresh (#49278)
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. <img width="2301" height="374" alt="Screenshot 2026-02-16 at 5 46 23 PM" src="https://github.com/user-attachments/assets/f687f8d4-cffd-47f1-ada1-f6c4d3ac3cd4" /> Release Notes: - Improved project diff performance when opening very large diffs/repositories.
This commit is contained in:
1 parent
f2661549ee
commit
08cee47ed3
4 files changed
+82
-43
No files matched your search
@@ -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<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();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -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<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();
|
||||
@@ -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();
|
||||
})?;
|
||||
|
||||
Reference in new issue
Block a user