From 171e7cb4a7e470f9cbd6580b2b268d445ffb620b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:59:32 -0300 Subject: [PATCH] sidebar: Improve behavior of "view more" button (#51105) This PR adjusts the "View More" button in the sidebar to expose threads in batches of 5. Once you've expanded the whole available set, a button to collapse the list back to the default number appears at the bottom. Similarly, as soon as you expand the list even once, a button in the group header shows up that does the same thing. No release notes because this is still under feature flag. Release Notes: - N/A --- assets/icons/list_collapse.svg | 8 +- crates/sidebar/src/sidebar.rs | 191 ++++++++++++++++++++++++++++----- 2 files changed, 169 insertions(+), 30 deletions(-) diff --git a/assets/icons/list_collapse.svg b/assets/icons/list_collapse.svg index f18bc550b90228c2f689848b86cfc5bea3d6ff50..dbdb2aaa4537c25ba1867d4957c23819af425835 100644 --- a/assets/icons/list_collapse.svg +++ b/assets/icons/list_collapse.svg @@ -1 +1,7 @@ - + + + + + + + diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 45a56f7af203e8ffe01b8590f916b439a57c52fb..d8bfae85bcd40654086c05d52d0004c618055c31 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -89,6 +89,7 @@ enum ListEntry { ViewMore { path_list: PathList, remaining_count: usize, + is_fully_expanded: bool, }, NewThread { path_list: PathList, @@ -174,7 +175,7 @@ pub struct Sidebar { focused_thread: Option, active_entry_index: Option, collapsed_groups: HashSet, - expanded_groups: HashSet, + expanded_groups: HashMap, } impl EventEmitter for Sidebar {} @@ -269,7 +270,7 @@ impl Sidebar { focused_thread: None, active_entry_index: None, collapsed_groups: HashSet::new(), - expanded_groups: HashSet::new(), + expanded_groups: HashMap::new(), } } @@ -579,21 +580,20 @@ impl Sidebar { } let total = threads.len(); - let show_view_more = - total > DEFAULT_THREADS_SHOWN && !self.expanded_groups.contains(&path_list); - let count = if show_view_more { - DEFAULT_THREADS_SHOWN - } else { - total - }; + let extra_batches = self.expanded_groups.get(&path_list).copied().unwrap_or(0); + let threads_to_show = + DEFAULT_THREADS_SHOWN + (extra_batches * DEFAULT_THREADS_SHOWN); + let count = threads_to_show.min(total); + let is_fully_expanded = count >= total; entries.extend(threads.into_iter().take(count)); - if show_view_more { + if total > DEFAULT_THREADS_SHOWN { entries.push(ListEntry::ViewMore { path_list: path_list.clone(), - remaining_count: total - DEFAULT_THREADS_SHOWN, + remaining_count: total.saturating_sub(count), + is_fully_expanded, }); } @@ -632,10 +632,13 @@ impl Sidebar { let had_notifications = self.has_notifications(cx); + let scroll_position = self.list_state.logical_scroll_top(); + self.rebuild_contents(cx); self.recompute_active_entry_index(cx); self.list_state.reset(self.contents.entries.len()); + self.list_state.scroll_to(scroll_position); if had_notifications != self.has_notifications(cx) { multi_workspace.update(cx, |_, cx| { @@ -720,7 +723,15 @@ impl Sidebar { ListEntry::ViewMore { path_list, remaining_count, - } => self.render_view_more(ix, path_list, *remaining_count, is_selected, cx), + is_fully_expanded, + } => self.render_view_more( + ix, + path_list, + *remaining_count, + *is_fully_expanded, + is_selected, + cx, + ), ListEntry::NewThread { path_list, workspace, @@ -765,7 +776,11 @@ impl Sidebar { let workspace_for_new_thread = workspace.clone(); let workspace_for_remove = workspace.clone(); // let workspace_for_activate = workspace.clone(); + let path_list_for_toggle = path_list.clone(); + let path_list_for_collapse = path_list.clone(); + let view_more_expanded = self.expanded_groups.contains_key(path_list); + let multi_workspace = self.multi_workspace.upgrade(); let workspace_count = multi_workspace .as_ref() @@ -853,6 +868,25 @@ impl Sidebar { )), ) }) + .when(view_more_expanded && !is_collapsed, |this| { + this.child( + IconButton::new( + SharedString::from(format!("project-header-collapse-{}", ix)), + IconName::ListCollapse, + ) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Collapse Displayed Threads")) + .on_click(cx.listener({ + let path_list_for_collapse = path_list_for_collapse.clone(); + move |this, _, _window, cx| { + this.selection = None; + this.expanded_groups.remove(&path_list_for_collapse); + this.update_entries(cx); + } + })), + ) + }) .when(has_threads, |this| { this.child( IconButton::new(ib_id, IconName::NewThread) @@ -1031,9 +1065,18 @@ impl Sidebar { let workspace = workspace.clone(); self.activate_thread(session_info, &workspace, window, cx); } - ListEntry::ViewMore { path_list, .. } => { + ListEntry::ViewMore { + path_list, + is_fully_expanded, + .. + } => { let path_list = path_list.clone(); - self.expanded_groups.insert(path_list); + if *is_fully_expanded { + self.expanded_groups.remove(&path_list); + } else { + let current = self.expanded_groups.get(&path_list).copied().unwrap_or(0); + self.expanded_groups.insert(path_list, current + 1); + } self.update_entries(cx); } ListEntry::NewThread { workspace, .. } => { @@ -1202,32 +1245,42 @@ impl Sidebar { ix: usize, path_list: &PathList, remaining_count: usize, + is_fully_expanded: bool, is_selected: bool, cx: &mut Context, ) -> AnyElement { let path_list = path_list.clone(); let id = SharedString::from(format!("view-more-{}", ix)); - let count = format!("({})", remaining_count); + let (icon, label) = if is_fully_expanded { + (IconName::ListCollapse, "Collapse List") + } else { + (IconName::Plus, "View More") + }; ListItem::new(id) .focused(is_selected) .child( h_flex() - .px_1() - .py_1p5() + .p_1() .gap_1p5() - .child( - Icon::new(IconName::Plus) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("View More").color(Color::Muted)) - .child(Label::new(count).color(Color::Muted).size(LabelSize::Small)), + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Label::new(label).color(Color::Muted)) + .when(!is_fully_expanded, |this| { + this.child( + Label::new(format!("({})", remaining_count)) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))), + ) + }), ) .on_click(cx.listener(move |this, _, _window, cx| { this.selection = None; - this.expanded_groups.insert(path_list.clone()); + if is_fully_expanded { + this.expanded_groups.remove(&path_list); + } else { + let current = this.expanded_groups.get(&path_list).copied().unwrap_or(0); + this.expanded_groups.insert(path_list.clone(), current + 1); + } this.update_entries(cx); })) .into_any_element() @@ -1660,9 +1713,15 @@ mod tests { ) } ListEntry::ViewMore { - remaining_count, .. + remaining_count, + is_fully_expanded, + .. } => { - format!(" + View More ({}){}", remaining_count, selected) + if *is_fully_expanded { + format!(" - Collapse{}", selected) + } else { + format!(" + View More ({}){}", remaining_count, selected) + } } ListEntry::NewThread { .. } => { format!(" [+ New Thread]{}", selected) @@ -1824,6 +1883,78 @@ mod tests { ); } + #[gpui::test] + async fn test_view_more_batched_expansion(cx: &mut TestAppContext) { + let project = init_test_project("/my-project", cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let sidebar = setup_sidebar(&multi_workspace, cx); + + let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]); + // Create 17 threads: initially shows 5, then 10, then 15, then all 17 with Collapse + save_n_test_threads(17, &path_list, cx).await; + + multi_workspace.update_in(cx, |_, _window, cx| cx.notify()); + cx.run_until_parked(); + + // Initially shows 5 threads + View More (12 remaining) + let entries = visible_entries_as_strings(&sidebar, cx); + assert_eq!(entries.len(), 7); // header + 5 threads + View More + assert!(entries.iter().any(|e| e.contains("View More (12)"))); + + // Focus and navigate to View More, then confirm to expand by one batch + open_and_focus_sidebar(&sidebar, &multi_workspace, cx); + for _ in 0..7 { + cx.dispatch_action(SelectNext); + } + cx.dispatch_action(Confirm); + cx.run_until_parked(); + + // Now shows 10 threads + View More (7 remaining) + let entries = visible_entries_as_strings(&sidebar, cx); + assert_eq!(entries.len(), 12); // header + 10 threads + View More + assert!(entries.iter().any(|e| e.contains("View More (7)"))); + + // Expand again by one batch + sidebar.update_in(cx, |s, _window, cx| { + let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0); + s.expanded_groups.insert(path_list.clone(), current + 1); + s.update_entries(cx); + }); + cx.run_until_parked(); + + // Now shows 15 threads + View More (2 remaining) + let entries = visible_entries_as_strings(&sidebar, cx); + assert_eq!(entries.len(), 17); // header + 15 threads + View More + assert!(entries.iter().any(|e| e.contains("View More (2)"))); + + // Expand one more time - should show all 17 threads with Collapse button + sidebar.update_in(cx, |s, _window, cx| { + let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0); + s.expanded_groups.insert(path_list.clone(), current + 1); + s.update_entries(cx); + }); + cx.run_until_parked(); + + // All 17 threads shown with Collapse button + let entries = visible_entries_as_strings(&sidebar, cx); + assert_eq!(entries.len(), 19); // header + 17 threads + Collapse + assert!(!entries.iter().any(|e| e.contains("View More"))); + assert!(entries.iter().any(|e| e.contains("Collapse"))); + + // Click collapse - should go back to showing 5 threads + sidebar.update_in(cx, |s, _window, cx| { + s.expanded_groups.remove(&path_list); + s.update_entries(cx); + }); + cx.run_until_parked(); + + // Back to initial state: 5 threads + View More (12 remaining) + let entries = visible_entries_as_strings(&sidebar, cx); + assert_eq!(entries.len(), 7); // header + 5 threads + View More + assert!(entries.iter().any(|e| e.contains("View More (12)"))); + } + #[gpui::test] async fn test_collapse_and_expand_group(cx: &mut TestAppContext) { let project = init_test_project("/my-project", cx).await; @@ -1984,6 +2115,7 @@ mod tests { ListEntry::ViewMore { path_list: expanded_path.clone(), remaining_count: 42, + is_fully_expanded: false, }, // Collapsed project header ListEntry::ProjectHeader { @@ -2237,10 +2369,11 @@ mod tests { cx.dispatch_action(Confirm); cx.run_until_parked(); - // All 8 threads should now be visible, no "View More" + // All 8 threads should now be visible with a "Collapse" button let entries = visible_entries_as_strings(&sidebar, cx); - assert_eq!(entries.len(), 9); // header + 8 threads + assert_eq!(entries.len(), 10); // header + 8 threads + Collapse button assert!(!entries.iter().any(|e| e.contains("View More"))); + assert!(entries.iter().any(|e| e.contains("Collapse"))); } #[gpui::test]