Skip to content

Commit 7cfce60

Browse files
project_search: Add button to collapse/expand all excerpts (#41654)
<img width="500" height="834" alt="Screenshot 2025-11-03 at 12  59@2x" src="https://github.com/user-attachments/assets/15c5e1fc-2291-41b4-9eec-a8cfa5a446c7" /> Releases Note: - Added a button that allows to expand/collapse all project search excerpts at once. --------- Co-authored-by: Danilo Leal <[email protected]>
1 parent 45b7848 commit 7cfce60

File tree

9 files changed

+125
-8
lines changed

9 files changed

+125
-8
lines changed

assets/icons/chevron_down_up.svg

Lines changed: 4 additions & 0 deletions
Loading

assets/keymaps/default-linux.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@
407407
"bindings": {
408408
"escape": "project_search::ToggleFocus",
409409
"shift-find": "search::FocusSearch",
410+
"shift-enter": "project_search::ToggleAllSearchResults",
410411
"ctrl-shift-f": "search::FocusSearch",
411412
"ctrl-shift-h": "search::ToggleReplace",
412413
"alt-ctrl-g": "search::ToggleRegex",
@@ -479,6 +480,7 @@
479480
"alt-w": "search::ToggleWholeWord",
480481
"alt-find": "project_search::ToggleFilters",
481482
"alt-ctrl-f": "project_search::ToggleFilters",
483+
"shift-enter": "project_search::ToggleAllSearchResults",
482484
"ctrl-alt-shift-r": "search::ToggleRegex",
483485
"ctrl-alt-shift-x": "search::ToggleRegex",
484486
"alt-r": "search::ToggleRegex",

assets/keymaps/default-macos.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@
468468
"bindings": {
469469
"escape": "project_search::ToggleFocus",
470470
"cmd-shift-j": "project_search::ToggleFilters",
471+
"shift-enter": "project_search::ToggleAllSearchResults",
471472
"cmd-shift-f": "search::FocusSearch",
472473
"cmd-shift-h": "search::ToggleReplace",
473474
"alt-cmd-g": "search::ToggleRegex",
@@ -496,6 +497,7 @@
496497
"bindings": {
497498
"escape": "project_search::ToggleFocus",
498499
"cmd-shift-j": "project_search::ToggleFilters",
500+
"shift-enter": "project_search::ToggleAllSearchResults",
499501
"cmd-shift-h": "search::ToggleReplace",
500502
"alt-cmd-g": "search::ToggleRegex",
501503
"alt-cmd-x": "search::ToggleRegex"

assets/keymaps/default-windows.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@
488488
"alt-c": "search::ToggleCaseSensitive",
489489
"alt-w": "search::ToggleWholeWord",
490490
"alt-f": "project_search::ToggleFilters",
491+
"shift-enter": "project_search::ToggleAllSearchResults",
491492
"alt-r": "search::ToggleRegex",
492493
// "ctrl-shift-alt-x": "search::ToggleRegex",
493494
"ctrl-k shift-enter": "pane::TogglePinTab"

crates/breadcrumbs/src/breadcrumbs.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,21 @@ impl Render for Breadcrumbs {
100100

101101
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
102102

103+
let prefix_element = active_item.breadcrumb_prefix(window, cx);
104+
105+
let breadcrumbs = if let Some(prefix) = prefix_element {
106+
h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
107+
} else {
108+
breadcrumbs_stack
109+
};
110+
103111
match active_item
104112
.downcast::<Editor>()
105113
.map(|editor| editor.downgrade())
106114
{
107115
Some(editor) => element.child(
108116
ButtonLike::new("toggle outline view")
109-
.child(breadcrumbs_stack)
117+
.child(breadcrumbs)
110118
.style(ButtonStyle::Transparent)
111119
.on_click({
112120
let editor = editor.clone();
@@ -141,7 +149,7 @@ impl Render for Breadcrumbs {
141149
// Match the height and padding of the `ButtonLike` in the other arm.
142150
.h(rems_from_px(22.))
143151
.pl_1()
144-
.child(breadcrumbs_stack),
152+
.child(breadcrumbs),
145153
}
146154
}
147155
}

crates/icons/src/icons.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub enum IconName {
5353
Check,
5454
CheckDouble,
5555
ChevronDown,
56+
ChevronDownUp,
5657
ChevronLeft,
5758
ChevronRight,
5859
ChevronUp,

crates/search/src/project_search.rs

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ actions!(
5757
/// Moves to the next input field.
5858
NextField,
5959
/// Toggles the search filters panel.
60-
ToggleFilters
60+
ToggleFilters,
61+
/// Toggles collapse/expand state of all search result excerpts.
62+
ToggleAllSearchResults
6163
]
6264
);
6365

@@ -120,6 +122,20 @@ pub fn init(cx: &mut App) {
120122
ProjectSearchView::search_in_new(workspace, action, window, cx)
121123
});
122124

125+
register_workspace_action_for_present_search(
126+
workspace,
127+
|workspace, action: &ToggleAllSearchResults, window, cx| {
128+
if let Some(search_view) = workspace
129+
.active_item(cx)
130+
.and_then(|item| item.downcast::<ProjectSearchView>())
131+
{
132+
search_view.update(cx, |search_view, cx| {
133+
search_view.toggle_all_search_results(action, window, cx);
134+
});
135+
}
136+
},
137+
);
138+
123139
register_workspace_action_for_present_search(
124140
workspace,
125141
|workspace, _: &menu::Cancel, window, cx| {
@@ -219,6 +235,7 @@ pub struct ProjectSearchView {
219235
replace_enabled: bool,
220236
included_opened_only: bool,
221237
regex_language: Option<Arc<Language>>,
238+
results_collapsed: bool,
222239
_subscriptions: Vec<Subscription>,
223240
}
224241

@@ -651,6 +668,44 @@ impl Item for ProjectSearchView {
651668
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
652669
self.results_editor.breadcrumbs(theme, cx)
653670
}
671+
672+
fn breadcrumb_prefix(
673+
&self,
674+
_window: &mut Window,
675+
cx: &mut Context<Self>,
676+
) -> Option<gpui::AnyElement> {
677+
if !self.has_matches() {
678+
return None;
679+
}
680+
681+
let is_collapsed = self.results_collapsed;
682+
683+
let (icon, tooltip_label) = if is_collapsed {
684+
(IconName::ChevronUpDown, "Expand All Search Results")
685+
} else {
686+
(IconName::ChevronDownUp, "Collapse All Search Results")
687+
};
688+
689+
let focus_handle = self.query_editor.focus_handle(cx);
690+
691+
Some(
692+
IconButton::new("project-search-collapse-expand", icon)
693+
.shape(IconButtonShape::Square)
694+
.icon_size(IconSize::Small)
695+
.tooltip(move |_, cx| {
696+
Tooltip::for_action_in(
697+
tooltip_label,
698+
&ToggleAllSearchResults,
699+
&focus_handle,
700+
cx,
701+
)
702+
})
703+
.on_click(cx.listener(|this, _, window, cx| {
704+
this.toggle_all_search_results(&ToggleAllSearchResults, window, cx);
705+
}))
706+
.into_any_element(),
707+
)
708+
}
654709
}
655710

656711
impl ProjectSearchView {
@@ -753,6 +808,34 @@ impl ProjectSearchView {
753808
});
754809
}
755810

811+
fn toggle_all_search_results(
812+
&mut self,
813+
_: &ToggleAllSearchResults,
814+
_window: &mut Window,
815+
cx: &mut Context<Self>,
816+
) {
817+
self.results_collapsed = !self.results_collapsed;
818+
self.update_results_visibility(cx);
819+
}
820+
821+
fn update_results_visibility(&mut self, cx: &mut Context<Self>) {
822+
self.results_editor.update(cx, |editor, cx| {
823+
let multibuffer = editor.buffer().read(cx);
824+
let buffer_ids = multibuffer.excerpt_buffer_ids();
825+
826+
if self.results_collapsed {
827+
for buffer_id in buffer_ids {
828+
editor.fold_buffer(buffer_id, cx);
829+
}
830+
} else {
831+
for buffer_id in buffer_ids {
832+
editor.unfold_buffer(buffer_id, cx);
833+
}
834+
}
835+
});
836+
cx.notify();
837+
}
838+
756839
pub fn new(
757840
workspace: WeakEntity<Workspace>,
758841
entity: Entity<ProjectSearch>,
@@ -911,8 +994,10 @@ impl ProjectSearchView {
911994
replace_enabled: false,
912995
included_opened_only: false,
913996
regex_language: None,
997+
results_collapsed: false,
914998
_subscriptions: subscriptions,
915999
};
1000+
9161001
this.entity_changed(window, cx);
9171002
this
9181003
}
@@ -1411,6 +1496,7 @@ impl ProjectSearchView {
14111496

14121497
fn entity_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
14131498
let match_ranges = self.entity.read(cx).match_ranges.clone();
1499+
14141500
if match_ranges.is_empty() {
14151501
self.active_match_index = None;
14161502
self.results_editor.update(cx, |editor, cx| {
@@ -1968,6 +2054,8 @@ impl Render for ProjectSearchBar {
19682054
})
19692055
.unwrap_or_else(|| "0/0".to_string());
19702056

2057+
let query_focus = search.query_editor.focus_handle(cx);
2058+
19712059
let query_column = input_base_styles(InputPanel::Query)
19722060
.on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
19732061
.on_action(cx.listener(|this, action, window, cx| {
@@ -1997,11 +2085,9 @@ impl Render for ProjectSearchBar {
19972085
)),
19982086
);
19992087

2000-
let query_focus = search.query_editor.focus_handle(cx);
2001-
20022088
let matches_column = h_flex()
2003-
.pl_2()
2004-
.ml_2()
2089+
.ml_1()
2090+
.pl_1p5()
20052091
.border_l_1()
20062092
.border_color(theme_colors.border_variant)
20072093
.child(render_action_button(

crates/search/src/search_bar.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div
4646
.h_8()
4747
.pl_2()
4848
.pr_1()
49-
.py_1()
5049
.border_1()
5150
.border_color(border_color)
5251
.rounded_md()

crates/workspace/src/item.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,15 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
296296
None
297297
}
298298

299+
/// Returns optional elements to render to the left of the breadcrumb.
300+
fn breadcrumb_prefix(
301+
&self,
302+
_window: &mut Window,
303+
_cx: &mut Context<Self>,
304+
) -> Option<gpui::AnyElement> {
305+
None
306+
}
307+
299308
fn added_to_workspace(
300309
&mut self,
301310
_workspace: &mut Workspace,
@@ -479,6 +488,7 @@ pub trait ItemHandle: 'static + Send {
479488
fn to_searchable_item_handle(&self, cx: &App) -> Option<Box<dyn SearchableItemHandle>>;
480489
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation;
481490
fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>>;
491+
fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option<gpui::AnyElement>;
482492
fn show_toolbar(&self, cx: &App) -> bool;
483493
fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>>;
484494
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
@@ -979,6 +989,10 @@ impl<T: Item> ItemHandle for Entity<T> {
979989
self.read(cx).breadcrumbs(theme, cx)
980990
}
981991

992+
fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option<gpui::AnyElement> {
993+
self.update(cx, |item, cx| item.breadcrumb_prefix(window, cx))
994+
}
995+
982996
fn show_toolbar(&self, cx: &App) -> bool {
983997
self.read(cx).show_toolbar()
984998
}

0 commit comments

Comments
 (0)