Draft search include/exclude logic

Kirill Bulatov created

Change summary

crates/project/src/project.rs       | 45 +++++++++++++--------
crates/project/src/search.rs        | 25 +++++++++++
crates/search/src/project_search.rs | 63 +++++++++++++++++++++---------
crates/theme/src/theme.rs           |  1 
styles/src/styleTree/search.ts      |  4 +
5 files changed, 100 insertions(+), 38 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -4208,16 +4208,21 @@ impl Project {
                                                     if matching_paths_tx.is_closed() {
                                                         break;
                                                     }
-
-                                                    abs_path.clear();
-                                                    abs_path.push(&snapshot.abs_path());
-                                                    abs_path.push(&entry.path);
-                                                    let matches = if let Some(file) =
-                                                        fs.open_sync(&abs_path).await.log_err()
+                                                    let matches = if !query
+                                                        .file_matches(Some(&entry.path))
                                                     {
-                                                        query.detect(file).unwrap_or(false)
-                                                    } else {
                                                         false
+                                                    } else {
+                                                        abs_path.clear();
+                                                        abs_path.push(&snapshot.abs_path());
+                                                        abs_path.push(&entry.path);
+                                                        if let Some(file) =
+                                                            fs.open_sync(&abs_path).await.log_err()
+                                                        {
+                                                            query.detect(file).unwrap_or(false)
+                                                        } else {
+                                                            false
+                                                        }
                                                     };
 
                                                     if matches {
@@ -4299,15 +4304,21 @@ impl Project {
                             let mut buffers_rx = buffers_rx.clone();
                             scope.spawn(async move {
                                 while let Some((buffer, snapshot)) = buffers_rx.next().await {
-                                    let buffer_matches = query
-                                        .search(snapshot.as_rope())
-                                        .await
-                                        .iter()
-                                        .map(|range| {
-                                            snapshot.anchor_before(range.start)
-                                                ..snapshot.anchor_after(range.end)
-                                        })
-                                        .collect::<Vec<_>>();
+                                    let buffer_matches = if query.file_matches(
+                                        snapshot.file().map(|file| file.path().as_ref()),
+                                    ) {
+                                        query
+                                            .search(snapshot.as_rope())
+                                            .await
+                                            .iter()
+                                            .map(|range| {
+                                                snapshot.anchor_before(range.start)
+                                                    ..snapshot.anchor_after(range.end)
+                                            })
+                                            .collect()
+                                    } else {
+                                        Vec::new()
+                                    };
                                     if !buffer_matches.is_empty() {
                                         worker_matched_buffers
                                             .insert(buffer.clone(), buffer_matches);

crates/project/src/search.rs 🔗

@@ -8,10 +8,11 @@ use smol::future::yield_now;
 use std::{
     io::{BufRead, BufReader, Read},
     ops::Range,
+    path::Path,
     sync::Arc,
 };
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum SearchQuery {
     Text {
         search: Arc<AhoCorasick<usize>>,
@@ -97,11 +98,13 @@ impl SearchQuery {
                 message
                     .files_to_include
                     .split(',')
+                    .filter(|glob_str| !glob_str.trim().is_empty())
                     .map(|glob_str| glob::Pattern::new(glob_str))
                     .collect::<Result<_, _>>()?,
                 message
                     .files_to_exclude
                     .split(',')
+                    .filter(|glob_str| !glob_str.trim().is_empty())
                     .map(|glob_str| glob::Pattern::new(glob_str))
                     .collect::<Result<_, _>>()?,
             )
@@ -113,11 +116,13 @@ impl SearchQuery {
                 message
                     .files_to_include
                     .split(',')
+                    .filter(|glob_str| !glob_str.trim().is_empty())
                     .map(|glob_str| glob::Pattern::new(glob_str))
                     .collect::<Result<_, _>>()?,
                 message
                     .files_to_exclude
                     .split(',')
+                    .filter(|glob_str| !glob_str.trim().is_empty())
                     .map(|glob_str| glob::Pattern::new(glob_str))
                     .collect::<Result<_, _>>()?,
             ))
@@ -290,6 +295,7 @@ impl SearchQuery {
             } => files_to_include,
         }
     }
+
     pub fn files_to_exclude(&self) -> &[glob::Pattern] {
         match self {
             Self::Text {
@@ -300,4 +306,21 @@ impl SearchQuery {
             } => files_to_exclude,
         }
     }
+
+    pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
+        match file_path {
+            Some(file_path) => {
+                !self
+                    .files_to_exclude()
+                    .iter()
+                    .any(|exclude_glob| exclude_glob.matches_path(file_path))
+                    && (self.files_to_include().is_empty()
+                        || self
+                            .files_to_include()
+                            .iter()
+                            .any(|include_glob| include_glob.matches_path(file_path)))
+            }
+            None => self.files_to_include().is_empty(),
+        }
+    }
 }

crates/search/src/project_search.rs 🔗

@@ -555,23 +555,30 @@ impl ProjectSearchView {
 
     fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
         let text = self.query_editor.read(cx).text(cx);
-        let included_files = self
+        let Ok(included_files) = self
             .included_files_editor
             .read(cx)
             .text(cx)
             .split(',')
+            .filter(|glob_str| !glob_str.trim().is_empty())
             .map(|glob_str| glob::Pattern::new(glob_str))
-            .collect::<Result<_, _>>()
-            // TODO kb validation
-            .unwrap_or_default();
-        let excluded_files = self
+            .collect::<Result<_, _>>() else {
+                self.query_contains_error = true;
+                cx.notify();
+                return None
+            };
+        let Ok(excluded_files) = self
             .excluded_files_editor
             .read(cx)
             .text(cx)
             .split(',')
+            .filter(|glob_str| !glob_str.trim().is_empty())
             .map(|glob_str| glob::Pattern::new(glob_str))
-            .collect::<Result<_, _>>()
-            .unwrap_or_default();
+            .collect::<Result<_, _>>() else {
+                self.query_contains_error = true;
+                cx.notify();
+                return None
+            };
         if self.regex {
             match SearchQuery::regex(
                 text,
@@ -928,11 +935,11 @@ impl View for ProjectSearchBar {
             let included_files_view = ChildView::new(&search.included_files_editor, cx)
                 .aligned()
                 .left()
-                .flex(1., true);
+                .flex(0.5, true);
             let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
                 .aligned()
-                .left()
-                .flex(1., true);
+                .right()
+                .flex(0.5, true);
 
             Flex::row()
                 .with_child(
@@ -983,25 +990,41 @@ impl View for ProjectSearchBar {
                         .with_style(theme.search.option_button_group)
                         .aligned(),
                 )
+                // TODO kb better layout
                 .with_child(
-                    // TODO kb better layout
                     Flex::row()
                         .with_child(
-                            Label::new("Include files:", theme.search.match_index.text.clone())
-                                .contained()
-                                .with_style(theme.search.match_index.container)
-                                .aligned(),
+                            Label::new(
+                                "Include:",
+                                theme.search.include_exclude_inputs.text.clone(),
+                            )
+                            .contained()
+                            .with_style(theme.search.include_exclude_inputs.container)
+                            .aligned(),
                         )
                         .with_child(included_files_view)
+                        .contained()
+                        .with_style(theme.search.editor.input.container)
+                        .aligned()
+                        .constrained()
+                        .with_min_width(theme.search.editor.min_width)
+                        .with_max_width(theme.search.editor.max_width)
+                        .flex(1., false),
+                )
+                .with_child(
+                    Flex::row()
                         .with_child(
-                            Label::new("Exclude files:", theme.search.match_index.text.clone())
-                                .contained()
-                                .with_style(theme.search.match_index.container)
-                                .aligned(),
+                            Label::new(
+                                "Exclude:",
+                                theme.search.include_exclude_inputs.text.clone(),
+                            )
+                            .contained()
+                            .with_style(theme.search.include_exclude_inputs.container)
+                            .aligned(),
                         )
                         .with_child(excluded_files_view)
                         .contained()
-                        .with_style(editor_container)
+                        .with_style(theme.search.editor.input.container)
                         .aligned()
                         .constrained()
                         .with_min_width(theme.search.editor.min_width)

crates/theme/src/theme.rs 🔗

@@ -319,6 +319,7 @@ pub struct Search {
     pub editor: FindEditor,
     pub invalid_editor: ContainerStyle,
     pub option_button_group: ContainerStyle,
+    pub include_exclude_inputs: ContainedText,
     pub option_button: Interactive<ContainedText>,
     pub match_background: Color,
     pub match_index: ContainedText,

styles/src/styleTree/search.ts 🔗

@@ -74,6 +74,10 @@ export default function search(colorScheme: ColorScheme) {
                 right: 12,
             },
         },
+        includeExcludeInputs: {
+            ...text(layer, "mono", "variant"),
+            padding: 6,
+        },
         resultsStatus: {
             ...text(layer, "mono", "on"),
             size: 18,