search: Add "Error" borders for query editors with malformed content. (#3824)

Piotr Osiewicz created

This commit also changes the way search queries are built (we do not bail early anymore if include/exclude editor queries are malformed) to propagate error status of the panel.
Release Notes:

- N/A

Change summary

crates/search2/src/buffer_search.rs  | 10 ++
crates/search2/src/project_search.rs | 84 ++++++++++++++++++++++-------
2 files changed, 70 insertions(+), 24 deletions(-)

Detailed changes

crates/search2/src/buffer_search.rs 🔗

@@ -181,7 +181,11 @@ impl Render for BufferSearchBar {
         if in_replace {
             key_context.add("in_replace");
         }
-
+        let editor_border = if self.query_contains_error {
+            Color::Error.color(cx)
+        } else {
+            cx.theme().colors().border
+        };
         h_stack()
             .w_full()
             .gap_2()
@@ -217,7 +221,7 @@ impl Render for BufferSearchBar {
                     .py_1()
                     .gap_2()
                     .border_1()
-                    .border_color(cx.theme().colors().border)
+                    .border_color(editor_border)
                     .rounded_lg()
                     .child(IconElement::new(Icon::MagnifyingGlass))
                     .child(self.render_text_input(&self.query_editor, cx))
@@ -852,6 +856,7 @@ impl BufferSearchBar {
                         Ok(query) => query.with_replacement(self.replacement(cx)),
                         Err(_) => {
                             self.query_contains_error = true;
+                            self.active_match_index = None;
                             cx.notify();
                             return done_rx;
                         }
@@ -868,6 +873,7 @@ impl BufferSearchBar {
                         Ok(query) => query.with_replacement(self.replacement(cx)),
                         Err(_) => {
                             self.query_contains_error = true;
+                            self.active_match_index = None;
                             cx.notify();
                             return done_rx;
                         }

crates/search2/src/project_search.rs 🔗

@@ -13,10 +13,10 @@ use editor::{
 use editor::{EditorElement, EditorStyle};
 use gpui::{
     actions, div, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, EventEmitter,
-    FocusHandle, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, KeyContext,
-    Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled, Subscription,
-    Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace,
-    WindowContext,
+    FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement, IntoElement,
+    KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled,
+    Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
+    WhiteSpace, WindowContext,
 };
 use menu::Confirm;
 use project::{
@@ -1007,33 +1007,46 @@ impl ProjectSearchView {
     }
 
     fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
+        // Do not bail early in this function, as we want to fill out `self.panels_with_errors`.
         let text = self.query_editor.read(cx).text(cx);
         let included_files =
             match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
                 Ok(included_files) => {
-                    self.panels_with_errors.remove(&InputPanel::Include);
+                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Include);
+                    if should_unmark_error {
+                        cx.notify();
+                    }
                     included_files
                 }
                 Err(_e) => {
-                    self.panels_with_errors.insert(InputPanel::Include);
-                    cx.notify();
-                    return None;
+                    let should_mark_error = self.panels_with_errors.insert(InputPanel::Include);
+                    if should_mark_error {
+                        cx.notify();
+                    }
+                    vec![]
                 }
             };
         let excluded_files =
             match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
                 Ok(excluded_files) => {
-                    self.panels_with_errors.remove(&InputPanel::Exclude);
+                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Exclude);
+                    if should_unmark_error {
+                        cx.notify();
+                    }
+
                     excluded_files
                 }
                 Err(_e) => {
-                    self.panels_with_errors.insert(InputPanel::Exclude);
-                    cx.notify();
-                    return None;
+                    let should_mark_error = self.panels_with_errors.insert(InputPanel::Exclude);
+                    if should_mark_error {
+                        cx.notify();
+                    }
+                    vec![]
                 }
             };
+
         let current_mode = self.current_mode;
-        match current_mode {
+        let query = match current_mode {
             SearchMode::Regex => {
                 match SearchQuery::regex(
                     text,
@@ -1044,12 +1057,20 @@ impl ProjectSearchView {
                     excluded_files,
                 ) {
                     Ok(query) => {
-                        self.panels_with_errors.remove(&InputPanel::Query);
+                        let should_unmark_error =
+                            self.panels_with_errors.remove(&InputPanel::Query);
+                        if should_unmark_error {
+                            cx.notify();
+                        }
+
                         Some(query)
                     }
                     Err(_e) => {
-                        self.panels_with_errors.insert(InputPanel::Query);
-                        cx.notify();
+                        let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
+                        if should_mark_error {
+                            cx.notify();
+                        }
+
                         None
                     }
                 }
@@ -1063,16 +1084,27 @@ impl ProjectSearchView {
                 excluded_files,
             ) {
                 Ok(query) => {
-                    self.panels_with_errors.remove(&InputPanel::Query);
+                    let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
+                    if should_unmark_error {
+                        cx.notify();
+                    }
+
                     Some(query)
                 }
                 Err(_e) => {
-                    self.panels_with_errors.insert(InputPanel::Query);
-                    cx.notify();
+                    let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
+                    if should_mark_error {
+                        cx.notify();
+                    }
+
                     None
                 }
             },
+        };
+        if !self.panels_with_errors.is_empty() {
+            return None;
         }
+        query
     }
 
     fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
@@ -1185,6 +1217,13 @@ impl ProjectSearchView {
             SearchMode::Semantic => "\nSimply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into()
         }
     }
+    fn border_color_for(&self, panel: InputPanel, cx: &WindowContext) -> Hsla {
+        if self.panels_with_errors.contains(&panel) {
+            Color::Error.color(cx)
+        } else {
+            cx.theme().colors().border
+        }
+    }
 }
 
 impl Default for ProjectSearchBar {
@@ -1507,6 +1546,7 @@ impl Render for ProjectSearchBar {
         }
         let search = search.read(cx);
         let semantic_is_available = SemanticIndex::enabled(cx);
+
         let query_column = v_stack().child(
             h_stack()
                 .min_w(rems(512. / 16.))
@@ -1515,7 +1555,7 @@ impl Render for ProjectSearchBar {
                 .gap_2()
                 .bg(cx.theme().colors().editor_background)
                 .border_1()
-                .border_color(cx.theme().colors().border)
+                .border_color(search.border_color_for(InputPanel::Query, cx))
                 .rounded_lg()
                 .on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
                 .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx)))
@@ -1789,7 +1829,7 @@ impl Render for ProjectSearchBar {
                                 .px_2()
                                 .py_1()
                                 .border_1()
-                                .border_color(cx.theme().colors().border)
+                                .border_color(search.border_color_for(InputPanel::Include, cx))
                                 .rounded_lg()
                                 .child(self.render_text_input(&search.included_files_editor, cx))
                                 .when(search.current_mode != SearchMode::Semantic, |this| {
@@ -1815,7 +1855,7 @@ impl Render for ProjectSearchBar {
                                 .px_2()
                                 .py_1()
                                 .border_1()
-                                .border_color(cx.theme().colors().border)
+                                .border_color(search.border_color_for(InputPanel::Exclude, cx))
                                 .rounded_lg()
                                 .child(self.render_text_input(&search.excluded_files_editor, cx)),
                         ),