Fix release notes appearing in project search (#32898)

Ben Kunkle created

Closes #28829

Release Notes:

- Fixed an issue where release notes would appear in project search
results when opened locally

Change summary

crates/auto_update_ui/src/auto_update_ui.rs |   5 
crates/project/src/buffer_store.rs          |  11 +
crates/project/src/project.rs               |   6 +
crates/project/src/project_tests.rs         | 126 +++++++++++++++++++++++
crates/search/src/project_search.rs         |   6 
5 files changed, 149 insertions(+), 5 deletions(-)

Detailed changes

crates/auto_update_ui/src/auto_update_ui.rs 🔗

@@ -82,7 +82,10 @@ fn view_release_notes_locally(
                         .update_in(cx, |workspace, window, cx| {
                             let project = workspace.project().clone();
                             let buffer = project.update(cx, |project, cx| {
-                                project.create_local_buffer("", markdown, cx)
+                                let buffer = project.create_local_buffer("", markdown, cx);
+                                project
+                                    .mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
+                                buffer
                             });
                             buffer.update(cx, |buffer, cx| {
                                 buffer.edit([(0..0, body.release_notes)], None, cx)

crates/project/src/buffer_store.rs 🔗

@@ -39,6 +39,7 @@ pub struct BufferStore {
     path_to_buffer_id: HashMap<ProjectPath, BufferId>,
     downstream_client: Option<(AnyProtoClient, u64)>,
     shared_buffers: HashMap<proto::PeerId, HashMap<BufferId, SharedBuffer>>,
+    non_searchable_buffers: HashSet<BufferId>,
 }
 
 #[derive(Hash, Eq, PartialEq, Clone)]
@@ -726,6 +727,7 @@ impl BufferStore {
             path_to_buffer_id: Default::default(),
             shared_buffers: Default::default(),
             loading_buffers: Default::default(),
+            non_searchable_buffers: Default::default(),
             worktree_store,
         }
     }
@@ -750,6 +752,7 @@ impl BufferStore {
             path_to_buffer_id: Default::default(),
             loading_buffers: Default::default(),
             shared_buffers: Default::default(),
+            non_searchable_buffers: Default::default(),
             worktree_store,
         }
     }
@@ -1059,7 +1062,9 @@ impl BufferStore {
         let mut unnamed_buffers = Vec::new();
         for handle in self.buffers() {
             let buffer = handle.read(cx);
-            if let Some(entry_id) = buffer.entry_id(cx) {
+            if self.non_searchable_buffers.contains(&buffer.remote_id()) {
+                continue;
+            } else if let Some(entry_id) = buffer.entry_id(cx) {
                 open_buffers.insert(entry_id);
             } else {
                 limit = limit.saturating_sub(1);
@@ -1665,6 +1670,10 @@ impl BufferStore {
         }
         serialized_transaction
     }
+
+    pub(crate) fn mark_buffer_as_non_searchable(&mut self, buffer_id: BufferId) {
+        self.non_searchable_buffers.insert(buffer_id);
+    }
 }
 
 impl OpenBuffer {

crates/project/src/project.rs 🔗

@@ -5068,6 +5068,12 @@ impl Project {
     pub fn agent_location(&self) -> Option<AgentLocation> {
         self.agent_location.clone()
     }
+
+    pub fn mark_buffer_as_non_searchable(&self, buffer_id: BufferId, cx: &mut Context<Project>) {
+        self.buffer_store.update(cx, |buffer_store, _| {
+            buffer_store.mark_buffer_as_non_searchable(buffer_id)
+        });
+    }
 }
 
 pub struct PathMatchCandidateSet {

crates/project/src/project_tests.rs 🔗

@@ -5336,6 +5336,132 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let search_query = "file";
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/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(), [path!("/dir").as_ref()], cx).await;
+    let _buffer = project.update(cx, |project, cx| {
+        let buffer = project.create_local_buffer("file", None, cx);
+        project.mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
+        buffer
+    });
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                search_query,
+                false,
+                true,
+                false,
+                Default::default(),
+                PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
+                false,
+                None,
+            )
+            .unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            (path!("dir/one.rs").to_string(), vec![8..12]),
+            (path!("dir/one.ts").to_string(), vec![14..18]),
+            (path!("dir/two.rs").to_string(), vec![8..12]),
+            (path!("dir/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,
+                false,
+                Default::default(),
+                PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
+                false,
+                None,
+            )
+            .unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            (path!("dir/one.ts").to_string(), vec![14..18]),
+            (path!("dir/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,
+                false,
+                Default::default(),
+                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
+                false,
+                None,
+            )
+            .unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            (path!("dir/one.rs").to_string(), vec![8..12]),
+            (path!("dir/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,
+                false,
+                Default::default(),
+                PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
+                    .unwrap(),
+                false,
+                None,
+            )
+            .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) {
     init_test(cx);

crates/search/src/project_search.rs 🔗

@@ -164,7 +164,7 @@ pub fn init(cx: &mut App) {
     .detach();
 }
 
-fn is_contains_uppercase(str: &str) -> bool {
+fn contains_uppercase(str: &str) -> bool {
     str.chars().any(|c| c.is_uppercase())
 }
 
@@ -767,7 +767,7 @@ impl ProjectSearchView {
                         let query = this.search_query_text(cx);
                         if !query.is_empty()
                             && this.search_options.contains(SearchOptions::CASE_SENSITIVE)
-                                != is_contains_uppercase(&query)
+                                != contains_uppercase(&query)
                         {
                             this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
                         }
@@ -1323,7 +1323,7 @@ impl ProjectSearchView {
         if EditorSettings::get_global(cx).use_smartcase_search
             && !query.is_empty()
             && self.search_options.contains(SearchOptions::CASE_SENSITIVE)
-                != is_contains_uppercase(query)
+                != contains_uppercase(query)
         {
             self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
         }