Scroll project search results to the top (#8329)

Andrew Lygin created

Scroll project search results to the top after every new search.

Release Notes:

- Fixed autoscrolling of the project search results ([8237](https://github.com/zed-industries/zed/issues/8237))

Change summary

crates/search/src/project_search.rs | 115 ++++++++++++++++++++++++++----
1 file changed, 99 insertions(+), 16 deletions(-)

Detailed changes

crates/search/src/project_search.rs 🔗

@@ -7,16 +7,18 @@ use crate::{
 use anyhow::{Context as _, Result};
 use collections::HashMap;
 use editor::{
-    actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor, EditorEvent,
-    MultiBuffer, MAX_TAB_TITLE_LEN,
+    actions::SelectAll,
+    items::active_match_index,
+    scroll::{Autoscroll, Axis},
+    Anchor, Editor, EditorEvent, MultiBuffer, MAX_TAB_TITLE_LEN,
 };
 use editor::{EditorElement, EditorStyle};
 use gpui::{
     actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
     EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global, Hsla,
-    InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, PromptLevel,
-    Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
-    WeakModel, WeakView, WhiteSpace, WindowContext,
+    InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, Point,
+    PromptLevel, Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext,
+    VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext,
 };
 use menu::Confirm;
 use project::{
@@ -1302,6 +1304,7 @@ impl ProjectSearchView {
                     editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                         s.select_ranges(range_to_select)
                     });
+                    editor.scroll(Point::default(), Some(Axis::Vertical), cx);
                 }
                 editor.highlight_background::<Self>(
                     match_ranges,
@@ -2094,11 +2097,12 @@ fn register_workspace_action_for_present_search<A: Action>(
 pub mod tests {
     use super::*;
     use editor::DisplayPoint;
-    use gpui::{Action, TestAppContext};
+    use gpui::{Action, TestAppContext, WindowHandle};
     use project::FakeFs;
     use semantic_index::semantic_index_settings::SemanticIndexSettings;
     use serde_json::json;
     use settings::{Settings, SettingsStore};
+    use std::sync::Arc;
     use workspace::DeploySearch;
 
     #[gpui::test]
@@ -2120,15 +2124,7 @@ pub mod tests {
         let search = cx.new_model(|cx| ProjectSearch::new(project, cx));
         let search_view = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx, None));
 
-        search_view
-            .update(cx, |search_view, cx| {
-                search_view
-                    .query_editor
-                    .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
-                search_view.search(cx);
-            })
-            .unwrap();
-        cx.background_executor.run_until_parked();
+        perform_search(search_view, "TWO", cx);
         search_view.update(cx, |search_view, cx| {
             assert_eq!(
                 search_view
@@ -3377,7 +3373,78 @@ pub mod tests {
             .unwrap();
     }
 
-    pub fn init_test(cx: &mut TestAppContext) {
+    #[gpui::test]
+    async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        // We need many lines in the search results to be able to scroll the window
+        let fs = FakeFs::new(cx.background_executor.clone());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "1.txt": "\n\n\n\n\n A \n\n\n\n\n",
+                "2.txt": "\n\n\n\n\n A \n\n\n\n\n",
+                "3.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "4.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "5.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "6.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "7.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "8.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "9.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "a.rs": "\n\n\n\n\n A \n\n\n\n\n",
+                "b.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "c.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "d.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "e.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "f.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "g.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "h.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "i.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "j.rs": "\n\n\n\n\n B \n\n\n\n\n",
+                "k.rs": "\n\n\n\n\n B \n\n\n\n\n",
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let search = cx.new_model(|cx| ProjectSearch::new(project, cx));
+        let search_view = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx, None));
+
+        // First search
+        perform_search(search_view.clone(), "A", cx);
+        search_view
+            .update(cx, |search_view, cx| {
+                search_view.results_editor.update(cx, |results_editor, cx| {
+                    // Results are correct and scrolled to the top
+                    assert_eq!(
+                        results_editor.display_text(cx).match_indices(" A ").count(),
+                        10
+                    );
+                    assert_eq!(results_editor.scroll_position(cx), Point::default());
+
+                    // Scroll results all the way down
+                    results_editor.scroll(Point::new(0., f32::MAX), Some(Axis::Vertical), cx);
+                });
+            })
+            .expect("unable to update search view");
+
+        // Second search
+        perform_search(search_view.clone(), "B", cx);
+        search_view
+            .update(cx, |search_view, cx| {
+                search_view.results_editor.update(cx, |results_editor, cx| {
+                    // Results are correct...
+                    assert_eq!(
+                        results_editor.display_text(cx).match_indices(" B ").count(),
+                        10
+                    );
+                    // ...and scrolled back to the top
+                    assert_eq!(results_editor.scroll_position(cx), Point::default());
+                });
+            })
+            .expect("unable to update search view");
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
@@ -3394,4 +3461,20 @@ pub mod tests {
             super::init(cx);
         });
     }
+
+    fn perform_search(
+        search_view: WindowHandle<ProjectSearchView>,
+        text: impl Into<Arc<str>>,
+        cx: &mut TestAppContext,
+    ) {
+        search_view
+            .update(cx, |search_view, cx| {
+                search_view
+                    .query_editor
+                    .update(cx, |query_editor, cx| query_editor.set_text(text, cx));
+                search_view.search(cx);
+            })
+            .unwrap();
+        cx.background_executor.run_until_parked();
+    }
 }