From 03c6d6285c6d5988c1242c129ac246dc634e2c71 Mon Sep 17 00:00:00 2001 From: Chris <80088549+zeld-a@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:48:37 -0400 Subject: [PATCH] outline_panel: Fix collapse/expand all entries (#41342) Closes #39937 Release Notes: - Fixed expand/collapse all entries not working in singleton buffer mode --- crates/outline_panel/src/outline_panel.rs | 465 ++++++++++++++++++---- 1 file changed, 391 insertions(+), 74 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 112aa3d21ebda9ef57d3bedda20e3f90735a0173..f9b1afe34e5ebf51576b07164f5ccfa23428ca56 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1635,56 +1635,79 @@ impl OutlinePanel { let Some(active_editor) = self.active_editor() else { return; }; - let mut buffers_to_unfold = HashSet::default(); - let expanded_entries = - self.fs_entries - .iter() - .fold(HashSet::default(), |mut entries, fs_entry| { - match fs_entry { - FsEntry::ExternalFile(external_file) => { - buffers_to_unfold.insert(external_file.buffer_id); - entries.insert(CollapsedEntry::ExternalFile(external_file.buffer_id)); - entries.extend( - self.excerpts - .get(&external_file.buffer_id) - .into_iter() - .flat_map(|excerpts| { - excerpts.keys().map(|excerpt_id| { - CollapsedEntry::Excerpt( - external_file.buffer_id, - *excerpt_id, - ) - }) - }), - ); - } - FsEntry::Directory(directory) => { - entries.insert(CollapsedEntry::Dir( - directory.worktree_id, - directory.entry.id, + + let mut to_uncollapse: HashSet = HashSet::default(); + let mut buffers_to_unfold: HashSet = HashSet::default(); + + for fs_entry in &self.fs_entries { + match fs_entry { + FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + }) => { + to_uncollapse.insert(CollapsedEntry::File(*worktree_id, *buffer_id)); + buffers_to_unfold.insert(*buffer_id); + } + FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { + to_uncollapse.insert(CollapsedEntry::ExternalFile(*buffer_id)); + buffers_to_unfold.insert(*buffer_id); + } + FsEntry::Directory(FsEntryDirectory { + worktree_id, entry, .. + }) => { + to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, entry.id)); + } + } + } + + for (&buffer_id, excerpts) in &self.excerpts { + for (&excerpt_id, excerpt) in excerpts { + match &excerpt.outlines { + ExcerptOutlines::Outlines(outlines) => { + for outline in outlines { + to_uncollapse.insert(CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), )); } - FsEntry::File(file) => { - buffers_to_unfold.insert(file.buffer_id); - entries.insert(CollapsedEntry::File(file.worktree_id, file.buffer_id)); - entries.extend( - self.excerpts.get(&file.buffer_id).into_iter().flat_map( - |excerpts| { - excerpts.keys().map(|excerpt_id| { - CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id) - }) - }, - ), - ); + } + ExcerptOutlines::Invalidated(outlines) => { + for outline in outlines { + to_uncollapse.insert(CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + )); } - }; - entries - }); + } + ExcerptOutlines::NotFetched => {} + } + to_uncollapse.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id)); + } + } + + for cached in &self.cached_entries { + if let PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries, + .. + }) = &cached.entry + { + if let Some(last) = entries.last() { + to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, last.id)); + } + } + } + self.collapsed_entries - .retain(|entry| !expanded_entries.contains(entry)); + .retain(|entry| !to_uncollapse.contains(entry)); + active_editor.update(cx, |editor, cx| { buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); }); + if buffers_to_unfold.is_empty() { self.update_cached_entries(None, window, cx); } else { @@ -1703,37 +1726,44 @@ impl OutlinePanel { return; }; let mut buffers_to_fold = HashSet::default(); - let new_entries = self - .cached_entries - .iter() - .flat_map(|cached_entry| match &cached_entry.entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)), - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - buffers_to_fold.insert(*buffer_id); - Some(CollapsedEntry::File(*worktree_id, *buffer_id)) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - buffers_to_fold.insert(external_file.buffer_id); - Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) - } - PanelEntry::Search(_) | PanelEntry::Outline(..) => None, - }) - .collect::>(); - self.collapsed_entries.extend(new_entries); + self.collapsed_entries + .extend(self.cached_entries.iter().filter_map( + |cached_entry| match &cached_entry.entry { + PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { + worktree_id, + entry, + .. + })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)), + PanelEntry::Fs(FsEntry::File(FsEntryFile { + worktree_id, + buffer_id, + .. + })) => { + buffers_to_fold.insert(*buffer_id); + Some(CollapsedEntry::File(*worktree_id, *buffer_id)) + } + PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { + buffers_to_fold.insert(external_file.buffer_id); + Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) + } + PanelEntry::FoldedDirs(FoldedDirsEntry { + worktree_id, + entries, + .. + }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)), + PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { + Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) + } + PanelEntry::Outline(OutlineEntry::Outline(outline)) => { + Some(CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )) + } + PanelEntry::Search(_) => None, + }, + )); active_editor.update(cx, |editor, cx| { buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); @@ -6592,6 +6622,60 @@ outline: struct OutlineEntryExcerpt search: {{ "something": "static" }} src/ app/(site)/ + components/ + ErrorBoundary.tsx <==== selected + search: static"# + ) + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + format!(r#"frontend-project/"#) + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.expand_all_entries(&ExpandAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + format!( + r#"frontend-project/ + public/lottie/ + syntax-tree.json + search: {{ "something": "static" }} + src/ + app/(site)/ + (about)/jobs/[slug]/ + page.tsx + search: static + (blog)/post/[slug]/ + page.tsx + search: static components/ ErrorBoundary.tsx <==== selected search: static"# @@ -7510,4 +7594,237 @@ outline: fn main()" ); }); } + + #[gpui::test] + async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/test", + json!({ + "src": { + "lib.rs": indoc!(" + mod outer { + pub struct OuterStruct { + field: String, + } + impl OuterStruct { + pub fn new() -> Self { + Self { field: String::new() } + } + pub fn method(&self) { + println!(\"{}\", self.field); + } + } + mod inner { + pub fn inner_function() { + let x = 42; + println!(\"{}\", x); + } + pub struct InnerStruct { + value: i32, + } + } + } + fn main() { + let s = outer::OuterStruct::new(); + s.method(); + } + "), + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + project.read_with(cx, |project, _| { + project.languages().add(Arc::new( + rust_lang() + .with_outline_query( + r#" + (struct_item + (visibility_modifier)? @context + "struct" @context + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @context + "for"? @context + type: (_) @context + body: (_)) @item + (function_item + (visibility_modifier)? @context + "fn" @context + name: (_) @name + parameters: (_) @context) @item + (mod_item + (visibility_modifier)? @context + "mod" @context + name: (_) @name) @item + (enum_item + (visibility_modifier)? @context + "enum" @context + name: (_) @name) @item + (field_declaration + (visibility_modifier)? @context + name: (_) @name + ":" @context + type: (_) @context) @item + "#, + ) + .unwrap(), + )) + }); + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let outline_panel = outline_panel(&workspace, cx); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from("/test/src/lib.rs"), + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + // Force another update cycle to ensure outlines are fetched + outline_panel.update_in(cx, |panel, window, cx| { + panel.update_non_fs_items(window, cx); + panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: mod outer <==== selected + outline: pub struct OuterStruct + outline: field: String + outline: impl OuterStruct + outline: pub fn new() + outline: pub fn method(&self) + outline: mod inner + outline: pub fn inner_function() + outline: pub struct InnerStruct + outline: value: i32 +outline: fn main()" + ) + ); + }); + + let _parent_outline = outline_panel + .read_with(cx, |panel, _cx| { + panel + .cached_entries + .iter() + .find_map(|entry| match &entry.entry { + PanelEntry::Outline(OutlineEntry::Outline(outline)) + if panel + .outline_children_cache + .get(&outline.buffer_id) + .and_then(|children_map| { + let key = + (outline.outline.range.clone(), outline.outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false) => + { + Some(entry.entry.clone()) + } + _ => None, + }) + }) + .expect("Should find an outline with children"); + + // Collapse all entries + outline_panel.update_in(cx, |panel, window, cx| { + panel.collapse_all_entries(&CollapseAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + let expected_collapsed_output = indoc!( + " + outline: mod outer <==== selected + outline: fn main()" + ); + + outline_panel.update(cx, |panel, cx| { + assert_eq! { + display_entries( + &project, + &snapshot(panel, cx), + &panel.cached_entries, + panel.selected_entry(), + cx, + ), + expected_collapsed_output + }; + }); + + // Expand all entries + outline_panel.update_in(cx, |panel, window, cx| { + panel.expand_all_entries(&ExpandAllEntries, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + let expected_expanded_output = indoc!( + " + outline: mod outer <==== selected + outline: pub struct OuterStruct + outline: field: String + outline: impl OuterStruct + outline: pub fn new() + outline: pub fn method(&self) + outline: mod inner + outline: pub fn inner_function() + outline: pub struct InnerStruct + outline: value: i32 + outline: fn main()" + ); + + outline_panel.update(cx, |panel, cx| { + assert_eq! { + display_entries( + &project, + &snapshot(panel, cx), + &panel.cached_entries, + panel.selected_entry(), + cx, + ), + expected_expanded_output + }; + }); + } }