editor: Improve `apply_fetched_hints` for many excerpt multibuffers (#49444)

Lukas Wirth created

Reuses the new multi buffer api from
https://github.com/zed-industries/zed/pull/49349 for inlay hints

Release Notes:

- Reduced occasional stuttering coming from inlay hints in large
multibuffers

Change summary

crates/editor/src/bracket_colorization.rs | 11 ------
crates/editor/src/editor.rs               |  6 ++--
crates/editor/src/inlays/inlay_hints.rs   | 37 ++++++++++++++++--------
crates/editor/src/split.rs                |  2 
4 files changed, 29 insertions(+), 27 deletions(-)

Detailed changes

crates/editor/src/bracket_colorization.rs 🔗

@@ -24,20 +24,11 @@ impl Editor {
 
         let accents_count = cx.theme().accents().0.len();
         let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
-        let all_excerpts = self.buffer().read(cx).excerpt_ids();
         let anchors_in_multi_buffer = |current_excerpt: ExcerptId,
                                        text_anchors: [text::Anchor; 4]|
          -> Option<[Option<_>; 4]> {
             multi_buffer_snapshot
-                .anchors_in_excerpt(current_excerpt, text_anchors)
-                .or_else(|| {
-                    all_excerpts
-                        .iter()
-                        .filter(|&&excerpt_id| excerpt_id != current_excerpt)
-                        .find_map(|&excerpt_id| {
-                            multi_buffer_snapshot.anchors_in_excerpt(excerpt_id, text_anchors)
-                        })
-                })?
+                .anchors_in_excerpt(current_excerpt, text_anchors)?
                 .collect_array()
         };
 

crates/editor/src/editor.rs 🔗

@@ -17403,12 +17403,12 @@ impl Editor {
         };
 
         let snapshot = self.buffer.read(cx).snapshot(cx);
-        let mut excerpt_ids = selections
+        let excerpt_ids = selections
             .iter()
             .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range()))
+            .unique()
+            .sorted()
             .collect::<Vec<_>>();
-        excerpt_ids.sort();
-        excerpt_ids.dedup();
 
         if self.delegate_expand_excerpts {
             cx.emit(EditorEvent::ExpandExcerptsRequested {

crates/editor/src/inlays/inlay_hints.rs 🔗

@@ -8,6 +8,7 @@ use clock::Global;
 use collections::{HashMap, HashSet};
 use futures::future::join_all;
 use gpui::{App, Entity, Task};
+use itertools::Itertools;
 use language::{
     BufferRow,
     language_settings::{InlayHintKind, InlayHintSettings, language_settings},
@@ -772,8 +773,17 @@ impl Editor {
             return;
         };
 
-        let mut hints_to_remove = Vec::new();
         let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+        let Some(buffer_snapshot) = self
+            .buffer
+            .read(cx)
+            .buffer(buffer_id)
+            .map(|buffer| buffer.read(cx).snapshot())
+        else {
+            return;
+        };
+
+        let mut hints_to_remove = Vec::new();
 
         // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there,
         // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache.
@@ -786,9 +796,8 @@ impl Editor {
             hints_to_remove.extend(visible_inlay_hint_ids);
         }
 
-        let excerpts = self.buffer.read(cx).excerpt_ids();
         let mut inserted_hint_text = HashMap::default();
-        let hints_to_insert = new_hints
+        let new_hints = new_hints
             .into_iter()
             .filter_map(|(chunk_range, hints_result)| {
                 let chunks_fetched = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id);
@@ -843,22 +852,24 @@ impl Editor {
 
                 hints_deduplicated
             })
-            .filter_map(|(hint_id, lsp_hint)| {
-                if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
+            .filter(|(hint_id, lsp_hint)| {
+                inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
                     && inlay_hints
                         .added_hints
-                        .insert(hint_id, lsp_hint.kind)
+                        .insert(*hint_id, lsp_hint.kind)
                         .is_none()
-                {
-                    let position = excerpts.iter().find_map(|excerpt_id| {
-                        multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, lsp_hint.position)
-                    })?;
-                    return Some(Inlay::hint(hint_id, position, &lsp_hint));
-                }
-                None
             })
+            .sorted_by(|(_, a), (_, b)| a.position.cmp(&b.position, &buffer_snapshot))
             .collect::<Vec<_>>();
 
+        let hints_to_insert = multi_buffer_snapshot
+            .text_anchors_to_visible_anchors(
+                new_hints.iter().map(|(_, lsp_hint)| lsp_hint.position),
+            )
+            .into_iter()
+            .zip(&new_hints)
+            .filter_map(|(position, (hint_id, hint))| Some(Inlay::hint(*hint_id, position?, &hint)))
+            .collect();
         let invalidate_hints_for_buffers =
             std::mem::take(&mut inlay_hints.invalidate_hints_for_buffers);
         if !invalidate_hints_for_buffers.is_empty() {

crates/editor/src/split.rs 🔗

@@ -2254,7 +2254,7 @@ mod tests {
         assert_eq!(lhs_content, expected_lhs, "lhs");
     }
 
-    #[gpui::test(iterations = 100)]
+    #[gpui::test(iterations = 25)]
     async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
         use rand::prelude::*;