search: Improve performance of `replace_all` (#13654)

Piotr Osiewicz created

Previously replace_all amounted to what could be achieved by repeatedly
mashing "Replace" button, which had a bunch of overhead related to
buffer state syncing. This commit gets rid of the automated button
mashing, processing all of the replacements in one go.

Fixes #13455



Release Notes:

- Improved performance of "replace all" in buffer search and project
search

Change summary

crates/editor/src/items.rs          | 29 +++++++++++++++++++++++++++++
crates/search/src/buffer_search.rs  |  4 +---
crates/search/src/project_search.rs |  4 +---
crates/workspace/src/searchable.rs  | 27 +++++++++++++++++++++++++++
4 files changed, 58 insertions(+), 6 deletions(-)

Detailed changes

crates/editor/src/items.rs 🔗

@@ -1102,6 +1102,35 @@ impl SearchableItem for Editor {
             });
         }
     }
+    fn replace_all(
+        &mut self,
+        matches: &mut dyn Iterator<Item = &Self::Match>,
+        query: &SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let text = self.buffer.read(cx);
+        let text = text.snapshot(cx);
+        let mut edits = vec![];
+        for m in matches {
+            let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
+            let text: Cow<_> = if text.len() == 1 {
+                text.first().cloned().unwrap().into()
+            } else {
+                let joined_chunks = text.join("");
+                joined_chunks.into()
+            };
+
+            if let Some(replacement) = query.replacement_for(&text) {
+                edits.push((m.clone(), Arc::from(&*replacement)));
+            }
+        }
+
+        if !edits.is_empty() {
+            self.transact(cx, |this, cx| {
+                this.edit(edits, cx);
+            });
+        }
+    }
     fn match_index_for_direction(
         &mut self,
         matches: &[Range<Anchor>],

crates/search/src/buffer_search.rs 🔗

@@ -1122,9 +1122,7 @@ impl BufferSearchBar {
                             .as_ref()
                             .clone()
                             .with_replacement(self.replacement(cx));
-                        for m in matches {
-                            searchable_item.replace(m, &query, cx);
-                        }
+                        searchable_item.replace_all(&mut matches.iter(), &query, cx);
                     }
                 }
             }

crates/search/src/project_search.rs 🔗

@@ -591,9 +591,7 @@ impl ProjectSearchView {
         }
 
         self.results_editor.update(cx, |editor, cx| {
-            for item in &match_ranges {
-                editor.replace(item, &query, cx);
-            }
+            editor.replace_all(&mut match_ranges.iter(), &query, cx);
         });
 
         self.model.update(cx, |model, _cx| {

crates/workspace/src/searchable.rs 🔗

@@ -71,6 +71,16 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
     fn activate_match(&mut self, index: usize, matches: &[Self::Match], cx: &mut ViewContext<Self>);
     fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
     fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
+    fn replace_all(
+        &mut self,
+        matches: &mut dyn Iterator<Item = &Self::Match>,
+        query: &SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) {
+        for item in matches {
+            self.replace(item, query, cx);
+        }
+    }
     fn match_index_for_direction(
         &mut self,
         matches: &[Self::Match],
@@ -123,6 +133,12 @@ pub trait SearchableItemHandle: ItemHandle {
         _: &SearchQuery,
         _: &mut WindowContext,
     );
+    fn replace_all(
+        &self,
+        matches: &mut dyn Iterator<Item = any_vec::element::ElementRef<'_, dyn Send>>,
+        query: &SearchQuery,
+        cx: &mut WindowContext,
+    );
     fn match_index_for_direction(
         &self,
         matches: &AnyVec<dyn Send>,
@@ -241,6 +257,17 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
         self.update(cx, |this, cx| this.replace(mat, query, cx))
     }
 
+    fn replace_all(
+        &self,
+        matches: &mut dyn Iterator<Item = any_vec::element::ElementRef<'_, dyn Send>>,
+        query: &SearchQuery,
+        cx: &mut WindowContext,
+    ) {
+        self.update(cx, |this, cx| {
+            this.replace_all(&mut matches.map(|m| m.downcast_ref().unwrap()), query, cx);
+        })
+    }
+
     fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext) {
         self.update(cx, |this, cx| {
             this.search_bar_visibility_changed(visible, cx)