From f199d2e2e5d57ac9ae8fb9eb759d91354742e7a2 Mon Sep 17 00:00:00 2001 From: Alexander Embiricos Date: Tue, 19 Aug 2025 10:06:45 -0700 Subject: [PATCH] cr: move empty query logic into main state machine --- codex-rs/tui/src/app.rs | 3 + codex-rs/tui/src/app_event.rs | 4 + codex-rs/tui/src/bottom_pane/chat_composer.rs | 4 + codex-rs/tui/src/file_search.rs | 89 +++++++++++-------- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4d818e981a..2b31942e64 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -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); diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 1afffd756a..3e77185db7 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -51,6 +51,10 @@ pub(crate) enum AppEvent { matches: Vec, }, + /// 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), diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 5a438f9272..4a02219cca 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -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; } }; diff --git a/codex-rs/tui/src/file_search.rs b/codex-rs/tui/src/file_search.rs index 9e5274518d..f90d01259f 100644 --- a/codex-rs/tui/src/file_search.rs +++ b/codex-rs/tui/src/file_search.rs @@ -51,7 +51,7 @@ pub(crate) struct FileSearchManager { struct SearchState { /// Latest query typed by user (updated every keystroke). - latest_query: String, + latest_query: Option, /// 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,