diff --git a/assets/settings/default.json b/assets/settings/default.json index dab1684aef..3a7a48efc2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -691,7 +691,10 @@ // 5. Never show the scrollbar: // "never" "show": null - } + }, + // Default depth to expand outline items in the current file. + // Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper. + "expand_outlines_with_depth": 100 }, "collaboration_panel": { // Whether to show the collaboration panel button in the status bar. diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 12dcab9e87..50c6c2dcce 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1,19 +1,5 @@ mod outline_panel_settings; -use std::{ - cmp, - collections::BTreeMap, - hash::Hash, - ops::Range, - path::{MAIN_SEPARATOR_STR, Path, PathBuf}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicBool}, - }, - time::Duration, - u32, -}; - use anyhow::Context as _; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use db::kvp::KEY_VALUE_STORE; @@ -36,8 +22,21 @@ use gpui::{ uniform_list, }; use itertools::Itertools; -use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; +use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; +use std::{ + cmp, + collections::BTreeMap, + hash::Hash, + ops::Range, + path::{MAIN_SEPARATOR_STR, Path, PathBuf}, + sync::{ + Arc, OnceLock, + atomic::{self, AtomicBool}, + }, + time::Duration, + u32, +}; use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides}; use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem}; @@ -132,6 +131,8 @@ pub struct OutlinePanel { hide_scrollbar_task: Option>, max_width_item_index: Option, preserve_selection_on_buffer_fold_toggles: HashSet, + pending_default_expansion_depth: Option, + outline_children_cache: HashMap, usize), bool>>, } #[derive(Debug)] @@ -318,12 +319,13 @@ struct CachedEntry { entry: PanelEntry, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] enum CollapsedEntry { Dir(WorktreeId, ProjectEntryId), File(WorktreeId, BufferId), ExternalFile(BufferId), Excerpt(BufferId, ExcerptId), + Outline(BufferId, ExcerptId, Range), } #[derive(Debug)] @@ -803,8 +805,56 @@ impl OutlinePanel { outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); } } else if &outline_panel_settings != new_settings { + let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth; outline_panel_settings = *new_settings; - cx.notify(); + + if old_expansion_depth != new_settings.expand_outlines_with_depth { + let old_collapsed_entries = outline_panel.collapsed_entries.clone(); + outline_panel + .collapsed_entries + .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..))); + + let new_depth = new_settings.expand_outlines_with_depth; + + for (buffer_id, excerpts) in &outline_panel.excerpts { + for (excerpt_id, excerpt) in excerpts { + if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines { + for outline in outlines { + if outline_panel + .outline_children_cache + .get(buffer_id) + .and_then(|children_map| { + let key = + (outline.range.clone(), outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false) + && (new_depth == 0 || outline.depth >= new_depth) + { + outline_panel.collapsed_entries.insert( + CollapsedEntry::Outline( + *buffer_id, + *excerpt_id, + outline.range.clone(), + ), + ); + } + } + } + } + } + + if old_collapsed_entries != outline_panel.collapsed_entries { + outline_panel.update_cached_entries( + Some(UPDATE_DEBOUNCE), + window, + cx, + ); + } + } else { + cx.notify(); + } } }); @@ -841,6 +891,7 @@ impl OutlinePanel { updating_cached_entries: false, new_entries_for_fs_update: HashSet::default(), preserve_selection_on_buffer_fold_toggles: HashSet::default(), + pending_default_expansion_depth: None, fs_entries_update_task: Task::ready(()), cached_entries_update_task: Task::ready(()), reveal_selection_task: Task::ready(Ok(())), @@ -855,6 +906,7 @@ impl OutlinePanel { workspace_subscription, filter_update_subscription, ], + outline_children_cache: HashMap::default(), }; if let Some((item, editor)) = workspace_active_editor(workspace, cx) { outline_panel.replace_active_editor(item, editor, window, cx); @@ -1462,7 +1514,12 @@ impl OutlinePanel { PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) } - PanelEntry::Search(_) | PanelEntry::Outline(..) => return, + PanelEntry::Outline(OutlineEntry::Outline(outline)) => Some(CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )), + PanelEntry::Search(_) => return, }; let Some(collapsed_entry) = entry_to_expand else { return; @@ -1565,7 +1622,14 @@ impl OutlinePanel { PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self .collapsed_entries .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)), - PanelEntry::Search(_) | PanelEntry::Outline(..) => false, + PanelEntry::Outline(OutlineEntry::Outline(outline)) => { + self.collapsed_entries.insert(CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )) + } + PanelEntry::Search(_) => false, }; if collapsed { @@ -1780,7 +1844,17 @@ impl OutlinePanel { self.collapsed_entries.insert(collapsed_entry); } } - PanelEntry::Search(_) | PanelEntry::Outline(..) => return, + PanelEntry::Outline(OutlineEntry::Outline(outline)) => { + let collapsed_entry = CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + ); + if !self.collapsed_entries.remove(&collapsed_entry) { + self.collapsed_entries.insert(collapsed_entry); + } + } + _ => {} } active_editor.update(cx, |editor, cx| { @@ -2108,7 +2182,7 @@ impl OutlinePanel { PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())), item_id, depth, - Some(icon), + icon, is_active, label_element, window, @@ -2160,10 +2234,31 @@ impl OutlinePanel { _ => false, }; - let icon = if self.is_singleton_active(cx) { - None + let has_children = self + .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); + let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )); + + let icon = if has_children { + FileIcons::get_chevron_icon(is_expanded, cx) + .map(|icon_path| { + Icon::from_path(icon_path) + .color(entry_label_color(is_active)) + .into_any_element() + }) + .unwrap_or_else(empty_icon) } else { - Some(empty_icon()) + empty_icon() }; self.entry_element( @@ -2287,7 +2382,7 @@ impl OutlinePanel { PanelEntry::Fs(rendered_entry.clone()), item_id, depth, - Some(icon), + icon, is_active, label_element, window, @@ -2358,7 +2453,7 @@ impl OutlinePanel { PanelEntry::FoldedDirs(folded_dir.clone()), item_id, depth, - Some(icon), + icon, is_active, label_element, window, @@ -2449,7 +2544,7 @@ impl OutlinePanel { }), ElementId::from(SharedString::from(format!("search-{match_range:?}"))), depth, - None, + empty_icon(), is_active, entire_label, window, @@ -2462,7 +2557,7 @@ impl OutlinePanel { rendered_entry: PanelEntry, item_id: ElementId, depth: usize, - icon_element: Option, + icon_element: AnyElement, is_active: bool, label_element: gpui::AnyElement, window: &mut Window, @@ -2478,8 +2573,10 @@ impl OutlinePanel { if event.down.button == MouseButton::Right || event.down.first_mouse { return; } + let change_focus = event.down.click_count > 1; outline_panel.toggle_expanded(&clicked_entry, window, cx); + outline_panel.scroll_editor_to_entry( &clicked_entry, true, @@ -2495,10 +2592,11 @@ impl OutlinePanel { .indent_level(depth) .indent_step_size(px(settings.indent_size)) .toggle_state(is_active) - .when_some(icon_element, |list_item, icon_element| { - list_item.child(h_flex().child(icon_element)) - }) - .child(h_flex().h_6().child(label_element).ml_1()) + .child( + h_flex() + .child(h_flex().w(px(16.)).justify_center().child(icon_element)) + .child(h_flex().h_6().child(label_element).ml_1()), + ) .on_secondary_mouse_down(cx.listener( move |outline_panel, event: &MouseDownEvent, window, cx| { // Stop propagation to prevent the catch-all context menu for the project @@ -2940,7 +3038,12 @@ impl OutlinePanel { outline_panel.fs_entries_depth = new_depth_map; outline_panel.fs_children_count = new_children_count; outline_panel.update_non_fs_items(window, cx); - outline_panel.update_cached_entries(debounce, window, cx); + + // Only update cached entries if we don't have outlines to fetch + // If we do have outlines to fetch, let fetch_outdated_outlines handle the update + if outline_panel.excerpt_fetch_ranges(cx).is_empty() { + outline_panel.update_cached_entries(debounce, window, cx); + } cx.notify(); }) @@ -2956,6 +3059,12 @@ impl OutlinePanel { cx: &mut Context, ) { self.clear_previous(window, cx); + + let default_expansion_depth = + OutlinePanelSettings::get_global(cx).expand_outlines_with_depth; + // We'll apply the expansion depth after outlines are loaded + self.pending_default_expansion_depth = Some(default_expansion_depth); + let buffer_search_subscription = cx.subscribe_in( &new_active_editor, window, @@ -3004,6 +3113,7 @@ impl OutlinePanel { self.selected_entry = SelectedEntry::None; self.pinned = false; self.mode = ItemsDisplayMode::Outline; + self.pending_default_expansion_depth = None; } fn location_for_editor_selection( @@ -3259,25 +3369,74 @@ impl OutlinePanel { || buffer_language.as_ref() == buffer_snapshot.language_at(outline.range.start) }); - outlines + + let outlines_with_children = outlines + .windows(2) + .filter_map(|window| { + let current = &window[0]; + let next = &window[1]; + if next.depth > current.depth { + Some((current.range.clone(), current.depth)) + } else { + None + } + }) + .collect::>(); + + (outlines, outlines_with_children) }) .await; + + let (fetched_outlines, outlines_with_children) = fetched_outlines; + outline_panel .update_in(cx, |outline_panel, window, cx| { + let pending_default_depth = + outline_panel.pending_default_expansion_depth.take(); + + let debounce = + if first_update.fetch_and(false, atomic::Ordering::AcqRel) { + None + } else { + Some(UPDATE_DEBOUNCE) + }; + if let Some(excerpt) = outline_panel .excerpts .entry(buffer_id) .or_default() .get_mut(&excerpt_id) { - let debounce = if first_update - .fetch_and(false, atomic::Ordering::AcqRel) - { - None - } else { - Some(UPDATE_DEBOUNCE) - }; excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); + + if let Some(default_depth) = pending_default_depth { + if let ExcerptOutlines::Outlines(outlines) = + &excerpt.outlines + { + outlines + .iter() + .filter(|outline| { + (default_depth == 0 + || outline.depth >= default_depth) + && outlines_with_children.contains(&( + outline.range.clone(), + outline.depth, + )) + }) + .for_each(|outline| { + outline_panel.collapsed_entries.insert( + CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + ), + ); + }); + } + } + + // Even if no outlines to check, we still need to update cached entries + // to show the outline entries that were just fetched outline_panel.update_cached_entries(debounce, window, cx); } }) @@ -4083,7 +4242,7 @@ impl OutlinePanel { } fn add_excerpt_entries( - &self, + &mut self, state: &mut GenerationState, buffer_id: BufferId, entries_to_add: &[ExcerptId], @@ -4094,6 +4253,8 @@ impl OutlinePanel { cx: &mut Context, ) { if let Some(excerpts) = self.excerpts.get(&buffer_id) { + let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx); + for &excerpt_id in entries_to_add { let Some(excerpt) = excerpts.get(&excerpt_id) else { continue; @@ -4123,15 +4284,84 @@ impl OutlinePanel { continue; } - for outline in excerpt.iter_outlines() { + let mut last_depth_at_level: Vec>> = vec![None; 10]; + + let all_outlines: Vec<_> = excerpt.iter_outlines().collect(); + + let mut outline_has_children = HashMap::default(); + let mut visible_outlines = Vec::new(); + let mut collapsed_state: Option<(usize, Range)> = None; + + for (i, &outline) in all_outlines.iter().enumerate() { + let has_children = all_outlines + .get(i + 1) + .map(|next| next.depth > outline.depth) + .unwrap_or(false); + + outline_has_children + .insert((outline.range.clone(), outline.depth), has_children); + + let mut should_include = true; + + if let Some((collapsed_depth, collapsed_range)) = &collapsed_state { + if outline.depth <= *collapsed_depth { + collapsed_state = None; + } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() { + let outline_start = outline.range.start; + if outline_start + .cmp(&collapsed_range.start, buffer_snapshot) + .is_ge() + && outline_start + .cmp(&collapsed_range.end, buffer_snapshot) + .is_lt() + { + should_include = false; // Skip - inside collapsed range + } else { + collapsed_state = None; + } + } + } + + // Check if this outline itself is collapsed + if should_include + && self.collapsed_entries.contains(&CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + )) + { + collapsed_state = Some((outline.depth, outline.range.clone())); + } + + if should_include { + visible_outlines.push(outline); + } + } + + self.outline_children_cache + .entry(buffer_id) + .or_default() + .extend(outline_has_children); + + for outline in visible_outlines { + let outline_entry = OutlineEntryOutline { + buffer_id, + excerpt_id, + outline: outline.clone(), + }; + + if outline.depth < last_depth_at_level.len() { + last_depth_at_level[outline.depth] = Some(outline.range.clone()); + // Clear deeper levels when we go back to a shallower depth + for d in (outline.depth + 1)..last_depth_at_level.len() { + last_depth_at_level[d] = None; + } + } + self.push_entry( state, track_matches, - PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline { - buffer_id, - excerpt_id, - outline: outline.clone(), - })), + PanelEntry::Outline(OutlineEntry::Outline(outline_entry)), outline_base_depth + outline.depth, cx, ); @@ -6908,4 +7138,540 @@ outline: struct OutlineEntryExcerpt multi_buffer_snapshot.text_for_range(line_start..line_end).collect::().trim().to_owned() }) } + + #[gpui::test] + async fn test_outline_keyboard_expand_collapse(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"); + + outline_panel.update_in(cx, |panel, window, cx| { + panel.select_entry(parent_outline.clone(), true, window, cx); + panel.collapse_selected_entry(&CollapseSelectedEntry, 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, + ), + indoc!( + " +outline: mod outer <==== selected +outline: fn main()" + ) + ); + }); + + outline_panel.update_in(cx, |panel, window, cx| { + panel.expand_selected_entry(&ExpandSelectedEntry, 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, + ), + 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_in(cx, |panel, window, cx| { + panel.collapsed_entries.clear(); + panel.update_cached_entries(None, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update_in(cx, |panel, window, cx| { + let outlines_with_children: Vec<_> = panel + .cached_entries + .iter() + .filter_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, + }) + .collect(); + + for outline in outlines_with_children { + panel.select_entry(outline, false, window, cx); + panel.collapse_selected_entry(&CollapseSelectedEntry, 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, + ), + indoc!( + " +outline: mod outer +outline: fn main()" + ) + ); + }); + + let collapsed_entries_count = + outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len()); + assert!( + collapsed_entries_count > 0, + "Should have collapsed entries tracked" + ); + } + + #[gpui::test] + async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/test", + json!({ + "src": { + "main.rs": indoc!(" + struct Config { + name: String, + value: i32, + } + impl Config { + fn new(name: String) -> Self { + Self { name, value: 0 } + } + fn get_value(&self) -> i32 { + self.value + } + } + enum Status { + Active, + Inactive, + } + fn process_config(config: Config) -> Status { + if config.get_value() > 0 { + Status::Active + } else { + Status::Inactive + } + } + fn main() { + let config = Config::new(\"test\".to_string()); + let status = process_config(config); + } + "), + } + }), + ) + .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) + }); + + let _editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from("/test/src/main.rs"), + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, _cx| { + outline_panel.selected_entry = SelectedEntry::None; + }); + + // Check initial state - all entries should be expanded by default + 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: struct Config + outline: name: String + outline: value: i32 +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + + outline_panel.update(cx, |outline_panel, _cx| { + outline_panel.selected_entry = SelectedEntry::None; + }); + + cx.update(|window, cx| { + outline_panel.update(cx, |outline_panel, cx| { + outline_panel.select_first(&SelectFirst, 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, + ), + indoc!( + " +outline: struct Config <==== selected + outline: name: String + outline: value: i32 +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + + cx.update(|window, cx| { + outline_panel.update(cx, |outline_panel, cx| { + outline_panel.open_selected_entry(&OpenSelectedEntry, 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, + ), + indoc!( + " +outline: struct Config <==== selected +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + + cx.update(|window, cx| { + outline_panel.update(cx, |outline_panel, cx| { + outline_panel.open_selected_entry(&OpenSelectedEntry, 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, + ), + indoc!( + " +outline: struct Config <==== selected + outline: name: String + outline: value: i32 +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + } } diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 6b70cb54fb..133d28b748 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -31,6 +31,7 @@ pub struct OutlinePanelSettings { pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, pub scrollbar: ScrollbarSettings, + pub expand_outlines_with_depth: usize, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -105,6 +106,13 @@ pub struct OutlinePanelSettingsContent { pub indent_guides: Option, /// Scrollbar-related settings pub scrollbar: Option, + /// Default depth to expand outline items in the current file. + /// The default depth to which outline entries are expanded on reveal. + /// - Set to 0 to collapse all items that have children + /// - Set to 1 or higher to collapse items at that depth or deeper + /// + /// Default: 100 + pub expand_outlines_with_depth: Option, } impl Settings for OutlinePanelSettings {