From 97579662e6a62ec7532f077055a52413ab2e07de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 28 May 2025 16:05:06 -0700 Subject: [PATCH] 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. --- crates/editor/src/editor_tests.rs | 106 +++++++++++++++++++----- crates/editor/src/indent_guides.rs | 50 +++++++---- crates/multi_buffer/src/multi_buffer.rs | 19 ++++- 3 files changed, 137 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a94410d72e..c7af25f48d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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( diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index a17c0669b6..f6d51c929a 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -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 { - 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::>::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() } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a680c922d9..87f049330f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -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