mirror of
https://github.com/openai/codex.git
synced 2026-04-28 16:45:54 +00:00
683 lines
24 KiB
Markdown
683 lines
24 KiB
Markdown
# PR #2115: feat: render @-mention suggestions for empty queries
|
|
|
|
- URL: https://github.com/openai/codex/pull/2115
|
|
- Author: ae-openai
|
|
- Created: 2025-08-10 04:06:15 UTC
|
|
- Updated: 2025-08-19 17:38:19 UTC
|
|
- Changes: +153/-48, Files changed: 5, Commits: 3
|
|
|
|
## Description
|
|
|
|
- This is more helpful in helping users understand how the feature works.
|
|
|
|
## Full Diff
|
|
|
|
```diff
|
|
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
|
|
index d12eb76e8d..2b31942e64 100644
|
|
--- a/codex-rs/tui/src/app.rs
|
|
+++ b/codex-rs/tui/src/app.rs
|
|
@@ -485,9 +485,10 @@ impl App<'_> {
|
|
}
|
|
}
|
|
AppEvent::StartFileSearch(query) => {
|
|
- if !query.is_empty() {
|
|
- self.file_search.on_user_query(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 {
|
|
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<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),
|
|
|
|
diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs
|
|
index 71483f0495..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;
|
|
}
|
|
};
|
|
@@ -603,26 +607,16 @@ impl ChatComposer {
|
|
return;
|
|
}
|
|
|
|
- if !query.is_empty() {
|
|
- self.app_event_tx
|
|
- .send(AppEvent::StartFileSearch(query.clone()));
|
|
- }
|
|
+ self.app_event_tx
|
|
+ .send(AppEvent::StartFileSearch(query.clone()));
|
|
|
|
match &mut self.active_popup {
|
|
ActivePopup::File(popup) => {
|
|
- if query.is_empty() {
|
|
- popup.set_empty_prompt();
|
|
- } else {
|
|
- popup.set_query(&query);
|
|
- }
|
|
+ popup.set_query(&query);
|
|
}
|
|
_ => {
|
|
let mut popup = FileSearchPopup::new();
|
|
- if query.is_empty() {
|
|
- popup.set_empty_prompt();
|
|
- } else {
|
|
- popup.set_query(&query);
|
|
- }
|
|
+ popup.set_query(&query);
|
|
self.active_popup = ActivePopup::File(popup);
|
|
}
|
|
}
|
|
diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs
|
|
index a811a22a8c..c30a24f984 100644
|
|
--- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs
|
|
+++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs
|
|
@@ -54,17 +54,6 @@ impl FileSearchPopup {
|
|
}
|
|
}
|
|
|
|
- /// Put the popup into an "idle" state used for an empty query (just "@").
|
|
- /// Shows a hint instead of matches until the user types more characters.
|
|
- pub(crate) fn set_empty_prompt(&mut self) {
|
|
- self.display_query.clear();
|
|
- self.pending_query.clear();
|
|
- self.waiting = false;
|
|
- self.matches.clear();
|
|
- // Reset selection/scroll state when showing the empty prompt.
|
|
- self.state.reset();
|
|
- }
|
|
-
|
|
/// Replace matches when a `FileSearchResult` arrives.
|
|
/// Replace matches. Only applied when `query` matches `pending_query`.
|
|
pub(crate) fn set_matches(&mut self, query: &str, matches: Vec<FileMatch>) {
|
|
diff --git a/codex-rs/tui/src/file_search.rs b/codex-rs/tui/src/file_search.rs
|
|
index 8a05b2c165..829246a8c3 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<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,14 +83,13 @@ impl FileSearchManager {
|
|
{
|
|
#[expect(clippy::unwrap_used)]
|
|
let mut st = self.state.lock().unwrap();
|
|
- 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.
|
|
@@ -131,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();
|
|
st.is_search_scheduled = false;
|
|
- st.active_search = Some(ActiveSearch {
|
|
- query: query.clone(),
|
|
- cancellation_token: token,
|
|
- });
|
|
query
|
|
};
|
|
|
|
- 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,
|
|
@@ -196,3 +224,92 @@ impl FileSearchManager {
|
|
});
|
|
}
|
|
}
|
|
+
|
|
+/// Build a small, fast set of suggestions for an empty `@` mention.
|
|
+/// Strategy: list top-level non-hidden files first, then top-level directories
|
|
+/// (with trailing '/'), capped by `max_total`.
|
|
+fn collect_top_level_suggestions(
|
|
+ cwd: &std::path::Path,
|
|
+ max_total: usize,
|
|
+) -> Vec<file_search::FileMatch> {
|
|
+ use std::collections::HashSet;
|
|
+ use std::fs;
|
|
+
|
|
+ let mut seen: HashSet<String> = HashSet::new();
|
|
+ let mut out: Vec<file_search::FileMatch> = Vec::new();
|
|
+ let mut total_added: usize = 0;
|
|
+
|
|
+ // 1) Top-level non-hidden files in cwd (files only).
|
|
+ if let Ok(rd) = fs::read_dir(cwd) {
|
|
+ for entry in rd.flatten() {
|
|
+ if total_added >= max_total {
|
|
+ break;
|
|
+ }
|
|
+ let path = entry.path();
|
|
+ let file_name = match path.strip_prefix(cwd).ok().and_then(|p| p.to_str()) {
|
|
+ Some(s) => s,
|
|
+ None => continue,
|
|
+ };
|
|
+ if file_name.starts_with('.') {
|
|
+ continue;
|
|
+ }
|
|
+ match entry.file_type() {
|
|
+ Ok(ft) if ft.is_file() => {
|
|
+ push_mention_path(&mut out, &mut seen, &mut total_added, file_name.to_string());
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // 2) If still under cap, add top-level non-hidden directories (with trailing '/').
|
|
+ if total_added < max_total {
|
|
+ if let Ok(rd) = fs::read_dir(cwd) {
|
|
+ for entry in rd.flatten() {
|
|
+ if total_added >= max_total {
|
|
+ break;
|
|
+ }
|
|
+ let path = entry.path();
|
|
+ let file_name = match path.strip_prefix(cwd).ok().and_then(|p| p.to_str()) {
|
|
+ Some(s) => s,
|
|
+ None => continue,
|
|
+ };
|
|
+ if file_name.starts_with('.') {
|
|
+ continue;
|
|
+ }
|
|
+ if let Ok(ft) = entry.file_type() {
|
|
+ if ft.is_dir() {
|
|
+ push_mention_path(
|
|
+ &mut out,
|
|
+ &mut seen,
|
|
+ &mut total_added,
|
|
+ format!("{file_name}/"),
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if total_added > max_total {
|
|
+ out.truncate(max_total);
|
|
+ }
|
|
+ out
|
|
+}
|
|
+
|
|
+/// Insert a suggestion if not seen yet; updates `total_added` and returns true when inserted.
|
|
+fn push_mention_path(
|
|
+ out: &mut Vec<file_search::FileMatch>,
|
|
+ seen: &mut std::collections::HashSet<String>,
|
|
+ total_added: &mut usize,
|
|
+ rel: String,
|
|
+) {
|
|
+ if seen.insert(rel.clone()) {
|
|
+ out.push(file_search::FileMatch {
|
|
+ score: 0,
|
|
+ path: rel,
|
|
+ indices: None,
|
|
+ });
|
|
+ *total_added += 1;
|
|
+ }
|
|
+}
|
|
```
|
|
|
|
## Review Comments
|
|
|
|
### codex-rs/tui/src/file_search.rs
|
|
|
|
- Created: 2025-08-10 05:59:05 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2265128082
|
|
|
|
```diff
|
|
@@ -86,6 +86,23 @@ impl FileSearchManager {
|
|
{
|
|
#[allow(clippy::unwrap_used)]
|
|
let mut st = self.state.lock().unwrap();
|
|
+ // If the query is empty, build quick suggestions immediately and return.
|
|
```
|
|
|
|
> Do not take the lock until after the `query.is_empty()` check.
|
|
|
|
- Created: 2025-08-10 05:59:36 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2265128187
|
|
|
|
```diff
|
|
@@ -86,6 +86,23 @@ impl FileSearchManager {
|
|
{
|
|
#[allow(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;
|
|
+ }
|
|
+
|
|
```
|
|
|
|
> There is quite a bit of state management below that appear to be ignored by this PR, which concerns me.
|
|
|
|
- Created: 2025-08-19 17:19:38 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285874797
|
|
|
|
```diff
|
|
@@ -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>,
|
|
```
|
|
|
|
> I don't love this change because now `None` and `Some("")` are two seemingly different states?
|
|
>
|
|
> Just to make sure I understand, should `None` interpreted as, "popup has never been shown"? If so, maybe we should have a separate `bool` for that, which I think is a bit clearer.
|
|
|
|
- Created: 2025-08-19 17:22:40 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285881101
|
|
|
|
```diff
|
|
@@ -131,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.
|
|
```
|
|
|
|
> Why is this necessary now?
|
|
|
|
- Created: 2025-08-19 17:23:34 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285883038
|
|
|
|
```diff
|
|
@@ -196,3 +224,92 @@ impl FileSearchManager {
|
|
});
|
|
}
|
|
}
|
|
+
|
|
+/// Build a small, fast set of suggestions for an empty `@` mention.
|
|
+/// Strategy: list top-level non-hidden files first, then top-level directories
|
|
+/// (with trailing '/'), capped by `max_total`.
|
|
+fn collect_top_level_suggestions(
|
|
+ cwd: &std::path::Path,
|
|
+ max_total: usize,
|
|
+) -> Vec<file_search::FileMatch> {
|
|
+ use std::collections::HashSet;
|
|
```
|
|
|
|
> imports at the top, please
|
|
|
|
- Created: 2025-08-19 17:23:52 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285883601
|
|
|
|
```diff
|
|
@@ -196,3 +224,92 @@ impl FileSearchManager {
|
|
});
|
|
}
|
|
}
|
|
+
|
|
+/// Build a small, fast set of suggestions for an empty `@` mention.
|
|
+/// Strategy: list top-level non-hidden files first, then top-level directories
|
|
+/// (with trailing '/'), capped by `max_total`.
|
|
+fn collect_top_level_suggestions(
|
|
+ cwd: &std::path::Path,
|
|
+ max_total: usize,
|
|
+) -> Vec<file_search::FileMatch> {
|
|
+ use std::collections::HashSet;
|
|
+ use std::fs;
|
|
+
|
|
+ let mut seen: HashSet<String> = HashSet::new();
|
|
+ let mut out: Vec<file_search::FileMatch> = Vec::new();
|
|
+ let mut total_added: usize = 0;
|
|
+
|
|
+ // 1) Top-level non-hidden files in cwd (files only).
|
|
+ if let Ok(rd) = fs::read_dir(cwd) {
|
|
```
|
|
|
|
> Please swap your two-letter variable names for more descriptive ones.
|
|
|
|
- Created: 2025-08-19 17:25:21 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285886684
|
|
|
|
```diff
|
|
@@ -196,3 +224,92 @@ impl FileSearchManager {
|
|
});
|
|
}
|
|
}
|
|
+
|
|
+/// Build a small, fast set of suggestions for an empty `@` mention.
|
|
+/// Strategy: list top-level non-hidden files first, then top-level directories
|
|
+/// (with trailing '/'), capped by `max_total`.
|
|
+fn collect_top_level_suggestions(
|
|
+ cwd: &std::path::Path,
|
|
+ max_total: usize,
|
|
+) -> Vec<file_search::FileMatch> {
|
|
+ use std::collections::HashSet;
|
|
+ use std::fs;
|
|
+
|
|
+ let mut seen: HashSet<String> = HashSet::new();
|
|
+ let mut out: Vec<file_search::FileMatch> = Vec::new();
|
|
+ let mut total_added: usize = 0;
|
|
+
|
|
+ // 1) Top-level non-hidden files in cwd (files only).
|
|
+ if let Ok(rd) = fs::read_dir(cwd) {
|
|
+ for entry in rd.flatten() {
|
|
+ if total_added >= max_total {
|
|
+ break;
|
|
+ }
|
|
+ let path = entry.path();
|
|
+ let file_name = match path.strip_prefix(cwd).ok().and_then(|p| p.to_str()) {
|
|
+ Some(s) => s,
|
|
+ None => continue,
|
|
+ };
|
|
+ if file_name.starts_with('.') {
|
|
+ continue;
|
|
+ }
|
|
+ match entry.file_type() {
|
|
+ Ok(ft) if ft.is_file() => {
|
|
+ push_mention_path(&mut out, &mut seen, &mut total_added, file_name.to_string());
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // 2) If still under cap, add top-level non-hidden directories (with trailing '/').
|
|
+ if total_added < max_total {
|
|
+ if let Ok(rd) = fs::read_dir(cwd) {
|
|
+ for entry in rd.flatten() {
|
|
+ if total_added >= max_total {
|
|
+ break;
|
|
+ }
|
|
+ let path = entry.path();
|
|
+ let file_name = match path.strip_prefix(cwd).ok().and_then(|p| p.to_str()) {
|
|
+ Some(s) => s,
|
|
+ None => continue,
|
|
+ };
|
|
+ if file_name.starts_with('.') {
|
|
+ continue;
|
|
+ }
|
|
+ if let Ok(ft) = entry.file_type() {
|
|
+ if ft.is_dir() {
|
|
+ push_mention_path(
|
|
+ &mut out,
|
|
+ &mut seen,
|
|
+ &mut total_added,
|
|
+ format!("{file_name}/"),
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if total_added > max_total {
|
|
+ out.truncate(max_total);
|
|
+ }
|
|
+ out
|
|
+}
|
|
+
|
|
+/// Insert a suggestion if not seen yet; updates `total_added` and returns true when inserted.
|
|
+fn push_mention_path(
|
|
+ out: &mut Vec<file_search::FileMatch>,
|
|
+ seen: &mut std::collections::HashSet<String>,
|
|
```
|
|
|
|
> ```suggestion
|
|
> seen: &mut std::collections::HashSet<String>,
|
|
> ```
|
|
> ```suggestion
|
|
> seen: &mut HashSet<String>,
|
|
> ```
|
|
|
|
- Created: 2025-08-19 17:26:34 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285889018
|
|
|
|
```diff
|
|
@@ -131,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 });
|
|
```
|
|
|
|
> What's the downside of returning `FileSearchManager` here so the two codepaths are more similar?
|
|
|
|
- Created: 2025-08-19 17:32:25 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285900878
|
|
|
|
```diff
|
|
@@ -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>,
|
|
```
|
|
|
|
> That sounds like `active_search`?
|
|
|
|
- Created: 2025-08-19 17:38:19 UTC | Link: https://github.com/openai/codex/pull/2115#discussion_r2285912428
|
|
|
|
```diff
|
|
@@ -83,14 +83,13 @@ impl FileSearchManager {
|
|
{
|
|
#[expect(clippy::unwrap_used)]
|
|
let mut st = self.state.lock().unwrap();
|
|
- 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()) {
|
|
```
|
|
|
|
> Maybe this is changed to:
|
|
>
|
|
> ```suggestion
|
|
> if query == st.latest_query || query.is_empty()
|
|
> ``` |