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 <daniloleal09@gmail.com>
This commit is contained in:
Dylan 2025-11-04 01:50:34 +08:00 committed by GitHub
parent 45b78482f5
commit 7cfce60570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 125 additions and 8 deletions

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3335 13.3333L8.00017 10L4.66685 13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3335 2.66669L8.00017 6.00002L4.66685 2.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View file

@ -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",

View file

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

View file

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

View file

@ -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::<Editor>()
.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),
}
}
}

View file

@ -53,6 +53,7 @@ pub enum IconName {
Check,
CheckDouble,
ChevronDown,
ChevronDownUp,
ChevronLeft,
ChevronRight,
ChevronUp,

View file

@ -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::<ProjectSearchView>())
{
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<Arc<Language>>,
results_collapsed: bool,
_subscriptions: Vec<Subscription>,
}
@ -651,6 +668,44 @@ impl Item for ProjectSearchView {
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.results_editor.breadcrumbs(theme, cx)
}
fn breadcrumb_prefix(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<gpui::AnyElement> {
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>,
) {
self.results_collapsed = !self.results_collapsed;
self.update_results_visibility(cx);
}
fn update_results_visibility(&mut self, cx: &mut Context<Self>) {
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<Workspace>,
entity: Entity<ProjectSearch>,
@ -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<Self>) {
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(

View file

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

View file

@ -296,6 +296,15 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
None
}
/// Returns optional elements to render to the left of the breadcrumb.
fn breadcrumb_prefix(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<gpui::AnyElement> {
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<Box<dyn SearchableItemHandle>>;
fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation;
fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>>;
fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option<gpui::AnyElement>;
fn show_toolbar(&self, cx: &App) -> bool;
fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>>;
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
@ -979,6 +989,10 @@ impl<T: Item> ItemHandle for Entity<T> {
self.read(cx).breadcrumbs(theme, cx)
}
fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option<gpui::AnyElement> {
self.update(cx, |item, cx| item.breadcrumb_prefix(window, cx))
}
fn show_toolbar(&self, cx: &App) -> bool {
self.read(cx).show_toolbar()
}