fix semantic search panic which is created via incompatible build_search_query path

KCaverly and Piotr created

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

Change summary

crates/project/src/search.rs              | 73 ++++++++++++++----------
crates/search/src/project_search.rs       | 59 ++++++++++---------
crates/terminal_view/src/terminal_view.rs |  6 -
styles/src/style_tree/search.ts           |  1 
4 files changed, 75 insertions(+), 64 deletions(-)

Detailed changes

crates/project/src/search.rs 🔗

@@ -13,24 +13,39 @@ use std::{
     sync::Arc,
 };
 
+#[derive(Clone, Debug)]
+pub struct SearchInputs {
+    query: Arc<str>,
+    files_to_include: Vec<PathMatcher>,
+    files_to_exclude: Vec<PathMatcher>,
+}
+
+impl SearchInputs {
+    pub fn as_str(&self) -> &str {
+        self.query.as_ref()
+    }
+    pub fn files_to_include(&self) -> &[PathMatcher] {
+        &self.files_to_include
+    }
+    pub fn files_to_exclude(&self) -> &[PathMatcher] {
+        &self.files_to_exclude
+    }
+}
 #[derive(Clone, Debug)]
 pub enum SearchQuery {
     Text {
         search: Arc<AhoCorasick<usize>>,
-        query: Arc<str>,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<PathMatcher>,
-        files_to_exclude: Vec<PathMatcher>,
+        inner: SearchInputs,
     },
     Regex {
         regex: Regex,
-        query: Arc<str>,
+
         multiline: bool,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<PathMatcher>,
-        files_to_exclude: Vec<PathMatcher>,
+        inner: SearchInputs,
     },
 }
 
@@ -72,13 +87,16 @@ impl SearchQuery {
             .auto_configure(&[&query])
             .ascii_case_insensitive(!case_sensitive)
             .build(&[&query]);
+        let inner = SearchInputs {
+            query: query.into(),
+            files_to_exclude,
+            files_to_include,
+        };
         Self::Text {
             search: Arc::new(search),
-            query: Arc::from(query),
             whole_word,
             case_sensitive,
-            files_to_include,
-            files_to_exclude,
+            inner,
         }
     }
 
@@ -104,14 +122,17 @@ impl SearchQuery {
             .case_insensitive(!case_sensitive)
             .multi_line(multiline)
             .build()?;
+        let inner = SearchInputs {
+            query: initial_query,
+            files_to_exclude,
+            files_to_include,
+        };
         Ok(Self::Regex {
             regex,
-            query: initial_query,
             multiline,
             whole_word,
             case_sensitive,
-            files_to_include,
-            files_to_exclude,
+            inner,
         })
     }
 
@@ -267,10 +288,7 @@ impl SearchQuery {
     }
 
     pub fn as_str(&self) -> &str {
-        match self {
-            Self::Text { query, .. } => query.as_ref(),
-            Self::Regex { query, .. } => query.as_ref(),
-        }
+        self.as_inner().as_str()
     }
 
     pub fn whole_word(&self) -> bool {
@@ -292,25 +310,11 @@ impl SearchQuery {
     }
 
     pub fn files_to_include(&self) -> &[PathMatcher] {
-        match self {
-            Self::Text {
-                files_to_include, ..
-            } => files_to_include,
-            Self::Regex {
-                files_to_include, ..
-            } => files_to_include,
-        }
+        self.as_inner().files_to_include()
     }
 
     pub fn files_to_exclude(&self) -> &[PathMatcher] {
-        match self {
-            Self::Text {
-                files_to_exclude, ..
-            } => files_to_exclude,
-            Self::Regex {
-                files_to_exclude, ..
-            } => files_to_exclude,
-        }
+        self.as_inner().files_to_exclude()
     }
 
     pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
@@ -329,6 +333,11 @@ impl SearchQuery {
             None => self.files_to_include().is_empty(),
         }
     }
+    pub fn as_inner(&self) -> &SearchInputs {
+        match self {
+            Self::Regex { inner, .. } | Self::Text { inner, .. } => inner,
+        }
+    }
 }
 
 fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<Vec<PathMatcher>> {

crates/search/src/project_search.rs 🔗

@@ -24,7 +24,7 @@ use gpui::{
 use menu::Confirm;
 use postage::stream::Stream;
 use project::{
-    search::{PathMatcher, SearchQuery},
+    search::{PathMatcher, SearchInputs, SearchQuery},
     Entry, Project,
 };
 use semantic_index::SemanticIndex;
@@ -177,10 +177,12 @@ impl ProjectSearch {
     }
 
     fn kill_search(&mut self) {
+        dbg!("Killing search");
         self.active_query = None;
         self.match_ranges.clear();
         self.pending_search = None;
         self.no_results = None;
+        dbg!("Killed search");
     }
 
     fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
@@ -226,22 +228,22 @@ impl ProjectSearch {
         cx.notify();
     }
 
-    fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
+    fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext<Self>) {
         let search = SemanticIndex::global(cx).map(|index| {
             index.update(cx, |semantic_index, cx| {
                 semantic_index.search_project(
                     self.project.clone(),
-                    query.as_str().to_owned(),
+                    inputs.as_str().to_owned(),
                     10,
-                    query.files_to_include().to_vec(),
-                    query.files_to_exclude().to_vec(),
+                    inputs.files_to_include().to_vec(),
+                    inputs.files_to_exclude().to_vec(),
                     cx,
                 )
             })
         });
         self.search_id += 1;
         self.match_ranges.clear();
-        self.search_history.add(query.as_str().to_string());
+        self.search_history.add(inputs.as_str().to_string());
         self.no_results = Some(true);
         self.pending_search = Some(cx.spawn(|this, mut cx| async move {
             let results = search?.await.log_err()?;
@@ -682,6 +684,7 @@ impl ProjectSearchView {
 
     fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
         let previous_mode = self.current_mode;
+        log::error!("Going from {previous_mode:?} to {:?}", mode);
         if previous_mode == mode {
             return;
         }
@@ -690,6 +693,7 @@ impl ProjectSearchView {
 
         match mode {
             SearchMode::Semantic => {
+                dbg!("Matched on Semantic");
                 let has_permission = self.semantic_permissioned(cx);
                 self.active_match_index = None;
                 cx.spawn(|this, mut cx| async move {
@@ -947,7 +951,7 @@ impl ProjectSearchView {
 
                     if let Some(query) = self.build_search_query(cx) {
                         self.model
-                            .update(cx, |model, cx| model.semantic_search(query, cx));
+                            .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
                     }
                 }
             }
@@ -986,33 +990,34 @@ impl ProjectSearchView {
                     return None;
                 }
             };
-        if self.current_mode == SearchMode::Regex {
-            match SearchQuery::regex(
-                text,
-                self.search_options.contains(SearchOptions::WHOLE_WORD),
-                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
-                included_files,
-                excluded_files,
-            ) {
-                Ok(query) => {
-                    self.panels_with_errors.remove(&InputPanel::Query);
-                    Some(query)
-                }
-                Err(_e) => {
-                    self.panels_with_errors.insert(InputPanel::Query);
-                    cx.notify();
-                    None
+        let current_mode = self.current_mode;
+        match current_mode {
+            SearchMode::Regex => {
+                match SearchQuery::regex(
+                    text,
+                    self.search_options.contains(SearchOptions::WHOLE_WORD),
+                    self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+                    included_files,
+                    excluded_files,
+                ) {
+                    Ok(query) => {
+                        self.panels_with_errors.remove(&InputPanel::Query);
+                        Some(query)
+                    }
+                    Err(_e) => {
+                        self.panels_with_errors.insert(InputPanel::Query);
+                        cx.notify();
+                        None
+                    }
                 }
             }
-        } else {
-            debug_assert_ne!(self.current_mode, SearchMode::Semantic);
-            Some(SearchQuery::text(
+            _ => Some(SearchQuery::text(
                 text,
                 self.search_options.contains(SearchOptions::WHOLE_WORD),
                 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                 included_files,
                 excluded_files,
-            ))
+            )),
         }
     }
 

crates/terminal_view/src/terminal_view.rs 🔗

@@ -483,10 +483,8 @@ fn possible_open_targets(
 }
 
 pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
-    let searcher = match query {
-        project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
-        project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query),
-    };
+    let query = query.as_str();
+    let searcher = RegexSearch::new(&query);
     searcher.ok()
 }
 

styles/src/style_tree/search.ts 🔗

@@ -2,7 +2,6 @@ import { with_opacity } from "../theme/color"
 import { background, border, foreground, text } from "./components"
 import { interactive, toggleable } from "../element"
 import { useTheme } from "../theme"
-import { toggleable_icon_button } from "../component/icon_button"
 
 export default function search(): any {
     const theme = useTheme()