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) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
}"
fn main() {
let a = 1;
}"
.unindent(),
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) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
}"
fn main() {
let a = 1;
let b = 2;
}"
.unindent(),
cx,
)
@ -16799,14 +16799,14 @@ async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
async fn test_indent_guide_nested(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
@ -16828,11 +16828,11 @@ async fn test_indent_guide_nested(cx: &mut TestAppContext) {
async fn test_indent_guide_tab(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
.unindent(),
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]
async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
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 gpui::{App, AppContext as _, Context, Task, Window};
use language::language_settings::language_settings;
use multi_buffer::{IndentGuide, MultiBufferRow};
use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
use text::{LineIndent, Point};
use util::ResultExt;
@ -154,12 +154,28 @@ pub fn indent_guides_in_range(
snapshot: &DisplaySnapshot,
cx: &App,
) -> Vec<IndentGuide> {
let start_anchor = snapshot
let start_offset = snapshot
.buffer_snapshot
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
let end_anchor = snapshot
.point_to_offset(Point::new(visible_buffer_range.start.0, 0));
let end_offset = 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
.buffer_snapshot
@ -169,15 +185,19 @@ pub fn indent_guides_in_range(
return false;
}
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
// 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
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
let has_containing_fold = fold_ranges
.binary_search_by(|fold_range| {
if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
Ordering::Greater
} else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok();
!has_containing_fold
})
.collect()
}

View file

@ -5753,15 +5753,28 @@ impl MultiBufferSnapshot {
let mut result = Vec::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() {
if first_row > end_row {
break;
}
let current_depth = indent_stack.len() as u32;
let settings =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx);
let tab_size = settings.tab_size.get() as u32;
// Avoid retrieving the language settings repeatedly for every buffer row.
if let Some((prev_buffer_id, _)) = &prev_settings {
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
// then add to the indent stack with the depth found