feat: render @-mention suggestions for empty queries

- This is more helpful in helping users understand how the feature
  works.
This commit is contained in:
Alexander Embiricos
2025-08-09 21:03:43 -07:00
parent 096bca2fa2
commit bf46b34f30
4 changed files with 111 additions and 28 deletions

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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>) {

View File

@@ -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;
}
}