Detailed changes
@@ -217,7 +217,7 @@ use workspace::{
TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings,
item::{BreadcrumbText, ItemBufferKind, ItemHandle, PreviewTabsSettings, SaveOptions},
notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
- searchable::{CollapseDirection, SearchEvent},
+ searchable::SearchEvent,
};
use zed_actions::editor::{MoveDown, MoveUp};
@@ -20046,9 +20046,6 @@ impl Editor {
.ok();
});
}
- cx.emit(SearchEvent::ResultsCollapsedChanged(
- CollapseDirection::Collapsed,
- ));
}
pub fn fold_function_bodies(
@@ -20237,9 +20234,6 @@ impl Editor {
.ok();
});
}
- cx.emit(SearchEvent::ResultsCollapsedChanged(
- CollapseDirection::Expanded,
- ));
}
pub fn fold_selected_ranges(
@@ -20350,6 +20344,13 @@ impl Editor {
self.display_map.read(cx).is_buffer_folded(buffer)
}
+ pub fn has_any_buffer_folded(&self, cx: &App) -> bool {
+ if self.buffer().read(cx).is_singleton() {
+ return false;
+ }
+ !self.folded_buffers(cx).is_empty()
+ }
+
pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet<BufferId> {
self.display_map.read(cx).folded_buffers()
}
@@ -42,8 +42,8 @@ use workspace::{
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
item::{ItemBufferKind, ItemHandle},
searchable::{
- CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchToken,
- SearchableItemHandle, WeakSearchableItemHandle,
+ Direction, FilteredSearchRange, SearchEvent, SearchToken, SearchableItemHandle,
+ WeakSearchableItemHandle,
},
};
@@ -94,7 +94,6 @@ pub struct BufferSearchBar {
editor_scroll_handle: ScrollHandle,
editor_needed_width: Pixels,
regex_language: Option<Arc<Language>>,
- is_collapsed: bool,
splittable_editor: Option<WeakEntity<SplittableEditor>>,
_splittable_editor_subscription: Option<Subscription>,
}
@@ -212,7 +211,14 @@ impl Render for BufferSearchBar {
let collapse_expand_button = if self.needs_expand_collapse_option(cx) {
let query_editor_focus = self.query_editor.focus_handle(cx);
- let (icon, tooltip_label) = if self.is_collapsed {
+ let is_collapsed = self
+ .active_searchable_item
+ .as_ref()
+ .and_then(|item| item.act_as_type(TypeId::of::<Editor>(), cx))
+ .and_then(|item| item.downcast::<Editor>().ok())
+ .map(|editor: Entity<Editor>| editor.read(cx).has_any_buffer_folded(cx))
+ .unwrap_or_default();
+ let (icon, tooltip_label) = if is_collapsed {
(IconName::ChevronUpDown, "Expand All Files")
} else {
(IconName::ChevronDownUp, "Collapse All Files")
@@ -887,7 +893,6 @@ impl BufferSearchBar {
editor_scroll_handle: ScrollHandle::new(),
editor_needed_width: px(0.),
regex_language: None,
- is_collapsed: false,
splittable_editor: None,
_splittable_editor_subscription: None,
}
@@ -1053,11 +1058,11 @@ 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| {
+ let is_collapsed = editor.has_any_buffer_folded(cx);
if is_collapsed {
editor.unfold_all(&UnfoldAll, window, cx);
} else {
@@ -1434,15 +1439,6 @@ 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();
- }
}
}
@@ -3395,20 +3391,81 @@ mod tests {
editor.update_in(cx, |editor, window, cx| {
editor.fold_all(&FoldAll, window, cx);
});
+ cx.run_until_parked();
- let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
-
+ let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
assert!(is_collapsed);
editor.update_in(cx, |editor, window, cx| {
editor.unfold_all(&UnfoldAll, window, cx);
});
+ cx.run_until_parked();
- let is_collapsed = search_bar.read_with(cx, |search_bar, _| search_bar.is_collapsed);
-
+ let is_collapsed = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
assert!(!is_collapsed);
}
+ #[perf]
+ #[gpui::test]
+ async fn test_collapse_state_syncs_after_manual_buffer_fold(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_multibuffer_test(cx);
+
+ search_bar.update_in(cx, |search_bar, window, cx| {
+ search_bar.set_active_pane_item(Some(&editor), window, cx);
+ });
+
+ // Fold all buffers via fold_all
+ editor.update_in(cx, |editor, window, cx| {
+ editor.fold_all(&FoldAll, window, cx);
+ });
+ cx.run_until_parked();
+
+ let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
+ assert!(
+ has_any_folded,
+ "All buffers should be folded after fold_all"
+ );
+
+ // Manually unfold one buffer (simulating a chevron click)
+ let first_buffer_id = editor.read_with(cx, |editor, cx| {
+ editor.buffer().read(cx).excerpt_buffer_ids()[0]
+ });
+ editor.update_in(cx, |editor, _window, cx| {
+ editor.unfold_buffer(first_buffer_id, cx);
+ });
+
+ let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
+ assert!(
+ has_any_folded,
+ "Should still report folds when only one buffer is unfolded"
+ );
+
+ // Manually unfold the second buffer too
+ let second_buffer_id = editor.read_with(cx, |editor, cx| {
+ editor.buffer().read(cx).excerpt_buffer_ids()[1]
+ });
+ editor.update_in(cx, |editor, _window, cx| {
+ editor.unfold_buffer(second_buffer_id, cx);
+ });
+
+ let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
+ assert!(
+ !has_any_folded,
+ "No folds should remain after unfolding all buffers individually"
+ );
+
+ // Manually fold one buffer back
+ editor.update_in(cx, |editor, _window, cx| {
+ editor.fold_buffer(first_buffer_id, cx);
+ });
+
+ let has_any_folded = editor.read_with(cx, |editor, cx| editor.has_any_buffer_folded(cx));
+ assert!(
+ has_any_folded,
+ "Should report folds after manually folding one buffer"
+ );
+ }
+
#[perf]
#[gpui::test]
async fn test_search_options_changes(cx: &mut TestAppContext) {
@@ -49,10 +49,7 @@ use workspace::{
DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace, WorkspaceId,
item::{Item, ItemEvent, ItemHandle, SaveOptions},
- searchable::{
- CollapseDirection, Direction, SearchEvent, SearchToken, SearchableItem,
- SearchableItemHandle,
- },
+ searchable::{Direction, SearchEvent, SearchToken, SearchableItem, SearchableItemHandle},
};
actions!(
@@ -269,7 +266,6 @@ pub struct ProjectSearchView {
replace_enabled: bool,
included_opened_only: bool,
regex_language: Option<Arc<Language>>,
- results_collapsed: bool,
_subscriptions: Vec<Subscription>,
}
@@ -814,8 +810,9 @@ impl ProjectSearchView {
}
fn update_results_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let has_any_folded = self.results_editor.read(cx).has_any_buffer_folded(cx);
self.results_editor.update(cx, |editor, cx| {
- if self.results_collapsed {
+ if has_any_folded {
editor.unfold_all(&UnfoldAll, window, cx);
} else {
editor.fold_all(&FoldAll, window, cx);
@@ -910,18 +907,7 @@ impl ProjectSearchView {
);
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,
- }
- }
- _ => (),
- };
- cx.notify();
- },
+ |_this, _editor, _event: &SearchEvent, cx| cx.notify(),
));
let included_files_editor = cx.new(|cx| {
@@ -997,7 +983,6 @@ impl ProjectSearchView {
replace_enabled: false,
included_opened_only: false,
regex_language: None,
- results_collapsed: false,
_subscriptions: subscriptions,
};
@@ -2224,7 +2209,7 @@ impl Render for ProjectSearchBar {
))
.child(matches_column);
- let is_collapsed = search.results_collapsed;
+ let is_collapsed = search.results_editor.read(cx).has_any_buffer_folded(cx);
let (icon, tooltip_label) = if is_collapsed {
(IconName::ChevronUpDown, "Expand All Search Results")
@@ -2804,9 +2789,15 @@ pub mod tests {
})
})
.expect("Should fold fine");
+ cx.run_until_parked();
let results_collapsed = search_view
- .read_with(cx, |search_view, _| search_view.results_collapsed)
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
.expect("got results_collapsed");
assert!(results_collapsed);
@@ -2817,14 +2808,159 @@ pub mod tests {
})
})
.expect("Should unfold fine");
+ cx.run_until_parked();
let results_collapsed = search_view
- .read_with(cx, |search_view, _| search_view.results_collapsed)
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
.expect("got results_collapsed");
assert!(!results_collapsed);
}
+ #[perf]
+ #[gpui::test]
+ async fn test_collapse_state_syncs_after_manual_buffer_fold(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ path!("/dir"),
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let window =
+ cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ let workspace = window
+ .read_with(cx, |mw, _| mw.workspace().clone())
+ .unwrap();
+ let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
+ let search_view = cx.add_window(|window, cx| {
+ ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
+ });
+
+ // Search for "ONE" which appears in all 3 files
+ perform_search(search_view, "ONE", cx);
+
+ // Verify initial state: no folds
+ let has_any_folded = search_view
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
+ .expect("should read state");
+ assert!(!has_any_folded, "No buffers should be folded initially");
+
+ // Fold all via fold_all
+ search_view
+ .update(cx, |search_view, window, cx| {
+ search_view.results_editor.update(cx, |editor, cx| {
+ editor.fold_all(&FoldAll, window, cx);
+ })
+ })
+ .expect("Should fold fine");
+ cx.run_until_parked();
+
+ let has_any_folded = search_view
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
+ .expect("should read state");
+ assert!(
+ has_any_folded,
+ "All buffers should be folded after fold_all"
+ );
+
+ // Manually unfold one buffer (simulating a chevron click)
+ let first_buffer_id = search_view
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .excerpt_buffer_ids()[0]
+ })
+ .expect("should read buffer ids");
+
+ search_view
+ .update(cx, |search_view, _window, cx| {
+ search_view.results_editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(first_buffer_id, cx);
+ })
+ })
+ .expect("Should unfold one buffer");
+
+ let has_any_folded = search_view
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
+ .expect("should read state");
+ assert!(
+ has_any_folded,
+ "Should still report folds when only one buffer is unfolded"
+ );
+
+ // Unfold all via unfold_all
+ search_view
+ .update(cx, |search_view, window, cx| {
+ search_view.results_editor.update(cx, |editor, cx| {
+ editor.unfold_all(&UnfoldAll, window, cx);
+ })
+ })
+ .expect("Should unfold fine");
+ cx.run_until_parked();
+
+ let has_any_folded = search_view
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
+ .expect("should read state");
+ assert!(!has_any_folded, "No folds should remain after unfold_all");
+
+ // Manually fold one buffer back (simulating a chevron click)
+ search_view
+ .update(cx, |search_view, _window, cx| {
+ search_view.results_editor.update(cx, |editor, cx| {
+ editor.fold_buffer(first_buffer_id, cx);
+ })
+ })
+ .expect("Should fold one buffer");
+
+ let has_any_folded = search_view
+ .read_with(cx, |search_view, cx| {
+ search_view
+ .results_editor
+ .read(cx)
+ .has_any_buffer_folded(cx)
+ })
+ .expect("should read state");
+ assert!(
+ has_any_folded,
+ "Should report folds after manually folding one buffer"
+ );
+ }
+
#[perf]
#[gpui::test]
async fn test_deploy_project_search_focus(cx: &mut TestAppContext) {
@@ -25,17 +25,10 @@ impl SearchToken {
}
}
-#[derive(Clone, Debug)]
-pub enum CollapseDirection {
- Collapsed,
- Expanded,
-}
-
#[derive(Debug, Clone)]
pub enum SearchEvent {
MatchesInvalidated,
ActiveMatchChanged,
- ResultsCollapsedChanged(CollapseDirection),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]