Do less work when revealing entries in the outline panel (#20031)

Kirill Bulatov created

Before this change, we were trying to determine current element before
debouncing, causing a lot of extra work on caret movement. Now, we only
do this for the task that managed to wait the entire debounce period.

Closes https://github.com/zed-industries/zed/issues/19817
Closes https://github.com/zed-industries/zed/issues/14235

Release Notes:

- Fixed outline panel-related performance issues when selections change
in the large document
([#19817](https://github.com/zed-industries/zed/issues/19817)),
([#14235](https://github.com/zed-industries/zed/issues/14235))

Change summary

crates/outline_panel/src/outline_panel.rs | 45 +++++++++++++++----------
1 file changed, 27 insertions(+), 18 deletions(-)

Detailed changes

crates/outline_panel/src/outline_panel.rs 🔗

@@ -1420,26 +1420,26 @@ impl OutlinePanel {
         }
     }
 
-    fn reveal_entry_for_selection(
-        &mut self,
-        editor: &View<Editor>,
-        cx: &mut ViewContext<'_, Self>,
-    ) {
+    fn reveal_entry_for_selection(&mut self, editor: View<Editor>, cx: &mut ViewContext<'_, Self>) {
         if !self.active {
             return;
         }
         if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
             return;
         }
-        let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
-            self.selected_entry = SelectedEntry::None;
-            cx.notify();
-            return;
-        };
-
         let project = self.project.clone();
         self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move {
             cx.background_executor().timer(UPDATE_DEBOUNCE).await;
+            let entry_with_selection = outline_panel.update(&mut cx, |outline_panel, cx| {
+                outline_panel.location_for_editor_selection(&editor, cx)
+            })?;
+            let Some(entry_with_selection) = entry_with_selection else {
+                outline_panel.update(&mut cx, |outline_panel, cx| {
+                    outline_panel.selected_entry = SelectedEntry::None;
+                    cx.notify();
+                })?;
+                return Ok(());
+            };
             let related_buffer_entry = match &entry_with_selection {
                 PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
                     project.update(&mut cx, |project, cx| {
@@ -2406,7 +2406,7 @@ impl OutlinePanel {
     }
 
     fn location_for_editor_selection(
-        &mut self,
+        &self,
         editor: &View<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Option<PanelEntry> {
@@ -2472,7 +2472,7 @@ impl OutlinePanel {
     }
 
     fn outline_location(
-        &mut self,
+        &self,
         buffer_id: BufferId,
         excerpt_id: ExcerptId,
         multi_buffer_snapshot: editor::MultiBufferSnapshot,
@@ -4056,7 +4056,7 @@ fn subscribe_for_editor_events(
         editor,
         move |outline_panel, editor, e: &EditorEvent, cx| match e {
             EditorEvent::SelectionsChanged { local: true } => {
-                outline_panel.reveal_entry_for_selection(&editor, cx);
+                outline_panel.reveal_entry_for_selection(editor, cx);
                 cx.notify();
             }
             EditorEvent::ExcerptsAdded { excerpts, .. } => {
@@ -4199,7 +4199,13 @@ mod tests {
         cx.executor()
             .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
         cx.run_until_parked();
-        outline_panel.update(cx, |outline_panel, _| {
+        outline_panel.update(cx, |outline_panel, cx| {
+            // Project search re-adds items to the buffer, removing the caret from it.
+            // Select the first entry and move 4 elements down.
+            for _ in 0..6 {
+                outline_panel.select_next(&SelectNext, cx);
+            }
+
             assert_eq!(
                 display_entries(
                     &outline_panel.cached_entries,
@@ -4515,7 +4521,7 @@ mod tests {
                 r#"/
   public/lottie/
     syntax-tree.json
-      search: { "something": "static" }  <==== selected
+      search: { "something": "static" }
   src/
     app/(site)/
       (about)/jobs/[slug]/
@@ -4531,8 +4537,11 @@ mod tests {
         });
 
         outline_panel.update(cx, |outline_panel, cx| {
-            outline_panel.select_next(&SelectNext, cx);
-            outline_panel.select_next(&SelectNext, cx);
+            // After the search is done, we have updated the outline panel contents and caret is not in any excerot, so there are no selections.
+            // Move to 5th element in the list (0th action will selection the first element)
+            for _ in 0..6 {
+                outline_panel.select_next(&SelectNext, cx);
+            }
             outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
         });
         cx.run_until_parked();