Codex Handbook
tui/src/file_search.rs 133 lines
//! Session-based orchestration for `@` file searches.//!//! `ChatComposer` publishes every change of the `@token` as//! `AppEvent::StartFileSearch(query)`. This manager owns a single//! `codex-file-search` session for the current search root, updates the query//! on every keystroke, and drops the session when the query becomes empty.use codex_file_search as file_search;use std::path::PathBuf;use std::sync::Arc;use std::sync::Mutex;use crate::app_event::AppEvent;use crate::app_event_sender::AppEventSender;pub(crate) struct FileSearchManager {    state: Arc<Mutex<SearchState>>,    search_dir: PathBuf,    app_tx: AppEventSender,}struct SearchState {    latest_query: String,    session: Option<file_search::FileSearchSession>,    session_token: usize,}impl FileSearchManager {    pub fn new(search_dir: PathBuf, tx: AppEventSender) -> Self {        Self {            state: Arc::new(Mutex::new(SearchState {                latest_query: String::new(),                session: None,                session_token: 0,            })),            search_dir,            app_tx: tx,        }    }    /// Updates the directory used for file searches.    /// This should be called when the session's CWD changes on resume.    /// Drops the current session so it will be recreated with the new directory on next query.    pub fn update_search_dir(&mut self, new_dir: PathBuf) {        self.search_dir = new_dir;        #[expect(clippy::unwrap_used)]        let mut st = self.state.lock().unwrap();        st.session.take();        st.latest_query.clear();    }    /// Call whenever the user edits the `@` token.    pub fn on_user_query(&self, query: String) {        #[expect(clippy::unwrap_used)]        let mut st = self.state.lock().unwrap();        if query == st.latest_query {            return;        }        st.latest_query.clear();        st.latest_query.push_str(&query);        if query.is_empty() {            st.session.take();            return;        }        if st.session.is_none() {            self.start_session_locked(&mut st);        }        if let Some(session) = st.session.as_ref() {            session.update_query(&query);        }    }    fn start_session_locked(&self, st: &mut SearchState) {        st.session_token = st.session_token.wrapping_add(1);        let session_token = st.session_token;        let reporter = Arc::new(TuiSessionReporter {            state: self.state.clone(),            app_tx: self.app_tx.clone(),            session_token,        });        let session = file_search::create_session(            vec![self.search_dir.clone()],            file_search::FileSearchOptions {                compute_indices: true,                ..Default::default()            },            reporter,            /*cancel_flag*/ None,        );        match session {            Ok(session) => st.session = Some(session),            Err(err) => {                tracing::warn!("file search session failed to start: {err}");                st.session = None;            }        }    }}struct TuiSessionReporter {    state: Arc<Mutex<SearchState>>,    app_tx: AppEventSender,    session_token: usize,}impl TuiSessionReporter {    fn send_snapshot(&self, snapshot: &file_search::FileSearchSnapshot) {        #[expect(clippy::unwrap_used)]        let st = self.state.lock().unwrap();        if st.session_token != self.session_token            || st.latest_query.is_empty()            || snapshot.query.is_empty()        {            return;        }        let query = snapshot.query.clone();        drop(st);        self.app_tx.send(AppEvent::FileSearchResult {            query,            matches: snapshot.matches.clone(),        });    }}impl file_search::SessionReporter for TuiSessionReporter {    fn on_update(&self, snapshot: &file_search::FileSearchSnapshot) {        self.send_snapshot(snapshot);    }    fn on_complete(&self) {}}