mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
feat: render @-mention suggestions for empty queries
- This is more helpful in helping users understand how the feature works.
This commit is contained in:
@@ -485,9 +485,7 @@ impl App<'_> {
|
||||
}
|
||||
}
|
||||
AppEvent::StartFileSearch(query) => {
|
||||
if !query.is_empty() {
|
||||
self.file_search.on_user_query(query);
|
||||
}
|
||||
self.file_search.on_user_query(query);
|
||||
}
|
||||
AppEvent::FileSearchResult { query, matches } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
|
||||
@@ -603,26 +603,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -83,6 +83,23 @@ 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.
|
||||
return;
|
||||
@@ -196,3 +213,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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user