outline_panel: Fix collapse/expand all entries (#41342)

Closes #39937

Release Notes:

- Fixed expand/collapse all entries not working in singleton buffer mode
This commit is contained in:
Chris
2025-10-30 21:48:37 +00:00
committed by GitHub
parent 60c546196a
commit 03c6d6285c
+391 -74
View File
@@ -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<CollapsedEntry> = HashSet::default();
let mut buffers_to_unfold: HashSet<BufferId> = 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::<Vec<_>>();
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
};
});
}
}