From 7cfce605704d41ca247e3f84804bf323f6c6caaf Mon Sep 17 00:00:00 2001
From: Dylan <74223747+dylankyc@users.noreply.github.com>
Date: Tue, 4 Nov 2025 01:50:34 +0800
Subject: [PATCH] project_search: Add button to collapse/expand all excerpts
(#41654)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Releases Note:
- Added a button that allows to expand/collapse all project search
excerpts at once.
---------
Co-authored-by: Danilo Leal
---
assets/icons/chevron_down_up.svg | 4 ++
assets/keymaps/default-linux.json | 2 +
assets/keymaps/default-macos.json | 2 +
assets/keymaps/default-windows.json | 1 +
crates/breadcrumbs/src/breadcrumbs.rs | 12 +++-
crates/icons/src/icons.rs | 1 +
crates/search/src/project_search.rs | 96 +++++++++++++++++++++++++--
crates/search/src/search_bar.rs | 1 -
crates/workspace/src/item.rs | 14 ++++
9 files changed, 125 insertions(+), 8 deletions(-)
create mode 100644 assets/icons/chevron_down_up.svg
diff --git a/assets/icons/chevron_down_up.svg b/assets/icons/chevron_down_up.svg
new file mode 100644
index 0000000000000000000000000000000000000000..340b8d1ad93113a1affe5c723c9b5f5e12a228a8
--- /dev/null
+++ b/assets/icons/chevron_down_up.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 979e5a6ccc1d4520db65981fb3b8a01094f9c625..6f57e6f689a30543ee0b7d6b95d451af885d502a 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -407,6 +407,7 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
+ "shift-enter": "project_search::ToggleAllSearchResults",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ToggleRegex",
@@ -479,6 +480,7 @@
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-ctrl-f": "project_search::ToggleFilters",
+ "shift-enter": "project_search::ToggleAllSearchResults",
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex",
"alt-r": "search::ToggleRegex",
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 4f9b85ff03790a8c9a59a657a3e0ca0710d41e25..a1d38b6028f8ec7690f7133b765ffdbb8d261f17 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -468,6 +468,7 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
+ "shift-enter": "project_search::ToggleAllSearchResults",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
@@ -496,6 +497,7 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
+ "shift-enter": "project_search::ToggleAllSearchResults",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
index 29146f3080d6ecad75bb9754503bb93c6710ff30..2dd72845b196c029bb2c575bcdd07b5ef07ae970 100644
--- a/assets/keymaps/default-windows.json
+++ b/assets/keymaps/default-windows.json
@@ -488,6 +488,7 @@
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-f": "project_search::ToggleFilters",
+ "shift-enter": "project_search::ToggleAllSearchResults",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
"ctrl-k shift-enter": "pane::TogglePinTab"
diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs
index 08c0915c58ae50741238574cec5b6f2474d06eb8..7664de3c87673a405118911526cb6606a2fecacf 100644
--- a/crates/breadcrumbs/src/breadcrumbs.rs
+++ b/crates/breadcrumbs/src/breadcrumbs.rs
@@ -100,13 +100,21 @@ impl Render for Breadcrumbs {
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
+ let prefix_element = active_item.breadcrumb_prefix(window, cx);
+
+ let breadcrumbs = if let Some(prefix) = prefix_element {
+ h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
+ } else {
+ breadcrumbs_stack
+ };
+
match active_item
.downcast::()
.map(|editor| editor.downgrade())
{
Some(editor) => element.child(
ButtonLike::new("toggle outline view")
- .child(breadcrumbs_stack)
+ .child(breadcrumbs)
.style(ButtonStyle::Transparent)
.on_click({
let editor = editor.clone();
@@ -141,7 +149,7 @@ impl Render for Breadcrumbs {
// Match the height and padding of the `ButtonLike` in the other arm.
.h(rems_from_px(22.))
.pl_1()
- .child(breadcrumbs_stack),
+ .child(breadcrumbs),
}
}
}
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 1442c482d89f0c46e45ccd280e678021e6ba63c7..a4da8c6ccdf04f453a368b902af8543625100436 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -53,6 +53,7 @@ pub enum IconName {
Check,
CheckDouble,
ChevronDown,
+ ChevronDownUp,
ChevronLeft,
ChevronRight,
ChevronUp,
diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs
index 9ee92c73008338e0ee655f3d02cb1dcbf1a3326b..f5a9c272d4846a94230286cc3ae2f7903608dd7d 100644
--- a/crates/search/src/project_search.rs
+++ b/crates/search/src/project_search.rs
@@ -57,7 +57,9 @@ actions!(
/// Moves to the next input field.
NextField,
/// Toggles the search filters panel.
- ToggleFilters
+ ToggleFilters,
+ /// Toggles collapse/expand state of all search result excerpts.
+ ToggleAllSearchResults
]
);
@@ -120,6 +122,20 @@ pub fn init(cx: &mut App) {
ProjectSearchView::search_in_new(workspace, action, window, cx)
});
+ register_workspace_action_for_present_search(
+ workspace,
+ |workspace, action: &ToggleAllSearchResults, window, cx| {
+ if let Some(search_view) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::())
+ {
+ search_view.update(cx, |search_view, cx| {
+ search_view.toggle_all_search_results(action, window, cx);
+ });
+ }
+ },
+ );
+
register_workspace_action_for_present_search(
workspace,
|workspace, _: &menu::Cancel, window, cx| {
@@ -219,6 +235,7 @@ pub struct ProjectSearchView {
replace_enabled: bool,
included_opened_only: bool,
regex_language: Option>,
+ results_collapsed: bool,
_subscriptions: Vec,
}
@@ -651,6 +668,44 @@ impl Item for ProjectSearchView {
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> {
self.results_editor.breadcrumbs(theme, cx)
}
+
+ fn breadcrumb_prefix(
+ &self,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) -> Option {
+ if !self.has_matches() {
+ return None;
+ }
+
+ let is_collapsed = self.results_collapsed;
+
+ let (icon, tooltip_label) = if is_collapsed {
+ (IconName::ChevronUpDown, "Expand All Search Results")
+ } else {
+ (IconName::ChevronDownUp, "Collapse All Search Results")
+ };
+
+ let focus_handle = self.query_editor.focus_handle(cx);
+
+ Some(
+ IconButton::new("project-search-collapse-expand", icon)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .tooltip(move |_, cx| {
+ Tooltip::for_action_in(
+ tooltip_label,
+ &ToggleAllSearchResults,
+ &focus_handle,
+ cx,
+ )
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.toggle_all_search_results(&ToggleAllSearchResults, window, cx);
+ }))
+ .into_any_element(),
+ )
+ }
}
impl ProjectSearchView {
@@ -753,6 +808,34 @@ impl ProjectSearchView {
});
}
+ fn toggle_all_search_results(
+ &mut self,
+ _: &ToggleAllSearchResults,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.results_collapsed = !self.results_collapsed;
+ self.update_results_visibility(cx);
+ }
+
+ fn update_results_visibility(&mut self, cx: &mut Context) {
+ self.results_editor.update(cx, |editor, cx| {
+ let multibuffer = editor.buffer().read(cx);
+ let buffer_ids = multibuffer.excerpt_buffer_ids();
+
+ if self.results_collapsed {
+ for buffer_id in buffer_ids {
+ editor.fold_buffer(buffer_id, cx);
+ }
+ } else {
+ for buffer_id in buffer_ids {
+ editor.unfold_buffer(buffer_id, cx);
+ }
+ }
+ });
+ cx.notify();
+ }
+
pub fn new(
workspace: WeakEntity,
entity: Entity,
@@ -911,8 +994,10 @@ impl ProjectSearchView {
replace_enabled: false,
included_opened_only: false,
regex_language: None,
+ results_collapsed: false,
_subscriptions: subscriptions,
};
+
this.entity_changed(window, cx);
this
}
@@ -1411,6 +1496,7 @@ impl ProjectSearchView {
fn entity_changed(&mut self, window: &mut Window, cx: &mut Context) {
let match_ranges = self.entity.read(cx).match_ranges.clone();
+
if match_ranges.is_empty() {
self.active_match_index = None;
self.results_editor.update(cx, |editor, cx| {
@@ -1968,6 +2054,8 @@ impl Render for ProjectSearchBar {
})
.unwrap_or_else(|| "0/0".to_string());
+ let query_focus = search.query_editor.focus_handle(cx);
+
let query_column = input_base_styles(InputPanel::Query)
.on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
.on_action(cx.listener(|this, action, window, cx| {
@@ -1997,11 +2085,9 @@ impl Render for ProjectSearchBar {
)),
);
- let query_focus = search.query_editor.focus_handle(cx);
-
let matches_column = h_flex()
- .pl_2()
- .ml_2()
+ .ml_1()
+ .pl_1p5()
.border_l_1()
.border_color(theme_colors.border_variant)
.child(render_action_button(
diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs
index 14a5fefcf7341694260da96a8f2c43d149356074..61fa46ed9770fbaf49b43979d366655c1b658fc3 100644
--- a/crates/search/src/search_bar.rs
+++ b/crates/search/src/search_bar.rs
@@ -46,7 +46,6 @@ pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div
.h_8()
.pl_2()
.pr_1()
- .py_1()
.border_1()
.border_color(border_color)
.rounded_md()
diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs
index b77075f92bf69dc292cf69ac7eac147043d7d8b7..ee9a10d9c5344cfa372bf88a95e46de1705ee093 100644
--- a/crates/workspace/src/item.rs
+++ b/crates/workspace/src/item.rs
@@ -296,6 +296,15 @@ pub trait Item: Focusable + EventEmitter + Render + Sized {
None
}
+ /// Returns optional elements to render to the left of the breadcrumb.
+ fn breadcrumb_prefix(
+ &self,
+ _window: &mut Window,
+ _cx: &mut Context,
+ ) -> Option {
+ None
+ }
+
fn added_to_workspace(
&mut self,
_workspace: &mut Workspace,
@@ -479,6 +488,7 @@ pub trait ItemHandle: 'static + Send {
fn to_searchable_item_handle(&self, cx: &App) -> Option>;
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation;
fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option>;
+ fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option;
fn show_toolbar(&self, cx: &App) -> bool;
fn pixel_position_of_cursor(&self, cx: &App) -> Option>;
fn downgrade_item(&self) -> Box;
@@ -979,6 +989,10 @@ impl ItemHandle for Entity {
self.read(cx).breadcrumbs(theme, cx)
}
+ fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option {
+ self.update(cx, |item, cx| item.breadcrumb_prefix(window, cx))
+ }
+
fn show_toolbar(&self, cx: &App) -> bool {
self.read(cx).show_toolbar()
}