Add excerpts into outline panel (#13034)

Kirill Bulatov created

Follow-up of https://github.com/zed-industries/zed/pull/12637

Adds excerpt items into the outline panel: now all outline items are
initially hidden under excerpt items that could be toggled open/closed
similar to directories.


![Screenshot 2024-06-14 at 10 45
04](https://github.com/zed-industries/zed/assets/2690773/9c9ef91b-1666-43c3-acc4-96f850098a28)

On active editor's selection change, a corresponding outline will be
revealed still, expanding the corresponding excerpt

![Screenshot 2024-06-14 at 10 45
13](https://github.com/zed-industries/zed/assets/2690773/7dfd14f7-4aca-48f2-8760-8e1362b9a043)

Release Notes:

- N/A

Change summary

Cargo.lock                                    |   3 
crates/collab_ui/src/channel_view.rs          |   2 
crates/editor/src/editor.rs                   |  10 
crates/editor/src/items.rs                    |   2 
crates/language_tools/src/syntax_tree_view.rs |   2 
crates/multi_buffer/src/multi_buffer.rs       |  14 
crates/outline_panel/Cargo.toml               |   3 
crates/outline_panel/src/outline_panel.rs     | 738 +++++++++-----------
8 files changed, 352 insertions(+), 422 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7160,8 +7160,9 @@ dependencies = [
  "db",
  "editor",
  "file_icons",
- "git",
+ "futures 0.3.28",
  "gpui",
+ "itertools 0.11.0",
  "language",
  "log",
  "menu",

crates/collab_ui/src/channel_view.rs 🔗

@@ -228,7 +228,7 @@ impl ChannelView {
             &self.editor,
             move |this, _, e: &EditorEvent, cx| {
                 match e {
-                    EditorEvent::Reparsed => {
+                    EditorEvent::Reparsed(_) => {
                         this.focus_position_from_link(position.clone(), false, cx);
                         this._reparse_subscription.take();
                     }

crates/editor/src/editor.rs 🔗

@@ -10888,14 +10888,14 @@ impl Editor {
             multi_buffer::Event::ExcerptsExpanded { ids } => {
                 cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
             }
-            multi_buffer::Event::Reparsed => {
+            multi_buffer::Event::Reparsed(buffer_id) => {
                 self.tasks_update_task = Some(self.refresh_runnables(cx));
 
-                cx.emit(EditorEvent::Reparsed);
+                cx.emit(EditorEvent::Reparsed(*buffer_id));
             }
-            multi_buffer::Event::LanguageChanged => {
+            multi_buffer::Event::LanguageChanged(buffer_id) => {
                 linked_editing_ranges::refresh_linked_ranges(self, cx);
-                cx.emit(EditorEvent::Reparsed);
+                cx.emit(EditorEvent::Reparsed(*buffer_id));
                 cx.notify();
             }
             multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
@@ -11818,7 +11818,7 @@ pub enum EditorEvent {
     Edited {
         transaction_id: clock::Lamport,
     },
-    Reparsed,
+    Reparsed(BufferId),
     Focused,
     Blurred,
     DirtyChanged,

crates/editor/src/items.rs 🔗

@@ -903,7 +903,7 @@ impl Item for Editor {
                 f(ItemEvent::UpdateBreadcrumbs);
             }
 
-            EditorEvent::Reparsed => {
+            EditorEvent::Reparsed(_) => {
                 f(ItemEvent::UpdateBreadcrumbs);
             }
 

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -110,7 +110,7 @@ impl SyntaxTreeView {
 
         let subscription = cx.subscribe(&editor, |this, _, event, cx| {
             let did_reparse = match event {
-                editor::EditorEvent::Reparsed => true,
+                editor::EditorEvent::Reparsed(_) => true,
                 editor::EditorEvent::SelectionsChanged { .. } => false,
                 _ => return,
             };

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -94,9 +94,9 @@ pub enum Event {
     DiffUpdated {
         buffer: Model<Buffer>,
     },
-    LanguageChanged,
+    LanguageChanged(BufferId),
     CapabilityChanged,
-    Reparsed,
+    Reparsed(BufferId),
     Saved,
     FileHandleChanged,
     Closed,
@@ -538,9 +538,13 @@ impl MultiBuffer {
         });
 
         if let Some(buffer) = self.as_singleton() {
-            return buffer.update(cx, |buffer, cx| {
+            buffer.update(cx, |buffer, cx| {
                 buffer.edit(edits, autoindent_mode, cx);
             });
+            cx.emit(Event::ExcerptsEdited {
+                ids: self.excerpt_ids(),
+            });
+            return;
         }
 
         let original_indent_columns = match &mut autoindent_mode {
@@ -1639,8 +1643,8 @@ impl MultiBuffer {
             language::Event::Reloaded => Event::Reloaded,
             language::Event::DiffBaseChanged => Event::DiffBaseChanged,
             language::Event::DiffUpdated => Event::DiffUpdated { buffer },
-            language::Event::LanguageChanged => Event::LanguageChanged,
-            language::Event::Reparsed => Event::Reparsed,
+            language::Event::LanguageChanged => Event::LanguageChanged(buffer.read(cx).remote_id()),
+            language::Event::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()),
             language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
             language::Event::Closed => Event::Closed,
             language::Event::CapabilityChanged => {

crates/outline_panel/Cargo.toml 🔗

@@ -18,7 +18,8 @@ collections.workspace = true
 db.workspace = true
 editor.workspace = true
 file_icons.workspace = true
-git.workspace = true
+futures.workspace = true
+itertools.workspace = true
 gpui.workspace = true
 language.workspace = true
 log.workspace = true

crates/outline_panel/src/outline_panel.rs 🔗

@@ -2,7 +2,6 @@ mod outline_panel_settings;
 
 use std::{
     cmp,
-    hash::Hash,
     ops::Range,
     path::{Path, PathBuf},
     sync::Arc,
@@ -15,10 +14,10 @@ use db::kvp::KEY_VALUE_STORE;
 use editor::{
     items::{entry_git_aware_label_color, entry_label_color},
     scroll::ScrollAnchor,
-    Editor, EditorEvent, ExcerptId,
+    Editor, EditorEvent, ExcerptId, ExcerptRange,
 };
 use file_icons::FileIcons;
-use git::repository::GitFileStatus;
+use futures::{stream::FuturesUnordered, StreamExt};
 use gpui::{
     actions, anchored, deferred, div, px, uniform_list, Action, AppContext, AssetSource,
     AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId, EventEmitter,
@@ -27,11 +26,12 @@ use gpui::{
     Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView,
     WindowContext,
 };
-use language::{BufferId, OffsetRangeExt, OutlineItem};
+use itertools::Itertools;
+use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
 
 use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
-use project::{EntryKind, File, Fs, Project};
+use project::{File, Fs, Project};
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use unicase::UniCase;
@@ -80,7 +80,7 @@ pub struct OutlinePanel {
     pending_serialization: Task<Option<()>>,
     fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), (bool, usize)>,
     fs_entries: Vec<FsEntry>,
-    collapsed_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
+    collapsed_entries: HashSet<CollapsedEntry>,
     unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
     last_visible_range: Range<usize>,
     selected_entry: Option<EntryOwned>,
@@ -88,15 +88,57 @@ pub struct OutlinePanel {
     _subscriptions: Vec<Subscription>,
     update_task: Task<()>,
     outline_fetch_tasks: Vec<Task<()>>,
-    outlines: HashMap<OutlinesContainer, Vec<Outline>>,
+    excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
     cached_entries_with_depth: Option<Vec<(usize, EntryOwned)>>,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+enum CollapsedEntry {
+    Dir(WorktreeId, ProjectEntryId),
+    Excerpt(BufferId, ExcerptId),
+}
+
+struct Excerpt {
+    range: ExcerptRange<language::Anchor>,
+    outlines: ExcerptOutlines,
+}
+
+impl Excerpt {
+    fn invalidate_outlines(&mut self) {
+        if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines {
+            self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines));
+        }
+    }
+
+    fn iter_outlines(&self) -> impl Iterator<Item = &Outline> {
+        match &self.outlines {
+            ExcerptOutlines::Outlines(outlines) => outlines.iter(),
+            ExcerptOutlines::Invalidated(outlines) => outlines.iter(),
+            ExcerptOutlines::NotFetched => [].iter(),
+        }
+    }
+
+    fn should_fetch_outlines(&self) -> bool {
+        match &self.outlines {
+            ExcerptOutlines::Outlines(_) => false,
+            ExcerptOutlines::Invalidated(_) => true,
+            ExcerptOutlines::NotFetched => true,
+        }
+    }
+}
+
+enum ExcerptOutlines {
+    Outlines(Vec<Outline>),
+    Invalidated(Vec<Outline>),
+    NotFetched,
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 enum EntryOwned {
     Entry(FsEntry),
     FoldedDirs(WorktreeId, Vec<Entry>),
-    Outline(OutlinesContainer, Outline),
+    Excerpt(BufferId, ExcerptId, ExcerptRange<language::Anchor>),
+    Outline(BufferId, ExcerptId, Outline),
 }
 
 impl EntryOwned {
@@ -104,7 +146,12 @@ impl EntryOwned {
         match self {
             Self::Entry(entry) => EntryRef::Entry(entry),
             Self::FoldedDirs(worktree_id, dirs) => EntryRef::FoldedDirs(*worktree_id, dirs),
-            Self::Outline(container, outline) => EntryRef::Outline(*container, outline),
+            Self::Excerpt(buffer_id, excerpt_id, range) => {
+                EntryRef::Excerpt(*buffer_id, *excerpt_id, range)
+            }
+            Self::Outline(buffer_id, excerpt_id, outline) => {
+                EntryRef::Outline(*buffer_id, *excerpt_id, outline)
+            }
         }
     }
 
@@ -117,15 +164,7 @@ impl EntryOwned {
                     .worktree_for_id(*worktree_id, cx)
                     .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
             }),
-            Self::Outline(..) => None,
-        }
-    }
-
-    fn outlines_container(&self) -> Option<OutlinesContainer> {
-        match self {
-            Self::Entry(entry) => entry.outlines_container(),
-            Self::FoldedDirs(..) => None,
-            Self::Outline(container, _) => Some(*container),
+            Self::Excerpt(..) | Self::Outline(..) => None,
         }
     }
 }
@@ -134,7 +173,8 @@ impl EntryOwned {
 enum EntryRef<'a> {
     Entry(&'a FsEntry),
     FoldedDirs(WorktreeId, &'a [Entry]),
-    Outline(OutlinesContainer, &'a Outline),
+    Excerpt(BufferId, ExcerptId, &'a ExcerptRange<language::Anchor>),
+    Outline(BufferId, ExcerptId, &'a Outline),
 }
 
 impl EntryRef<'_> {
@@ -144,34 +184,34 @@ impl EntryRef<'_> {
             &Self::FoldedDirs(worktree_id, dirs) => {
                 EntryOwned::FoldedDirs(worktree_id, dirs.to_vec())
             }
-            &Self::Outline(container, outline) => EntryOwned::Outline(container, outline.clone()),
+            &Self::Excerpt(buffer_id, excerpt_id, range) => {
+                EntryOwned::Excerpt(buffer_id, excerpt_id, range.clone())
+            }
+            &Self::Outline(buffer_id, excerpt_id, outline) => {
+                EntryOwned::Outline(buffer_id, excerpt_id, outline.clone())
+            }
         }
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-enum OutlinesContainer {
-    ExternalFile(BufferId),
-    File(WorktreeId, ProjectEntryId),
-}
-
 #[derive(Clone, Debug, Eq)]
 enum FsEntry {
-    ExternalFile(BufferId),
+    ExternalFile(BufferId, Vec<ExcerptId>),
     Directory(WorktreeId, Entry),
-    File(WorktreeId, Entry),
+    File(WorktreeId, Entry, BufferId, Vec<ExcerptId>),
 }
 
 impl PartialEq for FsEntry {
     fn eq(&self, other: &Self) -> bool {
         match (self, other) {
-            (Self::ExternalFile(id_a), Self::ExternalFile(id_b)) => id_a == id_b,
+            (Self::ExternalFile(id_a, _), Self::ExternalFile(id_b, _)) => id_a == id_b,
             (Self::Directory(id_a, entry_a), Self::Directory(id_b, entry_b)) => {
                 id_a == id_b && entry_a.id == entry_b.id
             }
-            (Self::File(worktree_a, entry_a), Self::File(worktree_b, entry_b)) => {
-                worktree_a == worktree_b && entry_a.id == entry_b.id
-            }
+            (
+                Self::File(worktree_a, entry_a, id_a, ..),
+                Self::File(worktree_b, entry_b, id_b, ..),
+            ) => worktree_a == worktree_b && entry_a.id == entry_b.id && id_a == id_b,
             _ => false,
         }
     }
@@ -180,7 +220,7 @@ impl PartialEq for FsEntry {
 impl FsEntry {
     fn abs_path(&self, project: &Model<Project>, cx: &AppContext) -> Option<PathBuf> {
         match self {
-            Self::ExternalFile(buffer_id) => project
+            Self::ExternalFile(buffer_id, _) => project
                 .read(cx)
                 .buffer_for_id(*buffer_id)
                 .and_then(|buffer| File::from_dyn(buffer.read(cx).file()))
@@ -191,7 +231,7 @@ impl FsEntry {
                 .read(cx)
                 .absolutize(&entry.path)
                 .ok(),
-            Self::File(worktree_id, entry) => project
+            Self::File(worktree_id, entry, _, _) => project
                 .read(cx)
                 .worktree_for_id(*worktree_id, cx)?
                 .read(cx)
@@ -206,21 +246,13 @@ impl FsEntry {
         cx: &'a AppContext,
     ) -> Option<&'a Path> {
         match self {
-            Self::ExternalFile(buffer_id) => project
+            Self::ExternalFile(buffer_id, _) => project
                 .read(cx)
                 .buffer_for_id(*buffer_id)
                 .and_then(|buffer| buffer.read(cx).file())
                 .map(|file| file.path().as_ref()),
             Self::Directory(_, entry) => Some(entry.path.as_ref()),
-            Self::File(_, entry) => Some(entry.path.as_ref()),
-        }
-    }
-
-    fn outlines_container(&self) -> Option<OutlinesContainer> {
-        match self {
-            Self::ExternalFile(buffer_id) => Some(OutlinesContainer::ExternalFile(*buffer_id)),
-            Self::File(worktree_id, entry) => Some(OutlinesContainer::File(*worktree_id, entry.id)),
-            Self::Directory(..) => None,
+            Self::File(_, entry, ..) => Some(entry.path.as_ref()),
         }
     }
 }
@@ -228,7 +260,7 @@ impl FsEntry {
 struct ActiveItem {
     item_id: EntityId,
     active_editor: WeakView<Editor>,
-    _editor_subscrpiption: Option<Subscription>,
+    _editor_subscrpiption: Subscription,
 }
 
 #[derive(Debug)]
@@ -241,22 +273,6 @@ struct SerializedOutlinePanel {
     width: Option<Pixels>,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct EntryDetails {
-    filename: String,
-    icon: Option<Arc<str>>,
-    path: Arc<Path>,
-    depth: usize,
-    kind: EntryKind,
-    is_ignored: bool,
-    is_expanded: bool,
-    is_selected: bool,
-    git_status: Option<GitFileStatus>,
-    is_private: bool,
-    worktree_id: WorktreeId,
-    canonical_path: Option<PathBuf>,
-}
-
 pub fn init_settings(cx: &mut AppContext) {
     OutlinePanelSettings::register(cx);
 }
@@ -357,7 +373,7 @@ impl OutlinePanel {
                 focus_handle,
                 fs_entries: Vec::new(),
                 fs_entries_depth: HashMap::default(),
-                collapsed_dirs: HashMap::default(),
+                collapsed_entries: HashSet::default(),
                 unfolded_dirs: HashMap::default(),
                 selected_entry: None,
                 context_menu: None,
@@ -366,7 +382,7 @@ impl OutlinePanel {
                 pending_serialization: Task::ready(None),
                 update_task: Task::ready(()),
                 outline_fetch_tasks: Vec::new(),
-                outlines: HashMap::default(),
+                excerpts: HashMap::default(),
                 last_visible_range: 0..0,
                 cached_entries_with_depth: None,
                 _subscriptions: vec![
@@ -509,8 +525,8 @@ impl OutlinePanel {
             Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
         };
 
-        match &entry {
-            EntryOwned::Entry(FsEntry::ExternalFile(buffer_id)) => {
+        match entry {
+            EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _)) => {
                 let scroll_target = multi_buffer_snapshot.excerpts().find_map(
                     |(excerpt_id, buffer_snapshot, excerpt_range)| {
                         if &buffer_snapshot.remote_id() == buffer_id {
@@ -540,7 +556,7 @@ impl OutlinePanel {
             entry @ EntryOwned::FoldedDirs(..) => {
                 self.toggle_expanded(entry, cx);
             }
-            EntryOwned::Entry(FsEntry::File(_, file_entry)) => {
+            EntryOwned::Entry(FsEntry::File(_, file_entry, ..)) => {
                 let scroll_target = self
                     .project
                     .update(cx, |project, cx| {
@@ -571,45 +587,11 @@ impl OutlinePanel {
                     })
                 }
             }
-            EntryOwned::Outline(_, outline) => {
-                let Some(full_buffer_snapshot) =
-                    outline
-                        .range
-                        .start
-                        .buffer_id
-                        .and_then(|buffer_id| active_multi_buffer.read(cx).buffer(buffer_id))
-                        .or_else(|| {
-                            outline.range.end.buffer_id.and_then(|buffer_id| {
-                                active_multi_buffer.read(cx).buffer(buffer_id)
-                            })
-                        })
-                        .map(|buffer| buffer.read(cx).snapshot())
-                else {
-                    return;
-                };
-                let outline_offset_range = outline.range.to_offset(&full_buffer_snapshot);
+            EntryOwned::Outline(_, excerpt_id, outline) => {
                 let scroll_target = multi_buffer_snapshot
-                    .excerpts()
-                    .filter(|(_, buffer_snapshot, _)| {
-                        let buffer_id = buffer_snapshot.remote_id();
-                        Some(buffer_id) == outline.range.start.buffer_id
-                            || Some(buffer_id) == outline.range.end.buffer_id
-                    })
-                    .min_by_key(|(_, _, excerpt_range)| {
-                        let excerpt_offeset_range =
-                            excerpt_range.context.to_offset(&full_buffer_snapshot);
-                        ((outline_offset_range.start / 2 + outline_offset_range.end / 2) as isize
-                            - (excerpt_offeset_range.start / 2 + excerpt_offeset_range.end / 2)
-                                as isize)
-                            .abs()
-                    })
-                    .and_then(|(excerpt_id, excerpt_snapshot, excerpt_range)| {
-                        let location = if outline.range.start.is_valid(excerpt_snapshot) {
-                            outline.range.start
-                        } else {
-                            excerpt_range.context.start
-                        };
-                        multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, location)
+                    .anchor_in_excerpt(*excerpt_id, outline.range.start)
+                    .or_else(|| {
+                        multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, outline.range.end)
                     });
                 if let Some(anchor) = scroll_target {
                     self.selected_entry = Some(entry.clone());
@@ -624,242 +606,163 @@ impl OutlinePanel {
                     })
                 }
             }
+            excerpt_entry @ EntryOwned::Excerpt(_, excerpt_id, excerpt_range) => {
+                self.toggle_expanded(excerpt_entry, cx);
+                let scroll_target = multi_buffer_snapshot
+                    .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start);
+                if let Some(anchor) = scroll_target {
+                    self.selected_entry = Some(entry.clone());
+                    active_editor.update(cx, |editor, cx| {
+                        editor.set_scroll_anchor(
+                            ScrollAnchor {
+                                offset: Point::default(),
+                                anchor,
+                            },
+                            cx,
+                        );
+                    })
+                }
+            }
         }
     }
 
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if let Some(selected_entry) = &self.selected_entry {
-            let outline_to_select = match selected_entry {
-                EntryOwned::Entry(entry) => entry.outlines_container().and_then(|container| {
-                    let next_outline = self.outlines.get(&container)?.first()?.clone();
-                    Some((container, next_outline))
-                }),
-                EntryOwned::FoldedDirs(..) => None,
-                EntryOwned::Outline(container, outline) => self
-                    .outlines
-                    .get(container)
-                    .and_then(|outlines| {
-                        outlines.iter().skip_while(|o| o != &outline).skip(1).next()
-                    })
-                    .map(|outline| (*container, outline.clone())),
-            }
-            .map(|(container, outline)| EntryOwned::Outline(container, outline));
-
-            let entry_to_select = outline_to_select.or_else(|| {
-                match selected_entry {
-                    EntryOwned::Entry(entry) => self
-                        .fs_entries
-                        .iter()
-                        .skip_while(|e| e != &entry)
-                        .skip(1)
-                        .next(),
-                    EntryOwned::FoldedDirs(worktree_id, dirs) => self
-                        .fs_entries
-                        .iter()
-                        .skip_while(|e| {
-                            if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
-                                dir_worktree_id != worktree_id || dirs.last() != Some(dir_entry)
-                            } else {
-                                true
-                            }
-                        })
-                        .skip(1)
-                        .next(),
-                    EntryOwned::Outline(container, _) => self
-                        .fs_entries
-                        .iter()
-                        .skip_while(|entry| entry.outlines_container().as_ref() != Some(container))
-                        .skip(1)
-                        .next(),
-                }
+        if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
+            self.entries_with_depths(cx)
+                .iter()
+                .map(|(_, entry)| entry)
+                .skip_while(|entry| entry != &&selected_entry)
+                .skip(1)
+                .next()
                 .cloned()
-                .map(EntryOwned::Entry)
-            });
-
-            if let Some(entry_to_select) = entry_to_select {
-                self.selected_entry = Some(entry_to_select);
-                self.autoscroll(cx);
-                cx.notify();
-            }
+        }) {
+            self.selected_entry = Some(entry_to_select);
+            self.autoscroll(cx);
+            cx.notify();
         } else {
             self.select_first(&SelectFirst {}, cx)
         }
     }
 
     fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if let Some(selected_entry) = &self.selected_entry {
-            let outline_to_select = match selected_entry {
-                EntryOwned::Entry(entry) => {
-                    let previous_entry = self
-                        .fs_entries
-                        .iter()
-                        .rev()
-                        .skip_while(|e| e != &entry)
-                        .skip(1)
-                        .next();
-                    previous_entry
-                        .and_then(|entry| entry.outlines_container())
-                        .and_then(|container| {
-                            let previous_outline = self.outlines.get(&container)?.last()?.clone();
-                            Some((container, previous_outline))
-                        })
-                }
-                EntryOwned::FoldedDirs(worktree_id, dirs) => {
-                    let previous_entry = self
-                        .fs_entries
-                        .iter()
-                        .rev()
-                        .skip_while(|e| {
-                            if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
-                                dir_worktree_id != worktree_id || dirs.first() != Some(dir_entry)
-                            } else {
-                                true
-                            }
-                        })
-                        .skip(1)
-                        .next();
-                    previous_entry
-                        .and_then(|entry| entry.outlines_container())
-                        .and_then(|container| {
-                            let previous_outline = self.outlines.get(&container)?.last()?.clone();
-                            Some((container, previous_outline))
-                        })
-                }
-                EntryOwned::Outline(container, outline) => self
-                    .outlines
-                    .get(container)
-                    .and_then(|outlines| {
-                        outlines
-                            .iter()
-                            .rev()
-                            .skip_while(|o| o != &outline)
-                            .skip(1)
-                            .next()
-                    })
-                    .map(|outline| (*container, outline.clone())),
-            }
-            .map(|(container, outline)| EntryOwned::Outline(container, outline));
-
-            let entry_to_select = outline_to_select.or_else(|| {
-                match selected_entry {
-                    EntryOwned::Entry(entry) => self
-                        .fs_entries
-                        .iter()
-                        .rev()
-                        .skip_while(|e| e != &entry)
-                        .skip(1)
-                        .next(),
-                    EntryOwned::FoldedDirs(worktree_id, dirs) => self
-                        .fs_entries
-                        .iter()
-                        .rev()
-                        .skip_while(|e| {
-                            if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
-                                dir_worktree_id != worktree_id || dirs.first() != Some(dir_entry)
-                            } else {
-                                true
-                            }
-                        })
-                        .skip(1)
-                        .next(),
-                    EntryOwned::Outline(container, _) => self
-                        .fs_entries
-                        .iter()
-                        .rev()
-                        .find(|entry| entry.outlines_container().as_ref() == Some(container)),
-                }
+        if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
+            self.entries_with_depths(cx)
+                .iter()
+                .rev()
+                .map(|(_, entry)| entry)
+                .skip_while(|entry| entry != &&selected_entry)
+                .skip(1)
+                .next()
                 .cloned()
-                .map(EntryOwned::Entry)
-            });
-
-            if let Some(entry_to_select) = entry_to_select {
-                self.selected_entry = Some(entry_to_select);
-                self.autoscroll(cx);
-                cx.notify();
-            }
+        }) {
+            self.selected_entry = Some(entry_to_select);
+            self.autoscroll(cx);
+            cx.notify();
         } else {
-            self.select_first(&SelectFirst {}, cx);
+            self.select_first(&SelectFirst {}, cx)
         }
     }
 
     fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
-        if let Some(selected_entry) = &self.selected_entry {
-            let parent_entry = match selected_entry {
-                EntryOwned::Entry(entry) => self
-                    .fs_entries
-                    .iter()
-                    .rev()
-                    .skip_while(|e| e != &entry)
-                    .skip(1)
-                    .find(|entry_before_current| match (entry, entry_before_current) {
-                        (
-                            FsEntry::File(worktree_id, entry)
-                            | FsEntry::Directory(worktree_id, entry),
-                            FsEntry::Directory(parent_worktree_id, parent_entry),
-                        ) => {
-                            parent_worktree_id == worktree_id
-                                && directory_contains(parent_entry, entry)
+        if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
+            let mut previous_entries = self
+                .entries_with_depths(cx)
+                .iter()
+                .rev()
+                .map(|(_, entry)| entry)
+                .skip_while(|entry| entry != &&selected_entry)
+                .skip(1);
+            match &selected_entry {
+                EntryOwned::Entry(fs_entry) => match fs_entry {
+                    FsEntry::ExternalFile(..) => None,
+                    FsEntry::File(worktree_id, entry, ..)
+                    | FsEntry::Directory(worktree_id, entry) => {
+                        entry.path.parent().and_then(|parent_path| {
+                            previous_entries.find(|entry| match entry {
+                                EntryOwned::Entry(FsEntry::Directory(
+                                    dir_worktree_id,
+                                    dir_entry,
+                                )) => {
+                                    dir_worktree_id == worktree_id
+                                        && dir_entry.path.as_ref() == parent_path
+                                }
+                                EntryOwned::FoldedDirs(dirs_worktree_id, dirs) => {
+                                    dirs_worktree_id == worktree_id
+                                        && dirs
+                                            .first()
+                                            .map_or(false, |dir| dir.path.as_ref() == parent_path)
+                                }
+                                _ => false,
+                            })
+                        })
+                    }
+                },
+                EntryOwned::FoldedDirs(worktree_id, entries) => entries
+                    .first()
+                    .and_then(|entry| entry.path.parent())
+                    .and_then(|parent_path| {
+                        previous_entries.find(|entry| {
+                            if let EntryOwned::Entry(FsEntry::Directory(
+                                dir_worktree_id,
+                                dir_entry,
+                            )) = entry
+                            {
+                                dir_worktree_id == worktree_id
+                                    && dir_entry.path.as_ref() == parent_path
+                            } else {
+                                false
+                            }
+                        })
+                    }),
+                EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) => {
+                    previous_entries.find(|entry| match entry {
+                        EntryOwned::Entry(FsEntry::File(_, _, file_buffer_id, file_excerpts)) => {
+                            file_buffer_id == excerpt_buffer_id
+                                && file_excerpts.contains(&excerpt_id)
+                        }
+                        EntryOwned::Entry(FsEntry::ExternalFile(file_buffer_id, file_excerpts)) => {
+                            file_buffer_id == excerpt_buffer_id
+                                && file_excerpts.contains(&excerpt_id)
                         }
                         _ => false,
-                    }),
-                EntryOwned::FoldedDirs(worktree_id, dirs) => self
-                    .fs_entries
-                    .iter()
-                    .rev()
-                    .skip_while(|e| {
-                        if let FsEntry::Directory(dir_worktree_id, dir_entry) = e {
-                            dir_worktree_id != worktree_id || dirs.first() != Some(dir_entry)
+                    })
+                }
+                EntryOwned::Outline(outline_buffer_id, outline_excerpt_id, _) => previous_entries
+                    .find(|entry| {
+                        if let EntryOwned::Excerpt(excerpt_buffer_id, excerpt_id, _) = entry {
+                            outline_buffer_id == excerpt_buffer_id
+                                && outline_excerpt_id == excerpt_id
                         } else {
-                            true
+                            false
                         }
-                    })
-                    .skip(1)
-                    .find(
-                        |entry_before_current| match (dirs.first(), entry_before_current) {
-                            (Some(entry), FsEntry::Directory(parent_worktree_id, parent_entry)) => {
-                                parent_worktree_id == worktree_id
-                                    && directory_contains(parent_entry, entry)
-                            }
-                            _ => false,
-                        },
-                    ),
-                EntryOwned::Outline(container, _) => self
-                    .fs_entries
-                    .iter()
-                    .find(|entry| entry.outlines_container().as_ref() == Some(container)),
-            }
-            .cloned()
-            .map(EntryOwned::Entry);
-            if let Some(parent_entry) = parent_entry {
-                self.selected_entry = Some(parent_entry);
-                self.autoscroll(cx);
-                cx.notify();
+                    }),
             }
+        }) {
+            self.selected_entry = Some(entry_to_select.clone());
+            self.autoscroll(cx);
+            cx.notify();
         } else {
             self.select_first(&SelectFirst {}, cx);
         }
     }
 
     fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        if let Some(first_entry) = self.fs_entries.first().cloned().map(EntryOwned::Entry) {
-            self.selected_entry = Some(first_entry);
+        if let Some((_, first_entry)) = self.entries_with_depths(cx).iter().next() {
+            self.selected_entry = Some(first_entry.clone());
             self.autoscroll(cx);
             cx.notify();
         }
     }
 
     fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
-        if let Some(new_selection) = self.fs_entries.last().map(|last_entry| {
-            last_entry
-                .outlines_container()
-                .and_then(|container| {
-                    let outline = self.outlines.get(&container)?.last()?;
-                    Some((container, outline.clone()))
-                })
-                .map(|(container, outline)| EntryOwned::Outline(container, outline))
-                .unwrap_or_else(|| EntryOwned::Entry(last_entry.clone()))
-        }) {
-            self.selected_entry = Some(new_selection);
+        if let Some(new_selection) = self
+            .entries_with_depths(cx)
+            .iter()
+            .rev()
+            .map(|(_, entry)| entry)
+            .next()
+        {
+            self.selected_entry = Some(new_selection.clone());
             self.autoscroll(cx);
             cx.notify();
         }
@@ -892,7 +795,7 @@ impl OutlinePanel {
     ) {
         self.selected_entry = Some(entry.to_owned_entry());
         let is_root = match entry {
-            EntryRef::Entry(FsEntry::File(worktree_id, entry))
+            EntryRef::Entry(FsEntry::File(worktree_id, entry, ..))
             | EntryRef::Entry(FsEntry::Directory(worktree_id, entry)) => self
                 .project
                 .read(cx)
@@ -913,7 +816,11 @@ impl OutlinePanel {
                 })
                 .unwrap_or(false),
             EntryRef::Entry(FsEntry::ExternalFile(..)) => false,
-            EntryRef::Outline(_, _) => {
+            EntryRef::Excerpt(..) => {
+                cx.notify();
+                return;
+            }
+            EntryRef::Outline(..) => {
                 cx.notify();
                 return;
             }
@@ -985,8 +892,8 @@ impl OutlinePanel {
             })
             .skip(1)
             .filter(|next_entry| match next_entry {
-                FsEntry::ExternalFile(_) => false,
-                FsEntry::Directory(worktree_id, entry) | FsEntry::File(worktree_id, entry) => {
+                FsEntry::ExternalFile(..) => false,
+                FsEntry::Directory(worktree_id, entry) | FsEntry::File(worktree_id, entry, ..) => {
                     worktree_id == &directory_worktree
                         && entry.path.parent() == Some(directory_entry.path.as_ref())
                 }
@@ -1004,23 +911,32 @@ impl OutlinePanel {
         else {
             return;
         };
-        if let Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry))) =
-            &self.selected_entry
-        {
-            let expanded = self
-                .collapsed_dirs
-                .get_mut(worktree_id)
-                .map_or(false, |hidden_dirs| {
-                    hidden_dirs.remove(&selected_dir_entry.id)
-                });
-            if expanded {
+
+        let entry_to_expand = match &self.selected_entry {
+            Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
+                .last()
+                .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
+            Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry))) => {
+                Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
+            }
+            Some(EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
+                Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
+            }
+            _ => None,
+        };
+        let Some(collapsed_entry) = entry_to_expand else {
+            return;
+        };
+        let expanded = self.collapsed_entries.remove(&collapsed_entry);
+        if expanded {
+            if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
                 self.project.update(cx, |project, cx| {
-                    project.expand_entry(*worktree_id, selected_dir_entry.id, cx);
+                    project.expand_entry(worktree_id, dir_entry_id, cx);
                 });
-                self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
-            } else {
-                self.select_next(&SelectNext, cx)
             }
+            self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+        } else {
+            self.select_next(&SelectNext, cx)
         }
     }
 
@@ -1032,22 +948,54 @@ impl OutlinePanel {
         else {
             return;
         };
-        if let Some(
-            dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
-        ) = &self.selected_entry
-        {
-            self.collapsed_dirs
-                .entry(*worktree_id)
-                .or_default()
-                .insert(selected_dir_entry.id);
-            self.update_fs_entries(
-                &editor,
-                HashSet::default(),
-                Some(dir_entry.clone()),
-                None,
-                false,
-                cx,
-            );
+        match &self.selected_entry {
+            Some(
+                dir_entry @ EntryOwned::Entry(FsEntry::Directory(worktree_id, selected_dir_entry)),
+            ) => {
+                self.collapsed_entries
+                    .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
+                self.update_fs_entries(
+                    &editor,
+                    HashSet::default(),
+                    Some(dir_entry.clone()),
+                    None,
+                    false,
+                    cx,
+                );
+            }
+            Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
+                if let Some(dir_entry) = dir_entries.last() {
+                    if self
+                        .collapsed_entries
+                        .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
+                    {
+                        self.update_fs_entries(
+                            &editor,
+                            HashSet::default(),
+                            Some(dirs_entry.clone()),
+                            None,
+                            false,
+                            cx,
+                        );
+                    }
+                }
+            }
+            Some(excerpt_entry @ EntryOwned::Excerpt(buffer_id, excerpt_id, _)) => {
+                if self
+                    .collapsed_entries
+                    .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
+                {
+                    self.update_fs_entries(
+                        &editor,
+                        HashSet::default(),
+                        Some(excerpt_entry.clone()),
+                        None,
+                        false,
+                        cx,
+                    );
+                }
+            }
+            _ => (),
         }
     }
 
@@ -1060,15 +1008,12 @@ impl OutlinePanel {
             return;
         };
 
-        self.fs_entries_depth
-            .iter()
-            .filter(|(_, &(is_dir, depth))| is_dir && depth == 0)
-            .for_each(|(&(worktree_id, entry_id), _)| {
-                self.collapsed_dirs
-                    .entry(worktree_id)
-                    .or_default()
-                    .insert(entry_id);
-            });
+        self.collapsed_entries.extend(
+            self.fs_entries_depth
+                .iter()
+                .filter(|(_, &(is_dir, depth))| is_dir && depth == 0)
+                .map(|(&(worktree_id, entry_id), _)| CollapsedEntry::Dir(worktree_id, entry_id)),
+        );
         self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
     }
 
@@ -1084,47 +1029,39 @@ impl OutlinePanel {
         match entry {
             EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
                 let entry_id = dir_entry.id;
-                match self.collapsed_dirs.entry(*worktree_id) {
-                    hash_map::Entry::Occupied(mut o) => {
-                        let collapsed_dir_ids = o.get_mut();
-                        if collapsed_dir_ids.remove(&entry_id) {
-                            self.project
-                                .update(cx, |project, cx| {
-                                    project.expand_entry(*worktree_id, entry_id, cx)
-                                })
-                                .unwrap_or_else(|| Task::ready(Ok(())))
-                                .detach_and_log_err(cx);
-                        } else {
-                            collapsed_dir_ids.insert(entry_id);
-                        }
-                    }
-                    hash_map::Entry::Vacant(v) => {
-                        v.insert(BTreeSet::new()).insert(entry_id);
-                    }
+                let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+                if self.collapsed_entries.remove(&collapsed_entry) {
+                    self.project
+                        .update(cx, |project, cx| {
+                            project.expand_entry(*worktree_id, entry_id, cx)
+                        })
+                        .unwrap_or_else(|| Task::ready(Ok(())))
+                        .detach_and_log_err(cx);
+                } else {
+                    self.collapsed_entries.insert(collapsed_entry);
                 }
             }
             EntryOwned::FoldedDirs(worktree_id, dir_entries) => {
                 if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
-                    match self.collapsed_dirs.entry(*worktree_id) {
-                        hash_map::Entry::Occupied(mut o) => {
-                            let collapsed_dir_ids = o.get_mut();
-                            if collapsed_dir_ids.remove(&entry_id) {
-                                self.project
-                                    .update(cx, |project, cx| {
-                                        project.expand_entry(*worktree_id, entry_id, cx)
-                                    })
-                                    .unwrap_or_else(|| Task::ready(Ok(())))
-                                    .detach_and_log_err(cx);
-                            } else {
-                                collapsed_dir_ids.insert(entry_id);
-                            }
-                        }
-                        hash_map::Entry::Vacant(v) => {
-                            v.insert(BTreeSet::new()).insert(entry_id);
-                        }
+                    let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
+                    if self.collapsed_entries.remove(&collapsed_entry) {
+                        self.project
+                            .update(cx, |project, cx| {
+                                project.expand_entry(*worktree_id, entry_id, cx)
+                            })
+                            .unwrap_or_else(|| Task::ready(Ok(())))
+                            .detach_and_log_err(cx);
+                    } else {
+                        self.collapsed_entries.insert(collapsed_entry);
                     }
                 }
             }
+            EntryOwned::Excerpt(buffer_id, excerpt_id, _) => {
+                let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
+                if !self.collapsed_entries.remove(&collapsed_entry) {
+                    self.collapsed_entries.insert(collapsed_entry);
+                }
+            }
             _ => return,
         }
 
@@ -1156,7 +1093,7 @@ impl OutlinePanel {
             .and_then(|entry| match entry {
                 EntryOwned::Entry(entry) => entry.relative_path(&self.project, cx),
                 EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.as_ref()),
-                EntryOwned::Outline(..) => None,
+                EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
             })
             .map(|p| p.to_string_lossy().to_string())
         {
@@ -1197,37 +1134,24 @@ impl OutlinePanel {
         editor: &View<Editor>,
         cx: &mut ViewContext<'_, Self>,
     ) {
-        let Some((container, outline_item)) = self.location_for_editor_selection(editor, cx) else {
+        if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
+            return;
+        }
+        let Some((buffer_id, excerpt_id, outline)) = self.location_for_editor_selection(editor, cx)
+        else {
             return;
         };
-
-        let file_entry_to_expand = self
-            .fs_entries
-            .iter()
-            .find(|entry| match (entry, &container) {
-                (
-                    FsEntry::ExternalFile(buffer_id),
-                    OutlinesContainer::ExternalFile(container_buffer_id),
-                ) => buffer_id == container_buffer_id,
-                (
-                    FsEntry::File(file_worktree_id, file_entry),
-                    OutlinesContainer::File(worktree_id, id),
-                ) => file_worktree_id == worktree_id && &file_entry.id == id,
-                _ => false,
-            });
-        let Some(entry_to_select) = outline_item
-            .map(|outline| EntryOwned::Outline(container, outline))
-            .or_else(|| Some(EntryOwned::Entry(file_entry_to_expand.cloned()?)))
+        let Some((file_entry_with_selection, entry_with_selection)) =
+            self.entry_for_selection(buffer_id, excerpt_id, outline)
         else {
             return;
         };
-
-        if self.selected_entry.as_ref() == Some(&entry_to_select) {
+        if self.selected_entry.as_ref() == Some(&entry_with_selection) {
             return;
         }
 
-        if let Some(FsEntry::File(file_worktree_id, file_entry)) = file_entry_to_expand {
-            if let Some(worktree) = self.project.read(cx).worktree_for_id(*file_worktree_id, cx) {
+        if let FsEntry::File(file_worktree_id, file_entry, ..) = file_entry_with_selection {
+            if let Some(worktree) = self.project.read(cx).worktree_for_id(file_worktree_id, cx) {
                 let parent_entry = {
                     let mut traversal = worktree.read(cx).traverse_from_path(
                         true,