project search: Render results in batches (#16960)

Thorsten Ball created

This improves performance, because we don't render after every single
match/range that was added to the results.

I think for my example search it's twice as fast?

## Numbers

Recorded in debug mode (because it's 6:30pm and my poor computer has
spun its fan enough for today and also because you can see the change of
the effect more in debug mode), rendering `<` searched in `zed.dev`
repo:

- Before: `14.59558225s`
- After: `2.604320875s`




## Videos
Before (recorded in release mode):



https://github.com/user-attachments/assets/909260fa-3e69-49ab-8786-dd384e2a27ee

After (recorded in release mode):



https://github.com/user-attachments/assets/fc8a85d3-e575-470f-b59c-16a6df8b3f80
## Release Notes

Release Notes:

- Improved performance of rendering project-search results in the
multi-buffer after finding them.

Change summary

crates/search/src/project_search.rs | 74 +++++++++++++++++++-----------
1 file changed, 47 insertions(+), 27 deletions(-)

Detailed changes

crates/search/src/project_search.rs 🔗

@@ -221,36 +221,56 @@ impl ProjectSearch {
 
             let mut limit_reached = false;
             while let Some(results) = matches.next().await {
-                for result in results {
-                    match result {
-                        project::search::SearchResult::Buffer { buffer, ranges } => {
-                            let mut match_ranges = this
-                                .update(&mut cx, |this, cx| {
-                                    this.excerpts.update(cx, |excerpts, cx| {
-                                        excerpts.stream_excerpts_with_context_lines(
-                                            buffer,
-                                            ranges,
-                                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
-                                            cx,
-                                        )
-                                    })
-                                })
-                                .ok()?;
-
-                            while let Some(range) = match_ranges.next().await {
-                                this.update(&mut cx, |this, _| {
-                                    this.no_results = Some(false);
-                                    this.match_ranges.push(range)
-                                })
-                                .ok()?;
+                let tasks = results
+                    .into_iter()
+                    .map(|result| {
+                        let this = this.clone();
+
+                        cx.spawn(|mut cx| async move {
+                            match result {
+                                project::search::SearchResult::Buffer { buffer, ranges } => {
+                                    let mut match_ranges_rx =
+                                        this.update(&mut cx, |this, cx| {
+                                            this.excerpts.update(cx, |excerpts, cx| {
+                                                excerpts.stream_excerpts_with_context_lines(
+                                                    buffer,
+                                                    ranges,
+                                                    editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                                                    cx,
+                                                )
+                                            })
+                                        })?;
+
+                                    let mut match_ranges = vec![];
+                                    while let Some(range) = match_ranges_rx.next().await {
+                                        match_ranges.push(range);
+                                    }
+                                    anyhow::Ok((match_ranges, false))
+                                }
+                                project::search::SearchResult::LimitReached => {
+                                    anyhow::Ok((vec![], true))
+                                }
                             }
-                            this.update(&mut cx, |_, cx| cx.notify()).ok()?;
-                        }
-                        project::search::SearchResult::LimitReached => {
-                            limit_reached = true;
-                        }
+                        })
+                    })
+                    .collect::<Vec<_>>();
+
+                let result_ranges = futures::future::join_all(tasks).await;
+                let mut combined_ranges = vec![];
+                for (ranges, result_limit_reached) in result_ranges.into_iter().flatten() {
+                    combined_ranges.extend(ranges);
+                    if result_limit_reached {
+                        limit_reached = result_limit_reached;
                     }
                 }
+                this.update(&mut cx, |this, cx| {
+                    if !combined_ranges.is_empty() {
+                        this.no_results = Some(false);
+                        this.match_ranges.extend(combined_ranges);
+                        cx.notify();
+                    }
+                })
+                .ok()?;
             }
 
             this.update(&mut cx, |this, cx| {