diff --git a/crates/agent/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs index 9ccebd80e85..485084a406e 100644 --- a/crates/agent/src/tools/grep_tool.rs +++ b/crates/agent/src/tools/grep_tool.rs @@ -196,7 +196,7 @@ impl AgentTool for GrepTool { has_more_matches = true; break; } - Some(SearchResult::WaitingForScan) => continue, + Some(SearchResult::WaitingForScan | SearchResult::Searching) => continue, None => break, }; if ranges.is_empty() { diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index b7479f95624..d5e4d046b4e 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -5295,7 +5295,7 @@ async fn test_project_search( "Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call." ) } - SearchResult::WaitingForScan => {} + SearchResult::WaitingForScan | SearchResult::Searching => {} }; } diff --git a/crates/project/src/project_search.rs b/crates/project/src/project_search.rs index 2db893538c8..e865bb4d5cb 100644 --- a/crates/project/src/project_search.rs +++ b/crates/project/src/project_search.rs @@ -424,8 +424,12 @@ impl Search { worktree.as_local().map(|local| local.scan_complete()) }); if let Some(scan_complete) = scan_complete { - _ = results_tx.send(SearchResult::WaitingForScan).await; - scan_complete.await; + let mut scan_complete = pin!(scan_complete); + if scan_complete.as_mut().now_or_never().is_none() { + _ = results_tx.send(SearchResult::WaitingForScan).await; + scan_complete.await; + _ = results_tx.send(SearchResult::Searching).await; + } } let (mut snapshot, worktree_settings) = worktree diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index b04bf128974..83b4c585f14 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -26,6 +26,7 @@ pub enum SearchResult { }, LimitReached, WaitingForScan, + Searching, } #[derive(Clone, Copy, PartialEq)] diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 984d1de057c..6997435eb2a 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -12313,7 +12313,8 @@ async fn search( SearchResult::Buffer { buffer, ranges } => { results.entry(buffer).or_insert(ranges); } - SearchResult::LimitReached | SearchResult::WaitingForScan => {} + SearchResult::LimitReached | SearchResult::WaitingForScan | SearchResult::Searching => { + } } } Ok(results diff --git a/crates/project_benchmarks/src/main.rs b/crates/project_benchmarks/src/main.rs index cdeb8ed780e..054b5eb95a5 100644 --- a/crates/project_benchmarks/src/main.rs +++ b/crates/project_benchmarks/src/main.rs @@ -210,11 +210,13 @@ fn main() -> Result<(), anyhow::Error> { first_match = Some(time); println!("First match found after {time:?}"); } - if let SearchResult::Buffer { ranges, .. } = match_result { - matched_files += 1; - matched_chunks += ranges.len(); - } else { - break; + match match_result { + SearchResult::Buffer { ranges, .. } => { + matched_files += 1; + matched_chunks += ranges.len(); + } + SearchResult::LimitReached => break, + SearchResult::WaitingForScan | SearchResult::Searching => continue, } } let elapsed = timer.elapsed(); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 6f2c2e3f223..825c0ba26c0 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -200,9 +200,13 @@ async fn do_search_and_assert( let mut buffers = Vec::new(); for expected_path in expected_paths { - let response = receiver.rx.recv().await.unwrap(); - let SearchResult::Buffer { buffer, .. } = response else { - panic!("incorrect result"); + let buffer = loop { + let response = receiver.rx.recv().await.unwrap(); + match response { + SearchResult::Buffer { buffer, .. } => break buffer, + SearchResult::LimitReached => panic!("incorrect result"), + SearchResult::WaitingForScan | SearchResult::Searching => continue, + } }; buffer.update(&mut cx, |buffer, cx| { assert_eq!( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6a092818a02..00966436595 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -235,15 +235,44 @@ pub struct ProjectSearch { active_query: Option, last_search_query_text: Option, search_id: usize, - no_results: Option, - limit_reached: bool, - waiting_for_scan: bool, + search_state: SearchState, search_history_cursor: SearchHistoryCursor, search_included_history_cursor: SearchHistoryCursor, search_excluded_history_cursor: SearchHistoryCursor, _excerpts_subscription: Subscription, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum SearchState { + #[default] + Idle, + Running(SearchActivity), + Completed(SearchCompletion), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SearchActivity { + Searching, + WaitingForScan, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SearchCompletion { + NoResults, + Results { limit_reached: bool }, +} + +impl SearchState { + fn limit_reached(self) -> bool { + matches!( + self, + SearchState::Completed(SearchCompletion::Results { + limit_reached: true + }) + ) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum InputPanel { Query, @@ -298,9 +327,7 @@ impl ProjectSearch { active_query: None, last_search_query_text: None, search_id: 0, - no_results: None, - limit_reached: false, - waiting_for_scan: false, + search_state: SearchState::Idle, search_history_cursor: Default::default(), search_included_history_cursor: Default::default(), search_excluded_history_cursor: Default::default(), @@ -323,9 +350,11 @@ impl ProjectSearch { active_query: self.active_query.clone(), last_search_query_text: self.last_search_query_text.clone(), search_id: self.search_id, - no_results: self.no_results, - limit_reached: self.limit_reached, - waiting_for_scan: false, + search_state: if self.pending_search.is_some() { + SearchState::Idle + } else { + self.search_state + }, search_history_cursor: self.search_history_cursor.clone(), search_included_history_cursor: self.search_included_history_cursor.clone(), search_excluded_history_cursor: self.search_excluded_history_cursor.clone(), @@ -413,6 +442,7 @@ impl ProjectSearch { self.search_id += 1; self.active_query = Some(query); self.match_ranges.clear(); + self.search_state = SearchState::Running(SearchActivity::Searching); self.pending_search = Some(cx.spawn(async move |project_search, cx| { let SearchResults { rx, _task_handle } = search; @@ -423,19 +453,16 @@ impl ProjectSearch { project_search .excerpts .update(cx, |excerpts, cx| excerpts.clear(cx)); - project_search.no_results = Some(true); - project_search.limit_reached = false; - project_search.waiting_for_scan = false; }) .ok()?; let mut limit_reached = false; while let Some(results) = matches.next().await { - let (buffers_with_ranges, has_reached_limit, is_waiting_for_scan) = cx + let (buffers_with_ranges, has_reached_limit, search_activity) = cx .background_executor() .spawn(async move { let mut limit_reached = false; - let mut waiting_for_scan = false; + let mut search_activity = None; let mut buffers_with_ranges = Vec::with_capacity(results.len()); for result in results { match result { @@ -446,18 +473,21 @@ impl ProjectSearch { limit_reached = true; } project::search::SearchResult::WaitingForScan => { - waiting_for_scan = true; + search_activity = Some(SearchActivity::WaitingForScan); + } + project::search::SearchResult::Searching => { + search_activity = Some(SearchActivity::Searching); } } } - (buffers_with_ranges, limit_reached, waiting_for_scan) + (buffers_with_ranges, limit_reached, search_activity) }) .await; limit_reached |= has_reached_limit; - if is_waiting_for_scan { + if let Some(search_activity) = search_activity { project_search .update(cx, |project_search, cx| { - project_search.waiting_for_scan = true; + project_search.search_state = SearchState::Running(search_activity); cx.notify(); }) .ok()?; @@ -495,11 +525,11 @@ impl ProjectSearch { project_search .update(cx, |project_search, cx| { - if !project_search.match_ranges.is_empty() { - project_search.no_results = Some(false); - } - project_search.limit_reached = limit_reached; - project_search.waiting_for_scan = false; + project_search.search_state = if project_search.match_ranges.is_empty() { + SearchState::Completed(SearchCompletion::NoResults) + } else { + SearchState::Completed(SearchCompletion::Results { limit_reached }) + }; project_search.pending_search.take(); cx.notify(); }) @@ -531,36 +561,26 @@ impl Render for ProjectSearchView { .child(self.results_editor.clone()) } else { let model = self.entity.read(cx); - let has_no_results = model.no_results.unwrap_or(false); - let is_search_underway = model.pending_search.is_some(); - let is_waiting_for_scan = model.waiting_for_scan; - let heading_text = if is_waiting_for_scan { - "Loading project…" - } else if is_search_underway { - "Searching…" - } else if has_no_results { - "No Results" - } else { - "Search All Files" + let heading_text = match model.search_state { + SearchState::Running(SearchActivity::WaitingForScan) => "Loading project…", + SearchState::Running(SearchActivity::Searching) => "Searching…", + SearchState::Completed(SearchCompletion::NoResults) => "No Results", + _ => "Search All Files", }; let heading_text = div() .justify_center() .child(Label::new(heading_text).size(LabelSize::Large)); - let page_content: Option = if let Some(no_results) = model.no_results { - if model.pending_search.is_none() && no_results { - Some( - Label::new("No results found in this project for the provided query") - .size(LabelSize::Small) - .into_any_element(), - ) - } else { - None - } - } else { - Some(self.landing_text_minor(cx).into_any_element()) + let page_content: Option = match model.search_state { + SearchState::Idle => Some(self.landing_text_minor(cx).into_any_element()), + SearchState::Completed(SearchCompletion::NoResults) => Some( + Label::new("No results found in this project for the provided query") + .size(LabelSize::Small) + .into_any_element(), + ), + _ => None, }; let page_content = page_content.map(|text| div().child(text)); @@ -2179,16 +2199,19 @@ impl Render for ProjectSearchBar { }; let theme_colors = cx.theme().colors(); let project_search = search.entity.read(cx); - let limit_reached = project_search.limit_reached; + let limit_reached = project_search.search_state.limit_reached(); let is_search_underway = project_search.pending_search.is_some(); let color_override = match ( - &project_search.pending_search, - project_search.no_results, + project_search.search_state, &project_search.active_query, &project_search.last_search_query_text, ) { - (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), + ( + SearchState::Completed(SearchCompletion::NoResults), + Some(query), + Some(previous_query), + ) if query.as_str() == previous_query => Some(Color::Error), _ => None, };