refactored search mode to ensure state is consistent

KCaverly and Piotr created

Co-authored-by: Piotr <piotr@zed.dev>

Change summary

crates/search/src/project_search.rs | 289 +++++++++++++++++++++++++++---
1 file changed, 257 insertions(+), 32 deletions(-)

Detailed changes

crates/search/src/project_search.rs 🔗

@@ -51,7 +51,10 @@ actions!(
         ToggleFocus,
         NextField,
         ToggleSemanticSearch,
-        CycleMode
+        CycleMode,
+        ActivateTextMode,
+        ActivateSemanticMode,
+        ActivateRegexMode
     ]
 );
 
@@ -134,6 +137,68 @@ enum SearchMode {
     Regex,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq)]
+enum Side {
+    Left,
+    Right,
+}
+
+impl SearchMode {
+    fn label(&self) -> &'static str {
+        match self {
+            SearchMode::Text => "Text",
+            SearchMode::Semantic => "Semantic",
+            SearchMode::Regex => "Regex",
+        }
+    }
+
+    fn region_id(&self) -> usize {
+        match self {
+            SearchMode::Text => 3,
+            SearchMode::Semantic => 4,
+            SearchMode::Regex => 5,
+        }
+    }
+
+    fn tooltip_text(&self) -> &'static str {
+        match self {
+            SearchMode::Text => "Activate Text Search",
+            SearchMode::Semantic => "Activate Semantic Search",
+            SearchMode::Regex => "Activate Regex Search",
+        }
+    }
+
+    fn activate_action(&self) -> Box<dyn Action> {
+        match self {
+            SearchMode::Text => Box::new(ActivateTextMode),
+            SearchMode::Semantic => Box::new(ActivateSemanticMode),
+            SearchMode::Regex => Box::new(ActivateRegexMode),
+        }
+    }
+
+    fn border_left(&self) -> bool {
+        match self {
+            SearchMode::Text => false,
+            _ => true,
+        }
+    }
+
+    fn border_right(&self) -> bool {
+        match self {
+            SearchMode::Regex => false,
+            _ => true,
+        }
+    }
+
+    fn button_side(&self) -> Option<Side> {
+        match self {
+            SearchMode::Text => Some(Side::Left),
+            SearchMode::Semantic => None,
+            SearchMode::Regex => Some(Side::Right),
+        }
+    }
+}
+
 pub struct ProjectSearchBar {
     active_project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -560,6 +625,68 @@ impl Item for ProjectSearchView {
 }
 
 impl ProjectSearchView {
+    fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+        self.current_mode = mode;
+
+        match mode {
+            SearchMode::Semantic => {
+                if let Some(semantic_index) = SemanticIndex::global(cx) {
+                    // Semantic search uses no options
+                    self.search_options = SearchOptions::none();
+
+                    let project = self.model.read(cx).project.clone();
+                    let index_task = semantic_index.update(cx, |semantic_index, cx| {
+                        semantic_index.index_project(project, cx)
+                    });
+
+                    cx.spawn(|search_view, mut cx| async move {
+                        let (files_to_index, mut files_remaining_rx) = index_task.await?;
+
+                        search_view.update(&mut cx, |search_view, cx| {
+                            cx.notify();
+                            search_view.semantic = Some(SemanticSearchState {
+                                file_count: files_to_index,
+                                outstanding_file_count: files_to_index,
+                                _progress_task: cx.spawn(|search_view, mut cx| async move {
+                                    while let Some(count) = files_remaining_rx.recv().await {
+                                        search_view
+                                            .update(&mut cx, |search_view, cx| {
+                                                if let Some(semantic_search_state) =
+                                                    &mut search_view.semantic
+                                                {
+                                                    semantic_search_state.outstanding_file_count =
+                                                        count;
+                                                    cx.notify();
+                                                    if count == 0 {
+                                                        return;
+                                                    }
+                                                }
+                                            })
+                                            .ok();
+                                    }
+                                }),
+                            });
+                        })?;
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                }
+            }
+            SearchMode::Regex => {
+                if !self.is_option_enabled(SearchOptions::REGEX) {
+                    self.toggle_search_option(SearchOptions::REGEX, cx);
+                }
+                self.semantic = None;
+            }
+            SearchMode::Text => {
+                if self.is_option_enabled(SearchOptions::REGEX) {
+                    self.toggle_search_option(SearchOptions::REGEX, cx);
+                }
+                self.semantic = None;
+            }
+        }
+        cx.notify();
+    }
     fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
         let project;
         let excerpts;
@@ -739,24 +866,29 @@ impl ProjectSearchView {
     }
 
     fn search(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(semantic) = &mut self.semantic {
-            if semantic.outstanding_file_count > 0 {
-                return;
-            }
+        let mode = self.current_mode;
+        match mode {
+            SearchMode::Semantic => {
+                if let Some(semantic) = &mut self.semantic {
+                    if semantic.outstanding_file_count > 0 {
+                        return;
+                    }
 
-            let query = self.query_editor.read(cx).text(cx);
-            if let Some((included_files, exclude_files)) =
-                self.get_included_and_excluded_globsets(cx)
-            {
-                self.model.update(cx, |model, cx| {
-                    model.semantic_search(query, included_files, exclude_files, cx)
-                });
+                    let query = self.query_editor.read(cx).text(cx);
+                    if let Some((included_files, exclude_files)) =
+                        self.get_included_and_excluded_globsets(cx)
+                    {
+                        self.model.update(cx, |model, cx| {
+                            model.semantic_search(query, included_files, exclude_files, cx)
+                        });
+                    }
+                }
+            }
+            _ => {
+                if let Some(query) = self.build_search_query(cx) {
+                    self.model.update(cx, |model, cx| model.search(query, cx));
+                }
             }
-            return;
-        }
-
-        if let Some(query) = self.build_search_query(cx) {
-            self.model.update(cx, |model, cx| model.search(query, cx));
         }
     }
 
@@ -791,7 +923,10 @@ impl ProjectSearchView {
 
         Some((included_files, excluded_files))
     }
-
+    fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
+        self.search_options.toggle(option);
+        self.semantic = None;
+    }
     fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
         let text = self.query_editor.read(cx).text(cx);
         let included_files =
@@ -957,6 +1092,9 @@ impl ProjectSearchView {
 
         cx.propagate_action();
     }
+    fn is_option_enabled(&self, option: SearchOptions) -> bool {
+        self.search_options.contains(option)
+    }
 }
 
 impl Default for ProjectSearchBar {
@@ -1117,12 +1255,12 @@ impl ProjectSearchBar {
                     SearchMode::Regex
                 };
 
-                this.current_mode = match mode {
+                let new_mode = match mode {
                     &SearchMode::Text => next_text_state,
                     &SearchMode::Semantic => SearchMode::Regex,
                     SearchMode::Regex => SearchMode::Text,
                 };
-                cx.notify();
+                this.activate_search_mode(new_mode, cx);
             })
         }
     }
@@ -1235,12 +1373,7 @@ impl ProjectSearchBar {
     fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                search_view.search_options.toggle(option);
-                if option.contains(SearchOptions::REGEX) {
-                    search_view.current_mode = SearchMode::Regex;
-                }
-                search_view.semantic = None;
-                search_view.search(cx);
+                search_view.toggle_search_option(option, cx);
             });
             cx.notify();
             true
@@ -1248,6 +1381,7 @@ impl ProjectSearchBar {
             false
         }
     }
+
     fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
@@ -1403,6 +1537,98 @@ impl ProjectSearchBar {
         .into_any()
     }
 
+    fn render_search_mode_button(
+        &self,
+        mode: SearchMode,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        let is_active = if let Some(search) = self.active_project_search.as_ref() {
+            let search = search.read(cx);
+            search.current_mode == mode
+        } else {
+            false
+        };
+
+        enum SearchModeButton {}
+        MouseEventHandler::<SearchModeButton, _>::new(mode.region_id(), cx, |state, cx| {
+            let theme = theme::current(cx);
+            let mut style = theme
+                .search
+                .mode_button
+                .in_state(is_active)
+                .style_for(state)
+                .clone();
+
+            let label = Label::new(mode.label(), style.text.clone())
+                .contained()
+                .with_style(style.container);
+
+            if let Some(button_side) = mode.button_side() {
+                style.container.border.left = mode.border_left();
+                style.container.border.right = mode.border_right();
+
+                if button_side == Side::Left {
+                    Flex::row()
+                        .with_child(
+                            ButtonSide::left(
+                                style
+                                    .container
+                                    .background_color
+                                    .unwrap_or_else(gpui::color::Color::transparent_black),
+                            )
+                            .with_border(style.container.border.width, style.container.border.color)
+                            .contained()
+                            .constrained()
+                            .with_max_width(theme.search.mode_filling_width),
+                        )
+                        .with_child(label)
+                        .into_any()
+                } else {
+                    Flex::row()
+                        .with_child(label)
+                        .with_child(
+                            ButtonSide::right(
+                                style
+                                    .container
+                                    .background_color
+                                    .unwrap_or_else(gpui::color::Color::transparent_black),
+                            )
+                            .with_border(style.container.border.width, style.container.border.color)
+                            .contained()
+                            .constrained()
+                            .with_max_width(theme.search.mode_filling_width),
+                        )
+                        .into_any()
+                }
+            } else {
+                label.into_any()
+            }
+        })
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.activate_search_mode(mode, cx);
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<SearchModeButton>(
+            mode.region_id(),
+            mode.tooltip_text().to_owned(),
+            Some(mode.activate_action()),
+            tooltip_style,
+            cx,
+        )
+        .into_any()
+    }
+
+    fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+        // Update Current Mode
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                search_view.activate_search_mode(mode, cx);
+            });
+            cx.notify();
+        }
+    }
+
     fn render_regex_button(
         &self,
         icon: &'static str,
@@ -1561,6 +1787,7 @@ impl ProjectSearchBar {
         )
         .into_any()
     }
+
     fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
         if let Some(search) = self.active_project_search.as_ref() {
             search.read(cx).search_options.contains(option)
@@ -1609,7 +1836,6 @@ impl View for ProjectSearchBar {
                 .aligned()
                 .right()
                 .flex(1.0, true);
-            let regex_button = self.render_regex_button("Regex", search.current_mode.clone(), cx);
             let row_spacing = theme.workspace.toolbar.container.padding.bottom;
             let search = _search.read(cx);
             let filter_button = {
@@ -1726,9 +1952,8 @@ impl View for ProjectSearchBar {
                     )
             });
 
-            let semantic_index =
-                SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx));
-            let normal_search = self.render_text_search_button(cx);
+            let semantic_index = SemanticIndex::enabled(cx)
+                .then(|| self.render_search_mode_button(SearchMode::Semantic, cx));
             Flex::row()
                 .with_child(
                     Flex::column()
@@ -1778,9 +2003,9 @@ impl View for ProjectSearchBar {
                     Flex::column().with_child(
                         Flex::row()
                             .align_children_center()
-                            .with_child(normal_search)
+                            .with_child(self.render_search_mode_button(SearchMode::Text, cx))
                             .with_children(semantic_index)
-                            .with_child(regex_button)
+                            .with_child(self.render_search_mode_button(SearchMode::Regex, cx))
                             .constrained()
                             .with_height(theme.workspace.toolbar.height)
                             .contained()