Fix editor rendering slowness with large folds (#31569)

Closes https://github.com/zed-industries/zed/issues/31565

* Looking up settings on every row was very slow in the case of large
folds, especially if there was an `.editorconfig` file with numerous
glob patterns
* Checking whether each indent guide was within a fold was very slow,
when a fold spanned many indent guides.

Release Notes:

- Fixed slowness that could happen when editing in the presence of large
folds.
This commit is contained in:
Max Brunsfeld 2025-05-28 16:05:06 -07:00 committed by GitHub
parent 53849cf983
commit 97579662e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 137 additions and 38 deletions

View file

@ -16768,9 +16768,9 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -
async fn test_indent_guide_single_line(cx: &mut TestAppContext) { async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor( let (buffer_id, mut cx) = setup_indent_guides_editor(
&" &"
fn main() { fn main() {
let a = 1; let a = 1;
}" }"
.unindent(), .unindent(),
cx, cx,
) )
@ -16783,10 +16783,10 @@ async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
async fn test_indent_guide_simple_block(cx: &mut TestAppContext) { async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor( let (buffer_id, mut cx) = setup_indent_guides_editor(
&" &"
fn main() { fn main() {
let a = 1; let a = 1;
let b = 2; let b = 2;
}" }"
.unindent(), .unindent(),
cx, cx,
) )
@ -16799,14 +16799,14 @@ async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
async fn test_indent_guide_nested(cx: &mut TestAppContext) { async fn test_indent_guide_nested(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor( let (buffer_id, mut cx) = setup_indent_guides_editor(
&" &"
fn main() { fn main() {
let a = 1; let a = 1;
if a == 3 { if a == 3 {
let b = 2; let b = 2;
} else { } else {
let c = 3; let c = 3;
} }
}" }"
.unindent(), .unindent(),
cx, cx,
) )
@ -16828,11 +16828,11 @@ async fn test_indent_guide_nested(cx: &mut TestAppContext) {
async fn test_indent_guide_tab(cx: &mut TestAppContext) { async fn test_indent_guide_tab(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor( let (buffer_id, mut cx) = setup_indent_guides_editor(
&" &"
fn main() { fn main() {
let a = 1; let a = 1;
let b = 2; let b = 2;
let c = 3; let c = 3;
}" }"
.unindent(), .unindent(),
cx, cx,
) )
@ -16962,6 +16962,72 @@ async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
); );
} }
#[gpui::test]
async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
if a {
b(
c,
d,
)
} else {
e(
f
)
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..11,
vec![
indent_guide(buffer_id, 1, 10, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 7, 9, 1),
indent_guide(buffer_id, 3, 4, 2),
indent_guide(buffer_id, 8, 8, 2),
],
None,
&mut cx,
);
cx.update_editor(|editor, window, cx| {
editor.fold_at(MultiBufferRow(2), window, cx);
assert_eq!(
editor.display_text(cx),
"
fn main() {
if a {
b(
)
} else {
e(
f
)
}
}"
.unindent()
);
});
assert_indent_guides(
0..11,
vec![
indent_guide(buffer_id, 1, 10, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 7, 9, 1),
indent_guide(buffer_id, 8, 8, 2),
],
None,
&mut cx,
);
}
#[gpui::test] #[gpui::test]
async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) { async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor( let (buffer_id, mut cx) = setup_indent_guides_editor(

View file

@ -1,9 +1,9 @@
use std::{ops::Range, time::Duration}; use std::{cmp::Ordering, ops::Range, time::Duration};
use collections::HashSet; use collections::HashSet;
use gpui::{App, AppContext as _, Context, Task, Window}; use gpui::{App, AppContext as _, Context, Task, Window};
use language::language_settings::language_settings; use language::language_settings::language_settings;
use multi_buffer::{IndentGuide, MultiBufferRow}; use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
use text::{LineIndent, Point}; use text::{LineIndent, Point};
use util::ResultExt; use util::ResultExt;
@ -154,12 +154,28 @@ pub fn indent_guides_in_range(
snapshot: &DisplaySnapshot, snapshot: &DisplaySnapshot,
cx: &App, cx: &App,
) -> Vec<IndentGuide> { ) -> Vec<IndentGuide> {
let start_anchor = snapshot let start_offset = snapshot
.buffer_snapshot .buffer_snapshot
.anchor_before(Point::new(visible_buffer_range.start.0, 0)); .point_to_offset(Point::new(visible_buffer_range.start.0, 0));
let end_anchor = snapshot let end_offset = snapshot
.buffer_snapshot .buffer_snapshot
.anchor_after(Point::new(visible_buffer_range.end.0, 0)); .point_to_offset(Point::new(visible_buffer_range.end.0, 0));
let start_anchor = snapshot.buffer_snapshot.anchor_before(start_offset);
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
let mut fold_ranges = Vec::<Range<Point>>::new();
let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
while let Some(fold) = folds.next() {
let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
if let Some(last_range) = fold_ranges.last_mut() {
if last_range.end >= start {
last_range.end = last_range.end.max(end);
continue;
}
}
fold_ranges.push(start..end);
}
snapshot snapshot
.buffer_snapshot .buffer_snapshot
@ -169,15 +185,19 @@ pub fn indent_guides_in_range(
return false; return false;
} }
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1)); let has_containing_fold = fold_ranges
// Filter out indent guides that are inside a fold .binary_search_by(|fold_range| {
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
// 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 Ordering::Greater
let is_folded = snapshot.is_line_folded(start); } else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
let line_indent = snapshot.line_indent_for_buffer_row(start); Ordering::Less
let contained_in_fold = } else {
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level(); Ordering::Equal
!(is_folded && contained_in_fold) }
})
.is_ok();
!has_containing_fold
}) })
.collect() .collect()
} }

View file

@ -5753,15 +5753,28 @@ impl MultiBufferSnapshot {
let mut result = Vec::new(); let mut result = Vec::new();
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new(); let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
let mut prev_settings = None;
while let Some((first_row, mut line_indent, buffer)) = row_indents.next() { while let Some((first_row, mut line_indent, buffer)) = row_indents.next() {
if first_row > end_row { if first_row > end_row {
break; break;
} }
let current_depth = indent_stack.len() as u32; let current_depth = indent_stack.len() as u32;
let settings = // Avoid retrieving the language settings repeatedly for every buffer row.
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx); if let Some((prev_buffer_id, _)) = &prev_settings {
let tab_size = settings.tab_size.get() as u32; if prev_buffer_id != &buffer.remote_id() {
prev_settings.take();
}
}
let settings = &prev_settings
.get_or_insert_with(|| {
(
buffer.remote_id(),
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx),
)
})
.1;
let tab_size = settings.tab_size.get();
// When encountering empty, continue until found useful line indent // When encountering empty, continue until found useful line indent
// then add to the indent stack with the depth found // then add to the indent stack with the depth found