Toggle collapse/expand button based on editor actions. Refactor folding logic.

KyleBarton created

Change summary

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

Detailed changes

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<DisplayRow>,
     pull_diagnostics_task: Task<()>,
     pull_diagnostics_background_task: Task<()>,
-    pub in_project_search: bool,
+    in_project_search: bool,
     previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
     breadcrumb_header: Option<String>,
     focused_block: Option<FocusedBlock>,
@@ -19179,37 +19179,25 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        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(

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 {

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

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<Vec<BreadcrumbText>> {

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<Arc<Language>>,
+    is_collapsed: bool,
 }
 
 impl EventEmitter<Event> for BufferSearchBar {}
 impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
 impl Render for BufferSearchBar {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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::<Editor>(), cx) {
-                    let editor = editor.downcast::<Editor>().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<Self>) {
+        let is_collapsed = self.is_collapsed;
+        if let Some(item) = &self.active_searchable_item {
+            if let Some(item) = item.act_as_type(TypeId::of::<Editor>(), cx) {
+                let editor = item.downcast::<Editor>().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<Self>) {
         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();
+            }
         }
     }
 

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>,
     ) {
-        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<Self>) {
+    fn update_results_visibility(&mut self, window: &mut Window, 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);
-                }
+                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);

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)]

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 {