Skip over folded regions when iterating over multibuffer chunks (#15646)

Piotr Osiewicz , Max , and Max Brunsfeld created

This commit weaves through new APIs for language::BufferChunks, multi_buffer::MultiBufferChunks and inlay_map::InlayChunks that allow seeking with an upper-bound. This allows us to omit doing syntax highligting and looking up diagnostics for folded ranges. This in turn directly improves performance of assistant panel with large contexts.

Release Notes:

- Fixed poor performance when editing in the assistant panel after
inserting large files using slash commands

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

Cargo.lock                                 |   1 
crates/editor/Cargo.toml                   |   1 
crates/editor/src/display_map/fold_map.rs  | 137 ++++++++++++++++-------
crates/editor/src/display_map/inlay_map.rs |  12 +
crates/editor/src/editor_tests.rs          |  57 +++++++++
crates/language/src/buffer.rs              | 124 +++++++++++++--------
crates/language/src/language.rs            |   2 
crates/multi_buffer/src/multi_buffer.rs    |  28 +++-
crates/rope/src/rope.rs                    |   5 
9 files changed, 261 insertions(+), 106 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -3599,6 +3599,7 @@ dependencies = [
  "time",
  "time_format",
  "tree-sitter-html",
+ "tree-sitter-md",
  "tree-sitter-rust",
  "tree-sitter-typescript",
  "ui",

crates/editor/Cargo.toml πŸ”—

@@ -93,6 +93,7 @@ settings = { workspace = true, features = ["test-support"] }
 text = { workspace = true, features = ["test-support"] }
 theme = { workspace = true, features = ["test-support"] }
 tree-sitter-html.workspace = true
+tree-sitter-md.workspace = true
 tree-sitter-rust.workspace = true
 tree-sitter-typescript.workspace = true
 unindent.workspace = true

crates/editor/src/display_map/fold_map.rs πŸ”—

@@ -11,7 +11,7 @@ use std::{
     ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
     sync::Arc,
 };
-use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
+use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary};
 use util::post_inc;
 
 #[derive(Clone)]
@@ -277,6 +277,17 @@ impl FoldMap {
                 "transform tree does not match inlay snapshot's length"
             );
 
+            let mut prev_transform_isomorphic = false;
+            for transform in self.snapshot.transforms.iter() {
+                if !transform.is_fold() && prev_transform_isomorphic {
+                    panic!(
+                        "found adjacent isomorphic transforms: {:?}",
+                        self.snapshot.transforms.items(&())
+                    );
+                }
+                prev_transform_isomorphic = !transform.is_fold();
+            }
+
             let mut folds = self.snapshot.folds.iter().peekable();
             while let Some(fold) = folds.next() {
                 if let Some(next_fold) = folds.peek() {
@@ -303,11 +314,24 @@ impl FoldMap {
         } else {
             let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
 
-            let mut new_transforms = SumTree::new();
+            let mut new_transforms = SumTree::<Transform>::new();
             let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
             cursor.seek(&InlayOffset(0), Bias::Right, &());
 
             while let Some(mut edit) = inlay_edits_iter.next() {
+                if let Some(item) = cursor.item() {
+                    if !item.is_fold() {
+                        new_transforms.update_last(
+                            |transform| {
+                                if !transform.is_fold() {
+                                    transform.summary.add_summary(&item.summary, &());
+                                    cursor.next(&());
+                                }
+                            },
+                            &(),
+                        );
+                    }
+                }
                 new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
                 edit.new.start -= edit.old.start - *cursor.start();
                 edit.old.start = *cursor.start();
@@ -392,16 +416,7 @@ impl FoldMap {
                     if fold_range.start.0 > sum.input.len {
                         let text_summary = inlay_snapshot
                             .text_summary_for_range(InlayOffset(sum.input.len)..fold_range.start);
-                        new_transforms.push(
-                            Transform {
-                                summary: TransformSummary {
-                                    output: text_summary.clone(),
-                                    input: text_summary,
-                                },
-                                placeholder: None,
-                            },
-                            &(),
-                        );
+                        push_isomorphic(&mut new_transforms, text_summary);
                     }
 
                     if fold_range.end > fold_range.start {
@@ -438,32 +453,14 @@ impl FoldMap {
                 if sum.input.len < edit.new.end.0 {
                     let text_summary = inlay_snapshot
                         .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
-                    new_transforms.push(
-                        Transform {
-                            summary: TransformSummary {
-                                output: text_summary.clone(),
-                                input: text_summary,
-                            },
-                            placeholder: None,
-                        },
-                        &(),
-                    );
+                    push_isomorphic(&mut new_transforms, text_summary);
                 }
             }
 
             new_transforms.append(cursor.suffix(&()), &());
             if new_transforms.is_empty() {
                 let text_summary = inlay_snapshot.text_summary();
-                new_transforms.push(
-                    Transform {
-                        summary: TransformSummary {
-                            output: text_summary.clone(),
-                            input: text_summary,
-                        },
-                        placeholder: None,
-                    },
-                    &(),
-                );
+                push_isomorphic(&mut new_transforms, text_summary);
             }
 
             drop(cursor);
@@ -715,17 +712,25 @@ impl FoldSnapshot {
         highlights: Highlights<'a>,
     ) -> FoldChunks<'a> {
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
+        transform_cursor.seek(&range.start, Bias::Right, &());
 
-        let inlay_end = {
-            transform_cursor.seek(&range.end, Bias::Right, &());
-            let overshoot = range.end.0 - transform_cursor.start().0 .0;
+        let inlay_start = {
+            let overshoot = range.start.0 - transform_cursor.start().0 .0;
             transform_cursor.start().1 + InlayOffset(overshoot)
         };
 
-        let inlay_start = {
-            transform_cursor.seek(&range.start, Bias::Right, &());
-            let overshoot = range.start.0 - transform_cursor.start().0 .0;
+        let transform_end = transform_cursor.end(&());
+
+        let inlay_end = if transform_cursor
+            .item()
+            .map_or(true, |transform| transform.is_fold())
+        {
+            inlay_start
+        } else if range.end < transform_end.0 {
+            let overshoot = range.end.0 - transform_cursor.start().0 .0;
             transform_cursor.start().1 + InlayOffset(overshoot)
+        } else {
+            transform_end.1
         };
 
         FoldChunks {
@@ -737,8 +742,8 @@ impl FoldSnapshot {
             ),
             inlay_chunk: None,
             inlay_offset: inlay_start,
-            output_offset: range.start.0,
-            max_output_offset: range.end.0,
+            output_offset: range.start,
+            max_output_offset: range.end,
         }
     }
 
@@ -783,6 +788,32 @@ impl FoldSnapshot {
     }
 }
 
+fn push_isomorphic(transforms: &mut SumTree<Transform>, summary: TextSummary) {
+    let mut did_merge = false;
+    transforms.update_last(
+        |last| {
+            if !last.is_fold() {
+                last.summary.input += summary.clone();
+                last.summary.output += summary.clone();
+                did_merge = true;
+            }
+        },
+        &(),
+    );
+    if !did_merge {
+        transforms.push(
+            Transform {
+                summary: TransformSummary {
+                    input: summary.clone(),
+                    output: summary,
+                },
+                placeholder: None,
+            },
+            &(),
+        )
+    }
+}
+
 fn intersecting_folds<'a, T>(
     inlay_snapshot: &'a InlaySnapshot,
     folds: &'a SumTree<Fold>,
@@ -1079,8 +1110,8 @@ pub struct FoldChunks<'a> {
     inlay_chunks: InlayChunks<'a>,
     inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
     inlay_offset: InlayOffset,
-    output_offset: usize,
-    max_output_offset: usize,
+    output_offset: FoldOffset,
+    max_output_offset: FoldOffset,
 }
 
 impl<'a> Iterator for FoldChunks<'a> {
@@ -1098,7 +1129,6 @@ impl<'a> Iterator for FoldChunks<'a> {
         if let Some(placeholder) = transform.placeholder.as_ref() {
             self.inlay_chunk.take();
             self.inlay_offset += InlayOffset(transform.summary.input.len);
-            self.inlay_chunks.seek(self.inlay_offset);
 
             while self.inlay_offset >= self.transform_cursor.end(&()).1
                 && self.transform_cursor.item().is_some()
@@ -1106,7 +1136,7 @@ impl<'a> Iterator for FoldChunks<'a> {
                 self.transform_cursor.next(&());
             }
 
-            self.output_offset += placeholder.text.len();
+            self.output_offset.0 += placeholder.text.len();
             return Some(Chunk {
                 text: placeholder.text,
                 renderer: Some(placeholder.renderer.clone()),
@@ -1114,6 +1144,23 @@ impl<'a> Iterator for FoldChunks<'a> {
             });
         }
 
+        // When we reach a non-fold region, seek the underlying text
+        // chunk iterator to the next unfolded range.
+        if self.inlay_offset == self.transform_cursor.start().1
+            && self.inlay_chunks.offset() != self.inlay_offset
+        {
+            let transform_start = self.transform_cursor.start();
+            let transform_end = self.transform_cursor.end(&());
+            let inlay_end = if self.max_output_offset < transform_end.0 {
+                let overshoot = self.max_output_offset.0 - transform_start.0 .0;
+                transform_start.1 + InlayOffset(overshoot)
+            } else {
+                transform_end.1
+            };
+
+            self.inlay_chunks.seek(self.inlay_offset..inlay_end);
+        }
+
         // Retrieve a chunk from the current location in the buffer.
         if self.inlay_chunk.is_none() {
             let chunk_offset = self.inlay_chunks.offset();
@@ -1136,7 +1183,7 @@ impl<'a> Iterator for FoldChunks<'a> {
             }
 
             self.inlay_offset = chunk_end;
-            self.output_offset += chunk.text.len();
+            self.output_offset.0 += chunk.text.len();
             return Some(chunk);
         }
 

crates/editor/src/display_map/inlay_map.rs πŸ”—

@@ -225,14 +225,16 @@ pub struct InlayChunks<'a> {
 }
 
 impl<'a> InlayChunks<'a> {
-    pub fn seek(&mut self, offset: InlayOffset) {
-        self.transforms.seek(&offset, Bias::Right, &());
+    pub fn seek(&mut self, new_range: Range<InlayOffset>) {
+        self.transforms.seek(&new_range.start, Bias::Right, &());
 
-        let buffer_offset = self.snapshot.to_buffer_offset(offset);
-        self.buffer_chunks.seek(buffer_offset);
+        let buffer_range = self.snapshot.to_buffer_offset(new_range.start)
+            ..self.snapshot.to_buffer_offset(new_range.end);
+        self.buffer_chunks.seek(buffer_range);
         self.inlay_chunks = None;
         self.buffer_chunk = None;
-        self.output_offset = offset;
+        self.output_offset = new_range.start;
+        self.max_output_offset = new_range.end;
     }
 
     pub fn offset(&self) -> InlayOffset {

crates/editor/src/editor_tests.rs πŸ”—

@@ -3668,6 +3668,63 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
         );
     });
 }
+#[gpui::test]
+async fn test_fold_perf(cx: &mut TestAppContext) {
+    use std::fmt::Write;
+    init_test(cx, |_| {});
+    let mut view = EditorTestContext::new(cx).await;
+    let language_registry = view.language_registry();
+    let language_name = Arc::from("Markdown");
+    let md_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: Arc::clone(&language_name),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["md".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_md::language()),
+        )
+        .with_highlights_query(
+            r#"
+        "#,
+        )
+        .unwrap(),
+    );
+    language_registry.add(md_language.clone());
+
+    let mut text = String::default();
+    writeln!(&mut text, "start").unwrap();
+    writeln!(&mut text, "```").unwrap();
+    const LINE_COUNT: u32 = 10000;
+    for i in 0..LINE_COUNT {
+        writeln!(&mut text, "{i}").unwrap();
+    }
+
+    writeln!(&mut text, "```").unwrap();
+    writeln!(&mut text, "end").unwrap();
+    view.update_buffer(|buffer, cx| {
+        buffer.set_language(Some(md_language), cx);
+    });
+    let t0 = Instant::now();
+    _ = view.update_editor(|view, cx| {
+        eprintln!("Text length: {}", text.len());
+        view.set_text(text, cx);
+        eprintln!(">>");
+        view.fold_ranges(
+            vec![(
+                Point::new(1, 0)..Point::new(LINE_COUNT + 2, 3),
+                FoldPlaceholder::test(),
+            )],
+            false,
+            cx,
+        );
+    });
+    eprintln!("{:?}", t0.elapsed());
+    eprintln!("<<");
+}
 
 #[gpui::test]
 fn test_move_line_up_down(cx: &mut TestAppContext) {

crates/language/src/buffer.rs πŸ”—

@@ -449,6 +449,7 @@ struct BufferChunkHighlights<'a> {
 /// An iterator that yields chunks of a buffer's text, along with their
 /// syntax highlights and diagnostic status.
 pub struct BufferChunks<'a> {
+    buffer_snapshot: Option<&'a BufferSnapshot>,
     range: Range<usize>,
     chunks: text::Chunks<'a>,
     diagnostic_endpoints: Peekable<vec::IntoIter<DiagnosticEndpoint>>,
@@ -2475,6 +2476,17 @@ impl BufferSnapshot {
         None
     }
 
+    fn get_highlights(&self, range: Range<usize>) -> (SyntaxMapCaptures, Vec<HighlightMap>) {
+        let captures = self.syntax.captures(range, &self.text, |grammar| {
+            grammar.highlights_query.as_ref()
+        });
+        let highlight_maps = captures
+            .grammars()
+            .into_iter()
+            .map(|grammar| grammar.highlight_map())
+            .collect();
+        (captures, highlight_maps)
+    }
     /// Iterates over chunks of text in the given range of the buffer. Text is chunked
     /// in an arbitrary way due to being stored in a [`Rope`](text::Rope). The text is also
     /// returned in chunks where each chunk has a single syntax highlighting style and
@@ -2483,36 +2495,11 @@ impl BufferSnapshot {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
 
         let mut syntax = None;
-        let mut diagnostic_endpoints = Vec::new();
         if language_aware {
-            let captures = self.syntax.captures(range.clone(), &self.text, |grammar| {
-                grammar.highlights_query.as_ref()
-            });
-            let highlight_maps = captures
-                .grammars()
-                .into_iter()
-                .map(|grammar| grammar.highlight_map())
-                .collect();
-            syntax = Some((captures, highlight_maps));
-            for entry in self.diagnostics_in_range::<_, usize>(range.clone(), false) {
-                diagnostic_endpoints.push(DiagnosticEndpoint {
-                    offset: entry.range.start,
-                    is_start: true,
-                    severity: entry.diagnostic.severity,
-                    is_unnecessary: entry.diagnostic.is_unnecessary,
-                });
-                diagnostic_endpoints.push(DiagnosticEndpoint {
-                    offset: entry.range.end,
-                    is_start: false,
-                    severity: entry.diagnostic.severity,
-                    is_unnecessary: entry.diagnostic.is_unnecessary,
-                });
-            }
-            diagnostic_endpoints
-                .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
+            syntax = Some(self.get_highlights(range.clone()));
         }
 
-        BufferChunks::new(self.text.as_rope(), range, syntax, diagnostic_endpoints)
+        BufferChunks::new(self.text.as_rope(), range, syntax, Some(self))
     }
 
     /// Invokes the given callback for each line of text in the given range of the buffer.
@@ -2936,7 +2923,7 @@ impl BufferSnapshot {
             }
 
             let mut offset = buffer_range.start;
-            chunks.seek(offset);
+            chunks.seek(buffer_range.clone());
             for mut chunk in chunks.by_ref() {
                 if chunk.text.len() > buffer_range.end - offset {
                     chunk.text = &chunk.text[0..(buffer_range.end - offset)];
@@ -3731,7 +3718,7 @@ impl<'a> BufferChunks<'a> {
         text: &'a Rope,
         range: Range<usize>,
         syntax: Option<(SyntaxMapCaptures<'a>, Vec<HighlightMap>)>,
-        diagnostic_endpoints: Vec<DiagnosticEndpoint>,
+        buffer_snapshot: Option<&'a BufferSnapshot>,
     ) -> Self {
         let mut highlights = None;
         if let Some((captures, highlight_maps)) = syntax {
@@ -3743,11 +3730,12 @@ impl<'a> BufferChunks<'a> {
             })
         }
 
-        let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
+        let diagnostic_endpoints = Vec::new().into_iter().peekable();
         let chunks = text.chunks_in_range(range.clone());
 
-        BufferChunks {
+        let mut this = BufferChunks {
             range,
+            buffer_snapshot,
             chunks,
             diagnostic_endpoints,
             error_depth: 0,
@@ -3756,30 +3744,72 @@ impl<'a> BufferChunks<'a> {
             hint_depth: 0,
             unnecessary_depth: 0,
             highlights,
-        }
+        };
+        this.initialize_diagnostic_endpoints();
+        this
     }
 
     /// Seeks to the given byte offset in the buffer.
-    pub fn seek(&mut self, offset: usize) {
-        self.range.start = offset;
-        self.chunks.seek(self.range.start);
+    pub fn seek(&mut self, range: Range<usize>) {
+        let old_range = std::mem::replace(&mut self.range, range.clone());
+        self.chunks.set_range(self.range.clone());
         if let Some(highlights) = self.highlights.as_mut() {
-            highlights
-                .stack
-                .retain(|(end_offset, _)| *end_offset > offset);
-            if let Some(capture) = &highlights.next_capture {
-                if offset >= capture.node.start_byte() {
-                    let next_capture_end = capture.node.end_byte();
-                    if offset < next_capture_end {
-                        highlights.stack.push((
-                            next_capture_end,
-                            highlights.highlight_maps[capture.grammar_index].get(capture.index),
-                        ));
+            if old_range.start >= self.range.start && old_range.end <= self.range.end {
+                //Β Reuse existing highlights stack, as the new range is a subrange of the old one.
+                highlights
+                    .stack
+                    .retain(|(end_offset, _)| *end_offset > range.start);
+                if let Some(capture) = &highlights.next_capture {
+                    if range.start >= capture.node.start_byte() {
+                        let next_capture_end = capture.node.end_byte();
+                        if range.start < next_capture_end {
+                            highlights.stack.push((
+                                next_capture_end,
+                                highlights.highlight_maps[capture.grammar_index].get(capture.index),
+                            ));
+                        }
+                        highlights.next_capture.take();
                     }
-                    highlights.next_capture.take();
                 }
+            } else if let Some(snapshot) = self.buffer_snapshot {
+                let (captures, highlight_maps) = snapshot.get_highlights(self.range.clone());
+                *highlights = BufferChunkHighlights {
+                    captures,
+                    next_capture: None,
+                    stack: Default::default(),
+                    highlight_maps,
+                };
+            } else {
+                // We cannot obtain new highlights for a language-aware buffer iterator, as we don't have a buffer snapshot.
+                // Seeking such BufferChunks is not supported.
+                debug_assert!(false, "Attempted to seek on a language-aware buffer iterator without associated buffer snapshot");
             }
+
             highlights.captures.set_byte_range(self.range.clone());
+            self.initialize_diagnostic_endpoints();
+        }
+    }
+
+    fn initialize_diagnostic_endpoints(&mut self) {
+        if let Some(buffer) = self.buffer_snapshot {
+            let mut diagnostic_endpoints = Vec::new();
+            for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) {
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: entry.range.start,
+                    is_start: true,
+                    severity: entry.diagnostic.severity,
+                    is_unnecessary: entry.diagnostic.is_unnecessary,
+                });
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: entry.range.end,
+                    is_start: false,
+                    severity: entry.diagnostic.severity,
+                    is_unnecessary: entry.diagnostic.is_unnecessary,
+                });
+            }
+            diagnostic_endpoints
+                .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
+            self.diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
         }
     }
 

crates/language/src/language.rs πŸ”—

@@ -1341,7 +1341,7 @@ impl Language {
                 });
             let highlight_maps = vec![grammar.highlight_map()];
             let mut offset = 0;
-            for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) {
+            for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), None) {
                 let end_offset = offset + chunk.text.len();
                 if let Some(highlight_id) = chunk.syntax_highlight_id {
                     if !highlight_id.is_default() {

crates/multi_buffer/src/multi_buffer.rs πŸ”—

@@ -2425,7 +2425,7 @@ impl MultiBufferSnapshot {
             excerpt_chunks: None,
             language_aware,
         };
-        chunks.seek(range.start);
+        chunks.seek(range);
         chunks
     }
 
@@ -4164,10 +4164,19 @@ impl Excerpt {
         }
     }
 
-    fn seek_chunks(&self, excerpt_chunks: &mut ExcerptChunks, offset: usize) {
+    fn seek_chunks(&self, excerpt_chunks: &mut ExcerptChunks, range: Range<usize>) {
         let content_start = self.range.context.start.to_offset(&self.buffer);
-        let chunks_start = content_start + offset;
-        excerpt_chunks.content_chunks.seek(chunks_start);
+        let chunks_start = content_start + range.start;
+        let chunks_end = content_start + cmp::min(range.end, self.text_summary.len);
+        excerpt_chunks.content_chunks.seek(chunks_start..chunks_end);
+        excerpt_chunks.footer_height = if self.has_trailing_newline
+            && range.start <= self.text_summary.len
+            && range.end > self.text_summary.len
+        {
+            1
+        } else {
+            0
+        };
     }
 
     fn bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
@@ -4504,9 +4513,9 @@ impl<'a> MultiBufferChunks<'a> {
         self.range.start
     }
 
-    pub fn seek(&mut self, offset: usize) {
-        self.range.start = offset;
-        self.excerpts.seek(&offset, Bias::Right, &());
+    pub fn seek(&mut self, new_range: Range<usize>) {
+        self.range = new_range.clone();
+        self.excerpts.seek(&new_range.start, Bias::Right, &());
         if let Some(excerpt) = self.excerpts.item() {
             let excerpt_start = self.excerpts.start();
             if let Some(excerpt_chunks) = self
@@ -4514,7 +4523,10 @@ impl<'a> MultiBufferChunks<'a> {
                 .as_mut()
                 .filter(|chunks| excerpt.id == chunks.excerpt_id)
             {
-                excerpt.seek_chunks(excerpt_chunks, self.range.start - excerpt_start);
+                excerpt.seek_chunks(
+                    excerpt_chunks,
+                    self.range.start - excerpt_start..self.range.end - excerpt_start,
+                );
             } else {
                 self.excerpt_chunks = Some(excerpt.chunks_in_range(
                     self.range.start - excerpt_start..self.range.end - excerpt_start,

crates/rope/src/rope.rs πŸ”—

@@ -615,6 +615,11 @@ impl<'a> Chunks<'a> {
         self.offset = offset;
     }
 
+    pub fn set_range(&mut self, range: Range<usize>) {
+        self.range = range.clone();
+        self.seek(range.start);
+    }
+
     /// Moves this cursor to the start of the next line in the rope.
     ///
     /// This method advances the cursor to the beginning of the next line.