Test the search inclusions/exclusions

Kirill Bulatov created

Change summary

crates/project/src/project.rs       |   6 
crates/project/src/project_tests.rs | 362 +++++++++++++++++++++++++++++-
crates/search/src/project_search.rs |   6 
crates/workspace/src/toolbar.rs     |   3 
4 files changed, 350 insertions(+), 27 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -4208,11 +4208,9 @@ impl Project {
                                                     if matching_paths_tx.is_closed() {
                                                         break;
                                                     }
-                                                    let matches = if !query
+                                                    let matches = if query
                                                         .file_matches(Some(&entry.path))
                                                     {
-                                                        false
-                                                    } else {
                                                         abs_path.clear();
                                                         abs_path.push(&snapshot.abs_path());
                                                         abs_path.push(&entry.path);
@@ -4223,6 +4221,8 @@ impl Project {
                                                         } else {
                                                             false
                                                         }
+                                                    } else {
+                                                        false
                                                     };
 
                                                     if matches {

crates/project/src/project_tests.rs 🔗

@@ -3335,28 +3335,348 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
             ("four.rs".to_string(), vec![25..28, 36..39])
         ])
     );
+}
 
-    async fn search(
-        project: &ModelHandle<Project>,
-        query: SearchQuery,
-        cx: &mut gpui::TestAppContext,
-    ) -> Result<HashMap<String, Vec<Range<usize>>>> {
-        let results = project
-            .update(cx, |project, cx| project.search(query, cx))
-            .await?;
+#[gpui::test]
+async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
+    let search_query = "file";
 
-        Ok(results
-            .into_iter()
-            .map(|(buffer, ranges)| {
-                buffer.read_with(cx, |buffer, _| {
-                    let path = buffer.file().unwrap().path().to_string_lossy().to_string();
-                    let ranges = ranges
-                        .into_iter()
-                        .map(|range| range.to_offset(buffer))
-                        .collect::<Vec<_>>();
-                    (path, ranges)
-                })
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "one.rs": r#"// Rust file one"#,
+            "one.ts": r#"// TypeScript file one"#,
+            "two.rs": r#"// Rust file two"#,
+            "two.ts": r#"// TypeScript file two"#,
+        }),
+    )
+    .await;
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+    assert!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![glob::Pattern::new("*.odd").unwrap()],
+                Vec::new()
+            ),
+            cx
+        )
+        .await
+        .unwrap()
+        .is_empty(),
+        "If no inclusions match, no files should be returned"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![glob::Pattern::new("*.rs").unwrap()],
+                Vec::new()
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.rs".to_string(), vec![8..12]),
+            ("two.rs".to_string(), vec![8..12]),
+        ]),
+        "Rust only search should give only Rust files"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap(),
+                ],
+                Vec::new()
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.ts".to_string(), vec![14..18]),
+            ("two.ts".to_string(), vec![14..18]),
+        ]),
+        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![
+                    glob::Pattern::new("*.rs").unwrap(),
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap(),
+                ],
+                Vec::new()
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.rs".to_string(), vec![8..12]),
+            ("one.ts".to_string(), vec![14..18]),
+            ("two.rs".to_string(), vec![8..12]),
+            ("two.ts".to_string(), vec![14..18]),
+        ]),
+        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
+    );
+}
+
+#[gpui::test]
+async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
+    let search_query = "file";
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "one.rs": r#"// Rust file one"#,
+            "one.ts": r#"// TypeScript file one"#,
+            "two.rs": r#"// Rust file two"#,
+            "two.ts": r#"// TypeScript file two"#,
+        }),
+    )
+    .await;
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                Vec::new(),
+                vec![glob::Pattern::new("*.odd").unwrap()],
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.rs".to_string(), vec![8..12]),
+            ("one.ts".to_string(), vec![14..18]),
+            ("two.rs".to_string(), vec![8..12]),
+            ("two.ts".to_string(), vec![14..18]),
+        ]),
+        "If no exclusions match, all files should be returned"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                Vec::new(),
+                vec![glob::Pattern::new("*.rs").unwrap()],
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.ts".to_string(), vec![14..18]),
+            ("two.ts".to_string(), vec![14..18]),
+        ]),
+        "Rust exclusion search should give only TypeScript files"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                Vec::new(),
+                vec![
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap(),
+                ],
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.rs".to_string(), vec![8..12]),
+            ("two.rs".to_string(), vec![8..12]),
+        ]),
+        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
+    );
+
+    assert!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                Vec::new(),
+                vec![
+                    glob::Pattern::new("*.rs").unwrap(),
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap(),
+                ],
+            ),
+            cx
+        )
+        .await
+        .unwrap().is_empty(),
+        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
+    );
+}
+
+#[gpui::test]
+async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
+    let search_query = "file";
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "one.rs": r#"// Rust file one"#,
+            "one.ts": r#"// TypeScript file one"#,
+            "two.rs": r#"// Rust file two"#,
+            "two.ts": r#"// TypeScript file two"#,
+        }),
+    )
+    .await;
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+    assert!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![glob::Pattern::new("*.odd").unwrap()],
+                vec![glob::Pattern::new("*.odd").unwrap()],
+            ),
+            cx
+        )
+        .await
+        .unwrap()
+        .is_empty(),
+        "If both no exclusions and inclusions match, exclusions should win and return nothing"
+    );
+
+    assert!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![glob::Pattern::new("*.ts").unwrap()],
+                vec![glob::Pattern::new("*.ts").unwrap()],
+            ),
+            cx
+        )
+        .await
+        .unwrap()
+        .is_empty(),
+        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
+    );
+
+    assert!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap()
+                ],
+                vec![
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap()
+                ],
+            ),
+            cx
+        )
+        .await
+        .unwrap()
+        .is_empty(),
+        "Non-matching inclusions and exclusions should not change that."
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                vec![
+                    glob::Pattern::new("*.ts").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap()
+                ],
+                vec![
+                    glob::Pattern::new("*.rs").unwrap(),
+                    glob::Pattern::new("*.odd").unwrap()
+                ],
+            ),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("one.ts".to_string(), vec![14..18]),
+            ("two.ts".to_string(), vec![14..18]),
+        ]),
+        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
+    );
+}
+
+async fn search(
+    project: &ModelHandle<Project>,
+    query: SearchQuery,
+    cx: &mut gpui::TestAppContext,
+) -> Result<HashMap<String, Vec<Range<usize>>>> {
+    let results = project
+        .update(cx, |project, cx| project.search(query, cx))
+        .await?;
+
+    Ok(results
+        .into_iter()
+        .map(|(buffer, ranges)| {
+            buffer.read_with(cx, |buffer, _| {
+                let path = buffer.file().unwrap().path().to_string_lossy().to_string();
+                let ranges = ranges
+                    .into_iter()
+                    .map(|range| range.to_offset(buffer))
+                    .collect::<Vec<_>>();
+                (path, ranges)
             })
-            .collect())
-    }
+        })
+        .collect())
 }

crates/search/src/project_search.rs 🔗

@@ -428,7 +428,7 @@ impl ProjectSearchView {
             editor.set_text(query_text, cx);
             editor
         });
-        // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
+        // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
         cx.subscribe(&query_editor, |_, _, event, cx| {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
@@ -462,7 +462,7 @@ impl ProjectSearchView {
 
             editor
         });
-        // Subcribe to include_files_editor in order to reraise editor events for workspace item activation purposes
+        // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
         cx.subscribe(&included_files_editor, |_, _, event, cx| {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
@@ -479,7 +479,7 @@ impl ProjectSearchView {
 
             editor
         });
-        // Subcribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
+        // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
         cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })

crates/workspace/src/toolbar.rs 🔗

@@ -23,6 +23,9 @@ pub trait ToolbarItemView: View {
 
     fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
 
+    /// Number of times toolbar's height will be repeated to get the effective height.
+    /// Useful when multiple rows one under each other are needed.
+    /// The rows have the same width and act as a whole when reacting to resizes and similar events.
     fn row_count(&self) -> usize {
         1
     }