project search: Reduce hangs on main thread (#39857)

Piotr Osiewicz and Smit Barmase created

This takes the idea that @RemcoSmitsDev started on in
https://github.com/zed-industries/zed/pull/39354. We did away with
grabbing a snapshot of the display map when buffer coordinates were
sufficient.
Closes #37267

Release Notes:

- Reduced micro-stutters in project search with large multi-buffer
contents.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

crates/editor/src/editor.rs                     | 58 +++++++++++-------
crates/editor/src/highlight_matching_bracket.rs | 31 ++++-----
crates/editor/src/selections_collection.rs      | 21 ++++++
crates/go_to_line/src/cursor_position.rs        |  4 
crates/multi_buffer/src/multi_buffer.rs         | 11 +++
5 files changed, 84 insertions(+), 41 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -3172,7 +3172,7 @@ impl Editor {
             self.refresh_code_actions(window, cx);
             self.refresh_document_highlights(cx);
             self.refresh_selected_text_highlights(false, window, cx);
-            refresh_matching_bracket_highlights(self, window, cx);
+            refresh_matching_bracket_highlights(self, cx);
             self.update_visible_edit_prediction(window, cx);
             self.edit_prediction_requires_modifier_in_indent_conflict = true;
             linked_editing_ranges::refresh_linked_ranges(self, window, cx);
@@ -6607,26 +6607,32 @@ impl Editor {
         &self.context_menu
     }
 
-    fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> {
-        let newest_selection = self.selections.newest_anchor().clone();
-        let newest_selection_adjusted = self.selections.newest_adjusted(cx);
-        let buffer = self.buffer.read(cx);
-        if newest_selection.head().diff_base_anchor.is_some() {
-            return None;
-        }
-        let (start_buffer, start) =
-            buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
-        let (end_buffer, end) =
-            buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
-        if start_buffer != end_buffer {
-            return None;
-        }
-
+    fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| {
             cx.background_executor()
                 .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
                 .await;
 
+            let (start_buffer, start, _, end, newest_selection) = this
+                .update(cx, |this, cx| {
+                    let newest_selection = this.selections.newest_anchor().clone();
+                    if newest_selection.head().diff_base_anchor.is_some() {
+                        return None;
+                    }
+                    let newest_selection_adjusted = this.selections.newest_adjusted(cx);
+                    let buffer = this.buffer.read(cx);
+
+                    let (start_buffer, start) =
+                        buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
+                    let (end_buffer, end) =
+                        buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
+
+                    Some((start_buffer, start, end_buffer, end, newest_selection))
+                })?
+                .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)
+                .context(
+                    "Expected selection to lie in a single buffer when refreshing code actions",
+                )?;
             let (providers, tasks) = this.update_in(cx, |this, window, cx| {
                 let providers = this.code_action_providers.clone();
                 let tasks = this
@@ -6667,7 +6673,6 @@ impl Editor {
                 cx.notify();
             })
         }));
-        None
     }
 
     fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -6917,19 +6922,24 @@ impl Editor {
         if self.selections.count() != 1 || self.selections.line_mode() {
             return None;
         }
-        let selection = self.selections.newest::<Point>(cx);
-        if selection.is_empty() || selection.start.row != selection.end.row {
+        let selection = self.selections.newest_anchor();
+        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+        let selection_point_range = selection.start.to_point(&multi_buffer_snapshot)
+            ..selection.end.to_point(&multi_buffer_snapshot);
+        // If the selection spans multiple rows OR it is empty
+        if selection_point_range.start.row != selection_point_range.end.row
+            || selection_point_range.start.column == selection_point_range.end.column
+        {
             return None;
         }
-        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
-        let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot);
+
         let query = multi_buffer_snapshot
-            .text_for_range(selection_anchor_range.clone())
+            .text_for_range(selection.range())
             .collect::<String>();
         if query.trim().is_empty() {
             return None;
         }
-        Some((query, selection_anchor_range))
+        Some((query, selection.range()))
     }
 
     fn update_selection_occurrence_highlights(
@@ -20805,7 +20815,7 @@ impl Editor {
                 self.refresh_code_actions(window, cx);
                 self.refresh_selected_text_highlights(true, window, cx);
                 self.refresh_single_line_folds(window, cx);
-                refresh_matching_bracket_highlights(self, window, cx);
+                refresh_matching_bracket_highlights(self, cx);
                 if self.has_active_edit_prediction() {
                     self.update_visible_edit_prediction(window, cx);
                 }

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -1,47 +1,46 @@
 use crate::{Editor, RangeToAnchorExt};
-use gpui::{Context, HighlightStyle, Window};
+use gpui::{Context, HighlightStyle};
 use language::CursorShape;
+use multi_buffer::ToOffset;
 use theme::ActiveTheme;
 
 enum MatchingBracketHighlight {}
 
-pub fn refresh_matching_bracket_highlights(
-    editor: &mut Editor,
-    window: &mut Window,
-    cx: &mut Context<Editor>,
-) {
+pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut Context<Editor>) {
     editor.clear_highlights::<MatchingBracketHighlight>(cx);
 
-    let newest_selection = editor.selections.newest::<usize>(cx);
+    let buffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+    let newest_selection = editor
+        .selections
+        .newest_anchor()
+        .map(|anchor| anchor.to_offset(&buffer_snapshot));
     // Don't highlight brackets if the selection isn't empty
     if !newest_selection.is_empty() {
         return;
     }
 
-    let snapshot = editor.snapshot(window, cx);
     let head = newest_selection.head();
-    if head > snapshot.buffer_snapshot().len() {
+    if head > buffer_snapshot.len() {
         log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
         return;
     }
 
     let mut tail = head;
     if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow)
-        && head < snapshot.buffer_snapshot().len()
+        && head < buffer_snapshot.len()
     {
-        if let Some(tail_ch) = snapshot.buffer_snapshot().chars_at(tail).next() {
+        if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
             tail += tail_ch.len_utf8();
         }
     }
 
-    if let Some((opening_range, closing_range)) = snapshot
-        .buffer_snapshot()
-        .innermost_enclosing_bracket_ranges(head..tail, None)
+    if let Some((opening_range, closing_range)) =
+        buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
     {
         editor.highlight_text::<MatchingBracketHighlight>(
             vec![
-                opening_range.to_anchors(&snapshot.buffer_snapshot()),
-                closing_range.to_anchors(&snapshot.buffer_snapshot()),
+                opening_range.to_anchors(&buffer_snapshot),
+                closing_range.to_anchors(&buffer_snapshot),
             ],
             HighlightStyle {
                 background_color: Some(

crates/editor/src/selections_collection.rs 🔗

@@ -184,6 +184,27 @@ impl SelectionsCollection {
         selections
     }
 
+    /// Returns all of the selections, adjusted to take into account the selection line_mode. Uses a provided snapshot to resolve selections.
+    pub fn all_adjusted_with_snapshot(
+        &self,
+        snapshot: &MultiBufferSnapshot,
+    ) -> Vec<Selection<Point>> {
+        let mut selections = self
+            .disjoint
+            .iter()
+            .chain(self.pending_anchor())
+            .map(|anchor| anchor.map(|anchor| anchor.to_point(&snapshot)))
+            .collect::<Vec<_>>();
+        if self.line_mode {
+            for selection in &mut selections {
+                let new_range = snapshot.expand_to_line(selection.range());
+                selection.start = new_range.start;
+                selection.end = new_range.end;
+            }
+        }
+        selections
+    }
+
     /// Returns the newest selection, adjusted to take into account the selection line_mode
     pub fn newest_adjusted(&self, cx: &mut App) -> Selection<Point> {
         let mut selection = self.newest::<Point>(cx);

crates/go_to_line/src/cursor_position.rs 🔗

@@ -113,7 +113,9 @@ impl CursorPosition {
                                 let mut last_selection = None::<Selection<Point>>;
                                 let snapshot = editor.buffer().read(cx).snapshot(cx);
                                 if snapshot.excerpts().count() > 0 {
-                                    for selection in editor.selections.all_adjusted(cx) {
+                                    for selection in
+                                        editor.selections.all_adjusted_with_snapshot(&snapshot)
+                                    {
                                         let selection_summary = snapshot
                                             .text_summary_for_range::<text::TextSummary, _>(
                                                 selection.start..selection.end,

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -6385,6 +6385,17 @@ impl MultiBufferSnapshot {
             debug_ranges.insert(key, text_ranges, format!("{value:?}").into())
         });
     }
+
+    // used by line_mode selections and tries to match vim behavior
+    pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
+        let new_start = MultiBufferPoint::new(range.start.row, 0);
+        let new_end = if range.end.column > 0 {
+            MultiBufferPoint::new(range.end.row, self.line_len(MultiBufferRow(range.end.row)))
+        } else {
+            range.end
+        };
+        new_start..new_end
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]