From c01513efb6226271fe7898482d7061bb437d31d4 Mon Sep 17 00:00:00 2001 From: KyleBarton Date: Fri, 19 Dec 2025 16:22:40 -0800 Subject: [PATCH] Toggle collapse/expand button based on editor actions. Refactor folding logic. --- crates/breadcrumbs/src/breadcrumbs.rs | 1 + crates/editor/src/editor.rs | 40 +++-- crates/editor/src/element.rs | 4 +- crates/editor/src/items.rs | 4 +- crates/git_ui/src/project_diff.rs | 2 +- crates/search/src/buffer_search.rs | 210 ++++++++++++++++++-------- crates/search/src/project_search.rs | 34 +++-- crates/workspace/src/searchable.rs | 7 + crates/workspace/src/toolbar.rs | 4 +- 9 files changed, 196 insertions(+), 110 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 9590e3f01e30e044d9b46c1c6b65869d99a17293..9a16a7907e2679e7ae4b5e1750ee8d5a503c76c8 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -50,6 +50,7 @@ impl Render for Breadcrumbs { let element = h_flex() .id("breadcrumb-container") .flex_grow() + .h_8() .overflow_x_scroll() .text_ui(cx); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aac0d28ead1ab3a8b51574b307bac121910dcc11..1ec169acc03041b2a6d0401783384f37b1323ced 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -209,7 +209,7 @@ use workspace::{ ViewId, Workspace, WorkspaceId, WorkspaceSettings, item::{BreadcrumbText, ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions}, notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, - searchable::SearchEvent, + searchable::{CollapseDirection, SearchEvent}, }; use crate::{ @@ -1183,7 +1183,7 @@ pub struct Editor { hovered_diff_hunk_row: Option, pull_diagnostics_task: Task<()>, pull_diagnostics_background_task: Task<()>, - pub in_project_search: bool, + in_project_search: bool, previous_search_ranges: Option]>>, breadcrumb_header: Option, focused_block: Option, @@ -19179,37 +19179,25 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.buffer.read(cx).is_singleton() { + let has_folds = if self.buffer.read(cx).is_singleton() { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let has_folds = display_map .folds_in_range(MultiBufferOffset(0)..display_map.buffer_snapshot().len()) .next() .is_some(); - - if has_folds { - self.unfold_all(&actions::UnfoldAll, window, cx); - } else { - self.fold_all(&actions::FoldAll, window, cx); - } + has_folds } else { let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids(); - let should_unfold = buffer_ids + let has_folds = buffer_ids .iter() .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); + has_folds + }; - self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { - editor - .update_in(cx, |editor, _, cx| { - for buffer_id in buffer_ids { - if should_unfold { - editor.unfold_buffer(buffer_id, cx); - } else { - editor.fold_buffer(buffer_id, cx); - } - } - }) - .ok(); - }); + if has_folds { + self.unfold_all(&actions::UnfoldAll, window, cx); + } else { + self.fold_all(&actions::FoldAll, window, cx); } } @@ -19374,6 +19362,9 @@ impl Editor { .ok(); }); } + cx.emit(SearchEvent::ResultsCollapsedChanged( + CollapseDirection::Collapsed, + )); } pub fn fold_function_bodies( @@ -19562,6 +19553,9 @@ impl Editor { .ok(); }); } + cx.emit(SearchEvent::ResultsCollapsedChanged( + CollapseDirection::Expanded, + )); } pub fn fold_selected_ranges( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 11d9dcb0c173af0638ac73a92b14bc7a346b8841..ec41c07bd808383002bd7bd0d9adc75487e653d1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4142,6 +4142,7 @@ impl EditorElement { Label::new(filename) .single_line() .color(file_status_label_color(file_status)) + .buffer_font(cx) .when( file_status.is_some_and(|s| s.is_deleted()), |label| label.strikethrough(), @@ -4160,7 +4161,8 @@ impl EditorElement { })), ) .when_some(parent_path, |then, path| { - then.child(Label::new(path).truncate().color( + // TODO: Swap to use `truncate_start()` + then.child(Label::new(path).buffer_font(cx).truncate().color( if file_status.is_some_and(FileStatus::is_deleted) { Color::Custom(colors.text_disabled) } else { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index be426ccc0982d07fd9433244256132a0a203f0c4..f872db68fafbedb45a79a9b609effb3d50aa7593 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -983,8 +983,8 @@ impl Item for Editor { ) } - fn breadcrumb_location(&self, _cx: &App) -> ToolbarItemLocation { - if self.show_breadcrumbs { + fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { + if self.show_breadcrumbs && self.buffer().read(cx).is_singleton() { ToolbarItemLocation::PrimaryLeft } else { ToolbarItemLocation::Hidden diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index ceb57bc6d6212c4bf08109cb1a80d71cad0542cd..5392e27a1ca920dab4f337318ce2fa7c872a40cd 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -956,7 +956,7 @@ impl Item for ProjectDiff { } fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft + ToolbarItemLocation::Hidden } fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 713ae54706447612f3e74a8d3bf7d94b5efdf351..a264be0e3843088f044c902594f5e9504a03f398 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,10 +1,9 @@ mod registrar; use crate::{ - FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ProjectSearchView, ReplaceAll, - ReplaceNext, SearchOption, SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, - SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, - ToggleWholeWord, + FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, + SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, + ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; @@ -12,7 +11,7 @@ use anyhow::Context as _; use collections::HashMap; use editor::{ DisplayPoint, Editor, EditorSettings, MultiBufferOffset, - actions::{Backtab, Tab}, + actions::{Backtab, FoldAll, Tab, ToggleFoldAll, UnfoldAll}, }; use futures::channel::oneshot; use gpui::{ @@ -37,10 +36,11 @@ use ui::{ }; use util::{ResultExt, paths::PathMatcher}; use workspace::{ - DeploySearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{ItemBufferKind, ItemHandle}, searchable::{ - Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle, + CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, + WeakSearchableItemHandle, }, }; @@ -125,18 +125,78 @@ pub struct BufferSearchBar { editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels, regex_language: Option>, + is_collapsed: bool, } impl EventEmitter for BufferSearchBar {} impl EventEmitter for BufferSearchBar {} impl Render for BufferSearchBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.dismissed { - return div().id("search_bar").into_any_element(); - } - let focus_handle = self.focus_handle(cx); + let collapse_expand_button = if self.needs_expand_collapse_option(cx) { + let (icon, label, tooltip_label) = if self.is_collapsed { + ( + IconName::ChevronUpDown, + "Expand All", + "Expand All Search Results", + ) + } else { + ( + IconName::ChevronDownUp, + "Collapse All", + "Collapse All Search Results", + ) + }; + + let tooltip_focus_handle = focus_handle.clone(); + + if self.dismissed { + let button = Button::new("multibuffer-collapse-expand-empty", label) + .icon_position(IconPosition::Start) + .icon(icon) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + tooltip_label, + &ToggleFoldAll, + &tooltip_focus_handle, + cx, + ) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_fold_all_in_item(window, cx); + })) + .into_any_element(); + + return h_flex() + .id("search_bar_button_only") + .py_px() + .justify_start() + .child(button) + .into_any_element(); + } + + Some( + IconButton::new("multibuffer-collapse-expand", icon) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + tooltip_label, + &ToggleFoldAll, + &tooltip_focus_handle, + cx, + ) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_fold_all_in_item(window, cx); + })) + .into_any_element(), + ) + } else { + None + }; + let narrow_mode = self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX; let hide_inline_icons = self.editor_needed_width @@ -334,10 +394,13 @@ impl Render for BufferSearchBar { let search_line = h_flex() .w_full() - .gap_2() + .gap_1() .when(find_in_results, |el| { el.child(Label::new("Find in results").color(Color::Hint)) }) + .when(!find_in_results && collapse_expand_button.is_some(), |el| { + el.child(collapse_expand_button.expect("button")) + }) .child(query_column) .child(mode_column); @@ -403,41 +466,10 @@ impl Render for BufferSearchBar { .w_full() }); - // let is_collapsed = self.is_multibuffer_collapsed; - // This shouldn't show on a singleton or on a project search - let button = if self.active_item_is_multibuffer(cx) { - let is_collapsed = false; - - let (icon, tooltip_label) = if is_collapsed { - (IconName::ChevronUpDown, "Expand All Search Results") - } else { - (IconName::ChevronDownUp, "Collapse All Search Results") - }; - - let tooltip_focus_handle = focus_handle.clone(); - - // emit_action!(ToggleFoldAll, focus_handle.clone()); - - Some( - IconButton::new("multibuffer-collapse-expand", icon) - .icon_size(IconSize::Small) - // .tooltip(move |_, cx| { - // Tooltip::for_action_in(tooltip_label, &ToggleFoldAll, &tooltip_focus_handle, cx) - // }) - // .on_click(cx.listener(|this, _, window, cx| { - // this.is_multibuffer_collapsed = !this.is_multibuffer_collapsed; - // this.toggle_fold_all(&ToggleFoldAll, window, cx); - // })) - .into_any_element(), - ) - } else { - None - }; - v_flex() .id("buffer_search") - .gap_2() - .py(px(1.0)) + .gap_0() + .py(px(0.0)) .w_full() .track_scroll(&self.scroll_handle) .key_context(key_context) @@ -480,12 +512,7 @@ impl Render for BufferSearchBar { .when(selection, |this| { this.on_action(cx.listener(Self::toggle_selection)) }) - .child( - h_flex() - .gap_1() - .when_some(button, |then, button| then.child(button)) - .child(search_line), - ) + .child(search_line) .children(query_error_line) .children(replace_line) .into_any_element() @@ -538,17 +565,27 @@ impl ToolbarItemView for BufferSearchBar { let is_project_search = searchable_item_handle.supported_options(cx).find_in_results; self.active_searchable_item = Some(searchable_item_handle); drop(self.update_matches(true, false, window, cx)); - if !self.dismissed { + // Need to think through this a bit + // Copy this over to dismiss + if self.needs_expand_collapse_option(cx) { + return ToolbarItemLocation::PrimaryLeft; + } else if !self.is_dismissed() { if is_project_search { self.dismiss(&Default::default(), window, cx); - } else { - if self.active_item_is_multibuffer(cx) { - return ToolbarItemLocation::PrimaryLeft; - } else { - return ToolbarItemLocation::Secondary; - } } + return ToolbarItemLocation::Secondary; } + // if !self.dismissed { + // if is_project_search { + // self.dismiss(&Default::default(), window, cx); + // } else { + // if self.needs_expand_collapse_option(cx) { + // return ToolbarItemLocation::PrimaryLeft; + // } else { + // return ToolbarItemLocation::Secondary; + // } + // } + // } } ToolbarItemLocation::Hidden } @@ -724,6 +761,7 @@ impl BufferSearchBar { editor_scroll_handle: ScrollHandle::new(), editor_needed_width: px(0.), regex_language: None, + is_collapsed: false, } } @@ -743,15 +781,28 @@ impl BufferSearchBar { searchable_item.clear_matches(window, cx); } } + + let needs_collapse_expand = self.needs_expand_collapse_option(cx); + let mut is_in_project_search = false; + if let Some(active_editor) = self.active_searchable_item.as_mut() { self.selection_search_enabled = None; self.replace_enabled = false; active_editor.search_bar_visibility_changed(false, window, cx); active_editor.toggle_filtered_search_ranges(None, window, cx); + is_in_project_search = active_editor.supported_options(cx).find_in_results; let handle = active_editor.item_focus_handle(cx); self.focus(&handle, window); } + if needs_collapse_expand && !is_in_project_search { + cx.emit(Event::UpdateLocation); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::PrimaryLeft, + )); + cx.notify(); + return; + } cx.emit(Event::UpdateLocation); cx.emit(ToolbarItemEvent::ChangeLocation( ToolbarItemLocation::Hidden, @@ -832,7 +883,7 @@ impl BufferSearchBar { cx.notify(); cx.emit(Event::UpdateLocation); cx.emit(ToolbarItemEvent::ChangeLocation( - if self.active_item_is_multibuffer(cx) { + if self.needs_expand_collapse_option(cx) { ToolbarItemLocation::PrimaryLeft } else { ToolbarItemLocation::Secondary @@ -848,17 +899,19 @@ impl BufferSearchBar { .unwrap_or_default() } - fn active_item_is_multibuffer(&self, cx: &App) -> bool { + // TODO we should clean this up + // We only provide an expand/collapse button if we are in a multibuffer and + // not doing a project search. In a project search, the button is already rendered. + // In a singleton buffer, this option doesn't make sense. + fn needs_expand_collapse_option(&self, cx: &App) -> bool { if let Some(item) = &self.active_searchable_item { let buffer_kind = item.buffer_kind(cx); if buffer_kind == ItemBufferKind::Multibuffer { - if let Some(editor) = item.act_as_type(TypeId::of::(), cx) { - let editor = editor.downcast::().expect("is an editor"); - return !editor.read(cx).in_project_search; - } else { - return false; - } + let workspace::searchable::SearchOptions { + find_in_results, .. + } = item.supported_options(cx); + !find_in_results } else { false } @@ -867,6 +920,22 @@ impl BufferSearchBar { } } + fn toggle_fold_all_in_item(&self, window: &mut Window, cx: &mut Context) { + let is_collapsed = self.is_collapsed; + if let Some(item) = &self.active_searchable_item { + if let Some(item) = item.act_as_type(TypeId::of::(), cx) { + let editor = item.downcast::().expect("Is an editor"); + editor.update(cx, |editor, cx| { + if is_collapsed { + editor.unfold_all(&UnfoldAll, window, cx); + } else { + editor.fold_all(&FoldAll, window, cx); + } + }) + } + } + } + pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context) { let search = self.query_suggestion(window, cx).map(|suggestion| { self.search(&suggestion, Some(self.default_options), true, window, cx) @@ -1197,6 +1266,15 @@ impl BufferSearchBar { drop(self.update_matches(false, false, window, cx)); } SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx), + SearchEvent::ResultsCollapsedChanged(collapse_direction) => { + if self.needs_expand_collapse_option(cx) { + match collapse_direction { + CollapseDirection::Collapsed => self.is_collapsed = true, + CollapseDirection::Expanded => self.is_collapsed = false, + } + } + cx.notify(); + } } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index deab3daec7812eb134c9741210aa3b1ad1d74a2e..5a0c77b5ebad13aa7d7ef2a363238abc068fc9cc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -10,7 +10,7 @@ use collections::HashMap; use editor::{ Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey, SelectionEffects, - actions::{Backtab, SelectAll, Tab}, + actions::{Backtab, FoldAll, SelectAll, Tab, ToggleFoldAll, UnfoldAll}, items::active_match_index, multibuffer_context_lines, scroll::Autoscroll, @@ -43,7 +43,7 @@ use workspace::{ DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions}, - searchable::{Direction, SearchableItem, SearchableItemHandle}, + searchable::{CollapseDirection, Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; actions!( @@ -763,26 +763,18 @@ impl ProjectSearchView { fn toggle_all_search_results( &mut self, _: &ToggleAllSearchResults, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { - self.results_collapsed = !self.results_collapsed; - self.update_results_visibility(cx); + self.update_results_visibility(window, cx); } - fn update_results_visibility(&mut self, cx: &mut Context) { + fn update_results_visibility(&mut self, window: &mut Window, 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); - } + editor.unfold_all(&UnfoldAll, window, cx); } else { - for buffer_id in buffer_ids { - editor.unfold_buffer(buffer_id, cx); - } + editor.fold_all(&FoldAll, window, cx); } }); cx.notify(); @@ -872,6 +864,18 @@ impl ProjectSearchView { cx.emit(ViewEvent::EditorEvent(event.clone())); }), ); + subscriptions.push(cx.subscribe( + &results_editor, + |this, _editor, event: &SearchEvent, _cx| match event { + SearchEvent::ResultsCollapsedChanged(collapsed_direction) => { + match collapsed_direction { + CollapseDirection::Collapsed => this.results_collapsed = true, + CollapseDirection::Expanded => this.results_collapsed = false, + } + } + _ => (), + }, + )); let included_files_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index badfe7d2437424c1ce18a1afde19507e7d6e1d3b..097bd7abe16978bf1b7b5d99b146f600ff602546 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -12,10 +12,17 @@ use crate::{ item::{Item, WeakItemHandle}, }; +#[derive(Clone, Debug)] +pub enum CollapseDirection { + Collapsed, + Expanded, +} + #[derive(Clone, Debug)] pub enum SearchEvent { MatchesInvalidated, ActiveMatchChanged, + ResultsCollapsedChanged(CollapseDirection), } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index e74c0a98268a9957f59e052744d600e5cd115d4d..20fb6582de9e1e9707e8d43d1b1fe4d01acaecfd 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -125,7 +125,7 @@ impl Render for Toolbar { .when(has_left_items, |this| { this.child( h_flex() - .min_h_6() + .min_h_8() .flex_auto() .justify_start() .overflow_x_hidden() @@ -135,7 +135,7 @@ impl Render for Toolbar { .when(has_right_items, |this| { this.child( h_flex() - .h_6() + .h_8() .flex_row_reverse() .map(|el| { if has_left_items {