Fix editor rendering slowness with large folds (#31569)

Max Brunsfeld created

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.

Change summary

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(-)

Detailed changes

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(

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<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()
 }

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