editor: Move runnables querying to background thread (#11487)

Piotr Osiewicz created

Originally reported by @mrnugget and @bennetbo 
Also, instead of requerying them every frame, we do so whenever buffer
changes.

As a bonus, I modified tree-sitter query for Rust tests.

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs             | 90 ++++++++++++++++++++------
crates/editor/src/element.rs            | 37 ++++------
crates/languages/src/rust/runnables.scm |  2 
3 files changed, 83 insertions(+), 46 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -505,6 +505,7 @@ pub struct Editor {
     last_bounds: Option<Bounds<Pixels>>,
     expect_bounds_change: Option<Bounds<Pixels>>,
     tasks: HashMap<u32, RunnableTasks>,
+    tasks_update_task: Option<Task<()>>,
 }
 
 #[derive(Clone)]
@@ -1688,8 +1689,9 @@ impl Editor {
                     });
                 }),
             ],
+            tasks_update_task: None,
         };
-
+        this.tasks_update_task = Some(this.refresh_runnables(cx));
         this._subscriptions.extend(project_subscriptions);
 
         this.end_selection(cx);
@@ -7687,25 +7689,68 @@ impl Editor {
         self.select_larger_syntax_node_stack = stack;
     }
 
-    fn runnable_display_rows(
-        &self,
-        range: Range<Anchor>,
+    fn refresh_runnables(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
+        let project = self.project.clone();
+        cx.spawn(|this, mut cx| async move {
+            let Ok(display_snapshot) = this.update(&mut cx, |this, cx| {
+                this.display_map.update(cx, |map, cx| map.snapshot(cx))
+            }) else {
+                return;
+            };
+
+            let Some(project) = project else {
+                return;
+            };
+            if project
+                .update(&mut cx, |this, _| this.is_remote())
+                .unwrap_or(true)
+            {
+                // Do not display any test indicators in remote projects.
+                return;
+            }
+            let new_rows =
+                cx.background_executor()
+                    .spawn({
+                        let snapshot = display_snapshot.clone();
+                        async move {
+                            Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max())
+                        }
+                    })
+                    .await;
+            let rows = Self::refresh_runnable_display_rows(
+                project,
+                display_snapshot,
+                new_rows,
+                cx.clone(),
+            );
+
+            this.update(&mut cx, |this, _| {
+                this.clear_tasks();
+                for (row, tasks) in rows {
+                    this.insert_tasks(row, tasks);
+                }
+            })
+            .ok();
+        })
+    }
+    fn fetch_runnable_ranges(
         snapshot: &DisplaySnapshot,
-        cx: &WindowContext,
+        range: Range<Anchor>,
+    ) -> Vec<(Range<usize>, Runnable)> {
+        snapshot.buffer_snapshot.runnable_ranges(range).collect()
+    }
+    fn refresh_runnable_display_rows(
+        project: Model<Project>,
+        snapshot: DisplaySnapshot,
+        runnable_ranges: Vec<(Range<usize>, Runnable)>,
+        mut cx: AsyncWindowContext,
     ) -> Vec<(u32, RunnableTasks)> {
-        if self
-            .project
-            .as_ref()
-            .map_or(false, |project| project.read(cx).is_remote())
-        {
-            // Do not display any test indicators in remote projects.
-            return vec![];
-        }
-        snapshot
-            .buffer_snapshot
-            .runnable_ranges(range)
+        runnable_ranges
+            .into_iter()
             .filter_map(|(multi_buffer_range, mut runnable)| {
-                let (tasks, _) = self.resolve_runnable(&mut runnable, cx);
+                let (tasks, _) = cx
+                    .update(|cx| Self::resolve_runnable(project.clone(), &mut runnable, cx))
+                    .ok()?;
                 if tasks.is_empty() {
                     return None;
                 }
@@ -7722,13 +7767,10 @@ impl Editor {
     }
 
     fn resolve_runnable(
-        &self,
+        project: Model<Project>,
         runnable: &mut Runnable,
         cx: &WindowContext<'_>,
     ) -> (Vec<(TaskSourceKind, TaskTemplate)>, Option<WorktreeId>) {
-        let Some(project) = self.project.as_ref() else {
-            return Default::default();
-        };
         let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
             let worktree_id = project
                 .buffer_for_id(runnable.buffer)
@@ -10070,7 +10112,11 @@ impl Editor {
                 self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
                 cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
             }
-            multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed),
+            multi_buffer::Event::Reparsed => {
+                self.tasks_update_task = Some(self.refresh_runnables(cx));
+
+                cx.emit(EditorEvent::Reparsed);
+            }
             multi_buffer::Event::LanguageChanged => {
                 cx.emit(EditorEvent::Reparsed);
                 cx.notify();

crates/editor/src/element.rs 🔗

@@ -15,8 +15,8 @@ use crate::{
     CodeActionsMenu, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite,
     Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts,
     GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp,
-    OpenExcerpts, PageDown, PageUp, Point, RunnableTasks, SelectPhase, Selection, SoftWrap,
-    ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
+    OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
+    CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
 };
 use anyhow::Result;
 use client::ParticipantIndex;
@@ -1377,7 +1377,6 @@ impl EditorElement {
 
     fn layout_run_indicators(
         &self,
-        task_lines: Vec<(u32, RunnableTasks)>,
         line_height: Pixels,
         scroll_pixel_position: gpui::Point<Pixels>,
         gutter_dimensions: &GutterDimensions,
@@ -1385,8 +1384,6 @@ impl EditorElement {
         cx: &mut WindowContext,
     ) -> Vec<AnyElement> {
         self.editor.update(cx, |editor, cx| {
-            editor.clear_tasks();
-
             let active_task_indicator_row =
                 if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
                     deployed_from_indicator,
@@ -1402,21 +1399,20 @@ impl EditorElement {
                 } else {
                     None
                 };
-            task_lines
-                .into_iter()
-                .map(|(row, tasks)| {
-                    editor.insert_tasks(row, tasks);
-
+            editor
+                .tasks
+                .keys()
+                .map(|row| {
                     let button = editor.render_run_indicator(
                         &self.style,
-                        Some(row) == active_task_indicator_row,
-                        row,
+                        Some(*row) == active_task_indicator_row,
+                        *row,
                         cx,
                     );
 
                     let button = prepaint_gutter_button(
                         button,
-                        row,
+                        *row,
                         line_height,
                         gutter_dimensions,
                         scroll_pixel_position,
@@ -3835,12 +3831,6 @@ impl Element for EditorElement {
                     cx,
                 );
 
-                let test_lines = self.editor.read(cx).runnable_display_rows(
-                    start_anchor..end_anchor,
-                    &snapshot.display_snapshot,
-                    cx,
-                );
-
                 let (selections, active_rows, newest_selection_head) = self.layout_selections(
                     start_anchor,
                     end_anchor,
@@ -4030,9 +4020,11 @@ impl Element for EditorElement {
                             cx,
                         );
                         if gutter_settings.code_actions {
-                            let has_test_indicator = test_lines
-                                .iter()
-                                .any(|(line, _)| *line == newest_selection_head.row());
+                            let has_test_indicator = self
+                                .editor
+                                .read(cx)
+                                .tasks
+                                .contains_key(&newest_selection_head.row());
                             if !has_test_indicator {
                                 code_actions_indicator = self.layout_code_actions_indicator(
                                     line_height,
@@ -4048,7 +4040,6 @@ impl Element for EditorElement {
                 }
 
                 let test_indicators = self.layout_run_indicators(
-                    test_lines,
                     line_height,
                     scroll_pixel_position,
                     &gutter_dimensions,