From 8cdbff99e2d599c4cbca6a67b2be9950fc824f46 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:37:07 +0800 Subject: [PATCH] feat(search): show recent then the rest on the empty All tab Before anything was typed, All showed only each tab's highlights: this window's other tabs, actions already used, and sessions from this directory. A fresh window had next to nothing to show. Each tab now leads with its recent rows, is topped up from the rest of the tab to five, and ends with a row into the tab for whatever did not fit. --- src/ui/search/sources.rs | 82 +++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/src/ui/search/sources.rs b/src/ui/search/sources.rs index 7857335a..af71211e 100644 --- a/src/ui/search/sources.rs +++ b/src/ui/search/sources.rs @@ -143,7 +143,7 @@ impl Catalog { return sources .into_iter() .filter_map(|s| { - let rows: Vec = s.highlights(cx).into_iter().map(Row::Item).collect(); + let rows = recent_then_rest(s.as_ref(), cx); (!rows.is_empty()).then(|| Section { title: Some(s.tab().title().into()), rows, @@ -181,6 +181,47 @@ impl Catalog { } } +/// A tab's rows on the All tab before anything is typed: what it has been +/// used for lately first, topped up from the rest of the tab to +/// [`ALL_TAB_ROWS`], then a row into the tab for whatever did not fit. An +/// empty query is someone looking around, and a list that only shows what they +/// have already used has nothing to show a fresh window — so every tab that +/// has anything is on the page, and the way into all of it is one row away. +fn recent_then_rest(source: &dyn Source, cx: &App) -> Vec { + let mut shown = source.highlights(cx); + shown.truncate(ALL_TAB_ROWS); + let rest: Vec = source + .browse(cx) + .into_iter() + .flat_map(|section| section.rows) + .filter_map(|row| match row { + Row::Item(item) => Some(item), + Row::More { .. } => None, + }) + .collect(); + let mut hidden = 0; + for item in rest { + if shown + .iter() + .any(|s| s.kind == item.kind && s.title == item.title) + { + continue; + } + match shown.len() < ALL_TAB_ROWS { + true => shown.push(item), + false => hidden += 1, + } + } + let mut rows: Vec = shown.into_iter().map(Row::Item).collect(); + if hidden > 0 { + rows.push(Row::More { + tab: source.tab(), + hidden, + }); + } + rows +} + /// A search result: one section, no header — the order is the answer. fn untitled(hits: Vec<(i32, Item)>) -> Vec
{ if hits.is_empty() { @@ -596,9 +637,9 @@ mod tests { }); } - /// Before anything is typed the All tab offers this window's other tabs — - /// the likeliest thing wanted — and never another workspace's, a launcher, - /// or a header over nothing. + /// Before anything is typed the All tab leads with this window's other + /// tabs — the likeliest thing wanted — and tops the section up from the + /// rest of the tab, so a tab with anything in it is never missing. #[gpui::test] fn the_empty_all_tab_leads_with_this_windows_other_tabs(cx: &mut TestAppContext) { with_config(cx); @@ -609,13 +650,12 @@ mod tests { let catalog = Catalog::new(Vec::new(), terminals, Vec::new()); cx.update(|cx| { let sections = catalog.sections(SearchTab::All, "", cx); - assert_eq!( - sections.len(), - 1, - "no recent actions and no hosts: no headers" - ); + assert_eq!(sections.len(), 1, "no actions and no hosts: no headers"); assert_eq!(sections[0].title.as_deref(), Some("Terminals")); - assert_eq!(row_titles(§ions[0]), vec!["previous", "older"]); + assert_eq!( + row_titles(§ions[0]), + vec!["previous", "older", "elsewhere", "Shell: zsh"] + ); }); } @@ -654,9 +694,9 @@ mod tests { } /// Before anything is typed, the All tab offers to pick up where you left - /// off here — and never a session from some other project. + /// off here first; past its rows, the rest of the tab is one row away. #[gpui::test] - fn the_empty_all_tab_offers_only_this_directorys_sessions(cx: &mut TestAppContext) { + fn the_empty_all_tab_leads_with_this_directorys_sessions(cx: &mut TestAppContext) { with_config(cx); let mut catalog = Catalog::new(Vec::new(), Vec::new(), Vec::new()); catalog.sessions = (0..5) @@ -668,13 +708,21 @@ mod tests { let sections = catalog.sections(SearchTab::All, "", cx); assert_eq!(sections.len(), 1); assert_eq!(sections[0].title.as_deref(), Some("Sessions")); - assert_eq!(row_titles(§ions[0]), vec!["here 0", "here 1", "here 2"]); + let rows = §ions[0].rows; + assert_eq!(rows.len(), ALL_TAB_ROWS + 1); + assert_eq!(rows[0].item().map(|i| i.title.as_ref()), Some("here 0")); + assert!(matches!( + rows[ALL_TAB_ROWS], + Row::More { + tab: SearchTab::Sessions, + hidden: 1 + } + )); + // Nothing ran here: the section still shows, from everywhere. catalog.sessions_here = 0; - assert!( - catalog.sections(SearchTab::All, "", cx).is_empty(), - "no session ran here, so none is offered" - ); + let sections = catalog.sections(SearchTab::All, "", cx); + assert_eq!(sections[0].title.as_deref(), Some("Sessions")); // The tab itself lists them all, here first. let own = catalog.sections(SearchTab::Sessions, "", cx);