Fix invalid regular expressions highlighting all search fields (#35001)

Peter Tripp created

Closes https://github.com/zed-industries/zed/issues/34969
Closes https://github.com/zed-industries/zed/issues/34970

Only highlight the search field on regex error (buffer search and
project search).
Clear errors when the buffer search hidden so stale errors aren't shown
on next search.

Before (all fields highlighted red): 
<img width="631" height="180" alt="Screenshot 2025-07-23 at 22 59 45"
src="https://github.com/user-attachments/assets/a91d3090-1bae-4718-a1f5-0a1237559ff2"
/>

After (only query field highlighted red): 
<img width="632" height="187" alt="Screenshot 2025-07-23 at 23 10 49"
src="https://github.com/user-attachments/assets/6ae72c84-9333-4cdb-907b-afb7a0a6e808"
/>

Release Notes:

- Improved highlighting of regex errors in search dialogs

Change summary

crates/search/src/buffer_search.rs  | 22 +++++++++++++---------
crates/search/src/project_search.rs | 13 +++++++------
2 files changed, 20 insertions(+), 15 deletions(-)

Detailed changes

crates/search/src/buffer_search.rs 🔗

@@ -228,16 +228,17 @@ impl Render for BufferSearchBar {
         if in_replace {
             key_context.add("in_replace");
         }
-        let editor_border = if self.query_error.is_some() {
+        let query_border = if self.query_error.is_some() {
             Color::Error.color(cx)
         } else {
             cx.theme().colors().border
         };
+        let replacement_border = cx.theme().colors().border;
 
         let container_width = window.viewport_size().width;
         let input_width = SearchInputWidth::calc_width(container_width);
 
-        let input_base_styles = || {
+        let input_base_styles = |border_color| {
             h_flex()
                 .min_w_32()
                 .w(input_width)
@@ -246,7 +247,7 @@ impl Render for BufferSearchBar {
                 .pr_1()
                 .py_1()
                 .border_1()
-                .border_color(editor_border)
+                .border_color(border_color)
                 .rounded_lg()
         };
 
@@ -256,7 +257,7 @@ impl Render for BufferSearchBar {
                 el.child(Label::new("Find in results").color(Color::Hint))
             })
             .child(
-                input_base_styles()
+                input_base_styles(query_border)
                     .id("editor-scroll")
                     .track_scroll(&self.editor_scroll_handle)
                     .child(self.render_text_input(&self.query_editor, color_override, cx))
@@ -430,11 +431,13 @@ impl Render for BufferSearchBar {
         let replace_line = should_show_replace_input.then(|| {
             h_flex()
                 .gap_2()
-                .child(input_base_styles().child(self.render_text_input(
-                    &self.replacement_editor,
-                    None,
-                    cx,
-                )))
+                .child(
+                    input_base_styles(replacement_border).child(self.render_text_input(
+                        &self.replacement_editor,
+                        None,
+                        cx,
+                    )),
+                )
                 .child(
                     h_flex()
                         .min_w_64()
@@ -775,6 +778,7 @@ impl BufferSearchBar {
 
     pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
         self.dismissed = true;
+        self.query_error = None;
         for searchable_item in self.searchable_items_with_matches.keys() {
             if let Some(searchable_item) =
                 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)

crates/search/src/project_search.rs 🔗

@@ -195,6 +195,7 @@ pub struct ProjectSearch {
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 enum InputPanel {
     Query,
+    Replacement,
     Exclude,
     Include,
 }
@@ -1962,7 +1963,7 @@ impl Render for ProjectSearchBar {
             MultipleInputs,
         }
 
-        let input_base_styles = |base_style: BaseStyle| {
+        let input_base_styles = |base_style: BaseStyle, panel: InputPanel| {
             h_flex()
                 .min_w_32()
                 .map(|div| match base_style {
@@ -1974,11 +1975,11 @@ impl Render for ProjectSearchBar {
                 .pr_1()
                 .py_1()
                 .border_1()
-                .border_color(search.border_color_for(InputPanel::Query, cx))
+                .border_color(search.border_color_for(panel, cx))
                 .rounded_lg()
         };
 
-        let query_column = input_base_styles(BaseStyle::SingleInput)
+        let query_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Query)
             .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
             .on_action(cx.listener(|this, action, window, cx| {
                 this.previous_history_query(action, window, cx)
@@ -2167,7 +2168,7 @@ impl Render for ProjectSearchBar {
             .child(h_flex().min_w_64().child(mode_column).child(matches_column));
 
         let replace_line = search.replace_enabled.then(|| {
-            let replace_column = input_base_styles(BaseStyle::SingleInput)
+            let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement)
                 .child(self.render_text_input(&search.replacement_editor, cx));
 
             let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
@@ -2241,7 +2242,7 @@ impl Render for ProjectSearchBar {
                         .gap_2()
                         .w(input_width)
                         .child(
-                            input_base_styles(BaseStyle::MultipleInputs)
+                            input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include)
                                 .on_action(cx.listener(|this, action, window, cx| {
                                     this.previous_history_query(action, window, cx)
                                 }))
@@ -2251,7 +2252,7 @@ impl Render for ProjectSearchBar {
                                 .child(self.render_text_input(&search.included_files_editor, cx)),
                         )
                         .child(
-                            input_base_styles(BaseStyle::MultipleInputs)
+                            input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude)
                                 .on_action(cx.listener(|this, action, window, cx| {
                                     this.previous_history_query(action, window, cx)
                                 }))