buffer_search: Fix replace buttons not working if search bar is not focused (#43569)

Dino created

Update the way that both
`search::buffer_search::BufferSearchBar.replace_next` and
`search::buffer_search::BufferSearchBar.replace_all` are registered as
listeners, so that we don't require the replacement editor to be focused
in order for these listeners to be active, only requiring the
replacement mode to be active in the buffer search bar.

This means that, even if the user is focused on the buffer editor, if
the "Replace Next Match" or "Replace All Matches" buttons are clicked,
the replacement will be performed.

Closes #42471 

Release Notes:

- Fixed issue with buffer search bar where the replacement buttons
("Replace Next Match" & "Replace All Matches") wouldn't work if search
bar was not focused

Change summary

crates/search/src/buffer_search.rs | 52 +++++++++++++++++++++++++++++--
crates/search/src/search_bar.rs    |  2 
2 files changed, 49 insertions(+), 5 deletions(-)

Detailed changes

crates/search/src/buffer_search.rs 🔗

@@ -432,10 +432,8 @@ impl Render for BufferSearchBar {
             }))
             .when(replacement, |this| {
                 this.on_action(cx.listener(Self::toggle_replace))
-                    .when(in_replace, |this| {
-                        this.on_action(cx.listener(Self::replace_next))
-                            .on_action(cx.listener(Self::replace_all))
-                    })
+                    .on_action(cx.listener(Self::replace_next))
+                    .on_action(cx.listener(Self::replace_all))
             })
             .when(case, |this| {
                 this.on_action(cx.listener(Self::toggle_case_sensitive))
@@ -2549,6 +2547,52 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_replace_focus(cx: &mut TestAppContext) {
+        let (editor, search_bar, cx) = init_test(cx);
+
+        editor.update_in(cx, |editor, window, cx| {
+            editor.set_text("What a bad day!", window, cx)
+        });
+
+        search_bar
+            .update_in(cx, |search_bar, window, cx| {
+                search_bar.search("bad", None, true, window, cx)
+            })
+            .await
+            .unwrap();
+
+        // Calling `toggle_replace` in the search bar ensures that the "Replace
+        // *" buttons are rendered, so we can then simulate clicking the
+        // buttons.
+        search_bar.update_in(cx, |search_bar, window, cx| {
+            search_bar.toggle_replace(&ToggleReplace, window, cx)
+        });
+
+        search_bar.update_in(cx, |search_bar, window, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("great", window, cx);
+            });
+        });
+
+        // Focus on the editor instead of the search bar, as we want to ensure
+        // that pressing the "Replace Next Match" button will work, even if the
+        // search bar is not focused.
+        cx.focus(&editor);
+
+        // We'll not simulate clicking the "Replace Next Match " button, asserting that
+        // the replacement was done.
+        let button_bounds = cx
+            .debug_bounds("ICON-ReplaceNext")
+            .expect("'Replace Next Match' button should be visible");
+        cx.simulate_click(button_bounds.center(), gpui::Modifiers::none());
+
+        assert_eq!(
+            editor.read_with(cx, |editor, cx| editor.text(cx)),
+            "What a great day!"
+        );
+    }
+
     struct ReplacementTestParams<'a> {
         editor: &'a Entity<Editor>,
         search_bar: &'a Entity<BufferSearchBar>,

crates/search/src/search_bar.rs 🔗

@@ -29,7 +29,7 @@ pub(super) fn render_action_button(
             if !focus_handle.is_focused(window) {
                 window.focus(&focus_handle);
             }
-            window.dispatch_action(action.boxed_clone(), cx)
+            window.dispatch_action(action.boxed_clone(), cx);
         }
     })
     .tooltip(move |_window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, cx))