Properly align excerpt and outline items (#13070)

Kirill Bulatov created

Change summary

crates/outline_panel/src/outline_panel.rs | 161 ++++++++++++------------
1 file changed, 79 insertions(+), 82 deletions(-)

Detailed changes

crates/outline_panel/src/outline_panel.rs 🔗

@@ -19,12 +19,12 @@ use editor::{
 use file_icons::FileIcons;
 use futures::{stream::FuturesUnordered, StreamExt};
 use gpui::{
-    actions, anchored, deferred, div, px, uniform_list, Action, AppContext, AssetSource,
-    AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId, EventEmitter,
-    FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, MouseButton,
-    MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful, Styled,
-    Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, anchored, deferred, div, px, uniform_list, Action, AnyElement, AppContext,
+    AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId,
+    EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyContext, Model,
+    MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful,
+    Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
+    WeakView, WindowContext,
 };
 use itertools::Itertools;
 use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
@@ -97,6 +97,7 @@ enum CollapsedEntry {
     Excerpt(BufferId, ExcerptId),
 }
 
+#[derive(Debug)]
 struct Excerpt {
     range: ExcerptRange<language::Anchor>,
     outlines: ExcerptOutlines,
@@ -126,6 +127,7 @@ impl Excerpt {
     }
 }
 
+#[derive(Debug)]
 enum ExcerptOutlines {
     Outlines(Vec<Outline>),
     Invalidated(Vec<Outline>),
@@ -1247,12 +1249,11 @@ impl OutlinePanel {
         let color = entry_git_aware_label_color(None, false, is_active);
         let icon = if has_outlines {
             FileIcons::get_chevron_icon(is_expanded, cx)
+                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
         } else {
             None
         }
-        .map(Icon::from_path)
-        .map(|icon| icon.color(color));
-        let depth = if icon.is_some() { depth + 1 } else { depth };
+        .unwrap_or_else(empty_icon);
 
         let buffer_snapshot = self
             .project
@@ -1274,7 +1275,7 @@ impl OutlinePanel {
             EntryRef::Excerpt(buffer_id, excerpt_id, range),
             item_id,
             depth,
-            icon,
+            Some(icon),
             is_active,
             label_element,
             cx,
@@ -1304,12 +1305,16 @@ impl OutlinePanel {
             }
             _ => false,
         };
-
+        let icon = if self.is_singleton_active(cx) {
+            None
+        } else {
+            Some(empty_icon())
+        };
         self.entry_element(
             EntryRef::Outline(buffer_id, excerpt_id, rendered_outline),
             item_id,
             depth,
-            None,
+            icon,
             is_active,
             label_element,
             cx,
@@ -1334,18 +1339,17 @@ impl OutlinePanel {
                     entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
                 let icon = if settings.file_icons {
                     FileIcons::get_icon(&entry.path, cx)
+                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
                 } else {
                     None
-                }
-                .map(Icon::from_path)
-                .map(|icon| icon.color(color));
+                };
                 (
                     ElementId::from(entry.id.to_proto() as usize),
                     Label::new(name)
                         .single_line()
                         .color(color)
                         .into_any_element(),
-                    icon,
+                    icon.unwrap_or_else(empty_icon),
                 )
             }
             FsEntry::Directory(worktree_id, entry) => {
@@ -1362,14 +1366,14 @@ impl OutlinePanel {
                     FileIcons::get_chevron_icon(is_expanded, cx)
                 }
                 .map(Icon::from_path)
-                .map(|icon| icon.color(color));
+                .map(|icon| icon.color(color).into_any_element());
                 (
                     ElementId::from(entry.id.to_proto() as usize),
                     Label::new(name)
                         .single_line()
                         .color(color)
                         .into_any_element(),
-                    icon,
+                    icon.unwrap_or_else(empty_icon),
                 )
             }
             FsEntry::ExternalFile(buffer_id, ..) => {
@@ -1384,7 +1388,7 @@ impl OutlinePanel {
                                 None
                             }
                             .map(Icon::from_path)
-                            .map(|icon| icon.color(color));
+                            .map(|icon| icon.color(color).into_any_element());
                             (icon, file_name(path.as_ref()))
                         }
                         None => (None, "Untitled".to_string()),
@@ -1397,7 +1401,7 @@ impl OutlinePanel {
                         .single_line()
                         .color(color)
                         .into_any_element(),
-                    icon,
+                    icon.unwrap_or_else(empty_icon),
                 )
             }
         };
@@ -1406,7 +1410,7 @@ impl OutlinePanel {
             EntryRef::Entry(rendered_entry),
             item_id,
             depth,
-            icon,
+            Some(icon),
             is_active,
             label_element,
             cx,
@@ -1450,7 +1454,7 @@ impl OutlinePanel {
                 FileIcons::get_chevron_icon(is_expanded, cx)
             }
             .map(Icon::from_path)
-            .map(|icon| icon.color(color));
+            .map(|icon| icon.color(color).into_any_element());
             (
                 ElementId::from(
                     dir_entries
@@ -1462,7 +1466,7 @@ impl OutlinePanel {
                     .single_line()
                     .color(color)
                     .into_any_element(),
-                icon,
+                icon.unwrap_or_else(empty_icon),
             )
         };
 
@@ -1470,7 +1474,7 @@ impl OutlinePanel {
             EntryRef::FoldedDirs(worktree_id, dir_entries),
             item_id,
             depth,
-            icon,
+            Some(icon),
             is_active,
             label_element,
             cx,
@@ -1483,7 +1487,7 @@ impl OutlinePanel {
         rendered_entry: EntryRef<'_>,
         item_id: ElementId,
         depth: usize,
-        icon: Option<Icon>,
+        icon_element: Option<AnyElement>,
         is_active: bool,
         label_element: gpui::AnyElement,
         cx: &mut ViewContext<OutlinePanel>,
@@ -1498,13 +1502,8 @@ impl OutlinePanel {
                     .indent_level(depth)
                     .indent_step_size(px(settings.indent_size))
                     .selected(is_active)
-                    .child(if let Some(icon) = icon {
-                        h_flex().child(icon)
-                    } else {
-                        h_flex()
-                            .size(IconSize::default().rems())
-                            .invisible()
-                            .flex_none()
+                    .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())
                     .on_click({
@@ -2172,32 +2171,15 @@ impl OutlinePanel {
     }
 
     fn entries_with_depths(&mut self, cx: &AppContext) -> &[(usize, EntryOwned)] {
+        let is_singleton = self.is_singleton_active(cx);
         self.cached_entries_with_depth.get_or_insert_with(|| {
             let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
             let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
             let mut entries = Vec::new();
-            let is_singleton = self
-                .active_item
-                .as_ref()
-                .and_then(|active_item| {
-                    Some(
-                        active_item
-                            .active_editor
-                            .upgrade()?
-                            .read(cx)
-                            .buffer()
-                            .read(cx)
-                            .is_singleton(),
-                    )
-                })
-                .unwrap_or(false);
 
             for entry in &self.fs_entries {
                 let depth = match entry {
                     FsEntry::Directory(worktree_id, dir_entry) => {
-                        if is_singleton {
-                            continue;
-                        }
                         let depth = self
                             .fs_entries_depth
                             .get(&(*worktree_id, dir_entry.id))
@@ -2240,16 +2222,11 @@ impl OutlinePanel {
                         depth
                     }
                     FsEntry::ExternalFile(..) => 0,
-                    FsEntry::File(worktree_id, file_entry, ..) => {
-                        if is_singleton {
-                            0
-                        } else {
-                            self.fs_entries_depth
-                                .get(&(*worktree_id, file_entry.id))
-                                .map(|&(_, depth)| depth)
-                                .unwrap_or(0)
-                        }
-                    }
+                    FsEntry::File(worktree_id, file_entry, ..) => self
+                        .fs_entries_depth
+                        .get(&(*worktree_id, file_entry.id))
+                        .map(|&(_, depth)| depth)
+                        .unwrap_or(0),
                 };
                 if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
                     entries.push((
@@ -2267,7 +2244,7 @@ impl OutlinePanel {
                             let Some(excerpt) = excerpts.get(&entry_excerpt) else {
                                 continue;
                             };
-                            let excerpt_depth = depth;
+                            let excerpt_depth = depth + 1;
                             entries.push((
                                 excerpt_depth,
                                 EntryOwned::Excerpt(
@@ -2277,30 +2254,25 @@ impl OutlinePanel {
                                 ),
                             ));
 
-                            if !self
+                            let mut outline_base_depth = excerpt_depth + 1;
+                            if is_singleton {
+                                outline_base_depth = 0;
+                                entries.clear();
+                            } else if self
                                 .collapsed_entries
                                 .contains(&CollapsedEntry::Excerpt(*buffer_id, entry_excerpt))
                             {
-                                let mut outline_data_depth = None::<usize>;
-                                let mut outline_depth = excerpt_depth + 1;
-                                for outline in excerpt.iter_outlines() {
-                                    if let Some(outline_data_depth) = outline_data_depth {
-                                        match outline_data_depth.cmp(&outline.depth) {
-                                            cmp::Ordering::Less => outline_depth += 1,
-                                            cmp::Ordering::Equal => {}
-                                            cmp::Ordering::Greater => outline_depth -= 1,
-                                        };
-                                    }
-                                    outline_data_depth = Some(outline.depth);
-                                    entries.push((
-                                        outline_depth,
-                                        EntryOwned::Outline(
-                                            *buffer_id,
-                                            entry_excerpt,
-                                            outline.clone(),
-                                        ),
-                                    ));
-                                }
+                                continue;
+                            }
+
+                            for outline in excerpt.iter_outlines() {
+                                entries.push((
+                                    outline_base_depth + outline.depth,
+                                    EntryOwned::Outline(*buffer_id, entry_excerpt, outline.clone()),
+                                ));
+                            }
+                            if is_singleton && entries.is_empty() {
+                                entries.push((0, EntryOwned::Entry(entry.clone())));
                             }
                         }
                     }
@@ -2316,6 +2288,23 @@ impl OutlinePanel {
         })
     }
 
+    fn is_singleton_active(&self, cx: &AppContext) -> bool {
+        self.active_item
+            .as_ref()
+            .and_then(|active_item| {
+                Some(
+                    active_item
+                        .active_editor
+                        .upgrade()?
+                        .read(cx)
+                        .buffer()
+                        .read(cx)
+                        .is_singleton(),
+                )
+            })
+            .unwrap_or(false)
+    }
+
     fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
         self.outline_fetch_tasks.clear();
         let mut ids = ids.into_iter().collect::<HashSet<_>>();
@@ -2668,3 +2657,11 @@ fn range_contains(
     range.start.cmp(&anchor, buffer_snapshot).is_le()
         && range.end.cmp(&anchor, buffer_snapshot).is_ge()
 }
+
+fn empty_icon() -> AnyElement {
+    h_flex()
+        .size(IconSize::default().rems())
+        .invisible()
+        .flex_none()
+        .into_any_element()
+}