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