cr: move empty query logic into main state machine

This commit is contained in:
Alexander Embiricos
2025-08-19 10:06:45 -07:00
parent bf46b34f30
commit f199d2e2e5
4 changed files with 61 additions and 39 deletions

View File

@@ -487,6 +487,9 @@ impl App<'_> {
AppEvent::StartFileSearch(query) => {
self.file_search.on_user_query(query);
}
AppEvent::StopFileSearch => {
self.file_search.reset();
}
AppEvent::FileSearchResult { query, matches } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.apply_file_search_result(query, matches);

View File

@@ -51,6 +51,10 @@ pub(crate) enum AppEvent {
matches: Vec<FileMatch>,
},
/// Stop or reset any pending/active file search in the manager.
/// Used when the user cancels the `@` popup or completes a mention.
StopFileSearch,
/// Result of computing a `/diff` command.
DiffResult(String),

View File

@@ -328,6 +328,7 @@ impl ChatComposer {
self.dismissed_file_popup_token = Some(tok.to_string());
}
self.active_popup = ActivePopup::None;
self.app_event_tx.send(AppEvent::StopFileSearch);
(InputResult::None, true)
}
KeyEvent {
@@ -343,6 +344,7 @@ impl ChatComposer {
// Drop popup borrow before using self mutably again.
self.insert_selected_path(&sel_path);
self.active_popup = ActivePopup::None;
self.app_event_tx.send(AppEvent::StopFileSearch);
return (InputResult::None, true);
}
(InputResult::None, false)
@@ -594,6 +596,8 @@ impl ChatComposer {
None => {
self.active_popup = ActivePopup::None;
self.dismissed_file_popup_token = None;
// No active @token under cursor; stop any search.
self.app_event_tx.send(AppEvent::StopFileSearch);
return;
}
};

View File

@@ -51,7 +51,7 @@ pub(crate) struct FileSearchManager {
struct SearchState {
/// Latest query typed by user (updated every keystroke).
latest_query: String,
latest_query: Option<String>,
/// true if a search is currently scheduled.
is_search_scheduled: bool,
@@ -69,7 +69,7 @@ impl FileSearchManager {
pub fn new(search_dir: PathBuf, tx: AppEventSender) -> Self {
Self {
state: Arc::new(Mutex::new(SearchState {
latest_query: String::new(),
latest_query: None,
is_search_scheduled: false,
active_search: None,
})),
@@ -83,31 +83,13 @@ impl FileSearchManager {
{
#[expect(clippy::unwrap_used)]
let mut st = self.state.lock().unwrap();
// If the query is empty, build quick suggestions immediately and return.
// Do this BEFORE the unchanged short-circuit so the initial empty
// query ("@") still yields results even though latest_query starts empty.
if query.is_empty() {
let search_dir = self.search_dir.clone();
let tx = self.app_tx.clone();
std::thread::spawn(move || {
let max_total = MAX_FILE_SEARCH_RESULTS.get();
let matches = collect_top_level_suggestions(&search_dir, max_total);
tx.send(AppEvent::FileSearchResult {
query: String::new(),
matches,
});
});
return;
}
if query == st.latest_query {
// No change, nothing to do.
// If the query hasn't changed, nothing to do.
if st.latest_query.as_deref() == Some(query.as_str()) {
return;
}
// Update latest query.
st.latest_query.clear();
st.latest_query.push_str(&query);
st.latest_query = Some(query.clone());
// If there is an in-flight search that is definitely obsolete,
// cancel it now.
@@ -148,30 +130,59 @@ impl FileSearchManager {
// The debounce timer has expired, so start a search using the
// latest query.
let cancellation_token = Arc::new(AtomicBool::new(false));
let token = cancellation_token.clone();
let query = {
let latest_query_opt = {
#[expect(clippy::unwrap_used)]
let mut st = state.lock().unwrap();
let query = st.latest_query.clone();
let q = st.latest_query.clone();
st.is_search_scheduled = false;
st.active_search = Some(ActiveSearch {
query: query.clone(),
cancellation_token: token,
});
query
q
};
FileSearchManager::spawn_file_search(
query,
search_dir,
tx_clone,
cancellation_token,
state,
);
let Some(query) = latest_query_opt else {
return;
};
if query.is_empty() {
// Quick, synchronous top-level suggestions for empty query.
let max_total = MAX_FILE_SEARCH_RESULTS.get();
let matches = collect_top_level_suggestions(&search_dir, max_total);
tx_clone.send(AppEvent::FileSearchResult { query, matches });
} else {
// Full-text file search for non-empty query.
let cancellation_token = Arc::new(AtomicBool::new(false));
let token = cancellation_token.clone();
{
#[expect(clippy::unwrap_used)]
let mut st = state.lock().unwrap();
st.active_search = Some(ActiveSearch {
query: query.clone(),
cancellation_token: token,
});
}
FileSearchManager::spawn_file_search(
query,
search_dir,
tx_clone,
cancellation_token,
state,
);
}
});
}
/// Reset any scheduled or active search and clear the last query.
pub fn reset(&self) {
#[expect(clippy::unwrap_used)]
let mut st = self.state.lock().unwrap();
st.latest_query = None;
st.is_search_scheduled = false;
if let Some(active) = &st.active_search {
active.cancellation_token.store(true, Ordering::Relaxed);
}
st.active_search = None;
}
fn spawn_file_search(
query: String,
search_dir: PathBuf,