Add a way to filter items in the outline panel (#13984)

Kirill Bulatov created

https://github.com/zed-industries/zed/assets/2690773/145a7cf2-332c-46c9-ab2f-42a77504f54f

Adds a way to filter entries in the outline panel, by showing all
entries (even if their parents were collapsed) that fuzzy match a given
query.

Release Notes:

- Added a way to filter items in the outline panel

Change summary

Cargo.lock                                |   1 
assets/keymaps/default-linux.json         |   1 
assets/keymaps/default-macos.json         |   1 
crates/language/src/outline.rs            |  10 
crates/outline/src/outline.rs             |  13 
crates/outline_panel/Cargo.toml           |   1 
crates/outline_panel/src/outline_panel.rs | 789 +++++++++++-------------
7 files changed, 385 insertions(+), 431 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7355,6 +7355,7 @@ dependencies = [
  "db",
  "editor",
  "file_icons",
+ "fuzzy",
  "gpui",
  "itertools 0.11.0",
  "language",

assets/keymaps/default-linux.json 🔗

@@ -502,6 +502,7 @@
   {
     "context": "OutlinePanel",
     "bindings": {
+      "escape": "menu::Cancel",
       "left": "outline_panel::CollapseSelectedEntry",
       "right": "outline_panel::ExpandSelectedEntry",
       "ctrl-alt-c": "outline_panel::CopyPath",

assets/keymaps/default-macos.json 🔗

@@ -523,6 +523,7 @@
   {
     "context": "OutlinePanel",
     "bindings": {
+      "escape": "menu::Cancel",
       "left": "outline_panel::CollapseSelectedEntry",
       "right": "outline_panel::ExpandSelectedEntry",
       "cmd-alt-c": "outline_panel::CopyPath",

crates/language/src/outline.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
 };
 use settings::Settings;
 use std::ops::Range;
-use theme::{ActiveTheme, ThemeSettings};
+use theme::{color_alpha, ActiveTheme, ThemeSettings};
 
 /// An outline of all the symbols contained in a buffer.
 #[derive(Debug)]
@@ -146,9 +146,15 @@ impl<T> Outline<T> {
 
 pub fn render_item<T>(
     outline_item: &OutlineItem<T>,
-    custom_highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+    match_ranges: impl IntoIterator<Item = Range<usize>>,
     cx: &AppContext,
 ) -> StyledText {
+    let mut highlight_style = HighlightStyle::default();
+    highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
+    let custom_highlights = match_ranges
+        .into_iter()
+        .map(|range| (range, highlight_style));
+
     let settings = ThemeSettings::get_global(cx);
 
     // TODO: We probably shouldn't need to build a whole new text style here

crates/outline/src/outline.rs 🔗

@@ -3,9 +3,8 @@ use editor::{
 };
 use fuzzy::StringMatch;
 use gpui::{
-    div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle,
-    ParentElement, Point, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, ParentElement,
+    Point, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
@@ -15,7 +14,7 @@ use std::{
     sync::Arc,
 };
 
-use theme::{color_alpha, ActiveTheme};
+use theme::ActiveTheme;
 use ui::{prelude::*, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{DismissDecision, ModalView};
@@ -272,10 +271,6 @@ impl PickerDelegate for OutlineViewDelegate {
         let mat = self.matches.get(ix)?;
         let outline_item = self.outline.items.get(mat.candidate_id)?;
 
-        let mut highlight_style = HighlightStyle::default();
-        highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
-        let custom_highlights = mat.ranges().map(|range| (range, highlight_style));
-
         Some(
             ListItem::new(ix)
                 .inset(true)
@@ -285,7 +280,7 @@ impl PickerDelegate for OutlineViewDelegate {
                     div()
                         .text_ui(cx)
                         .pl(rems(outline_item.depth as f32))
-                        .child(language::render_item(outline_item, custom_highlights, cx)),
+                        .child(language::render_item(outline_item, mat.ranges(), cx)),
                 ),
         )
     }

crates/outline_panel/Cargo.toml 🔗

@@ -18,6 +18,7 @@ collections.workspace = true
 db.workspace = true
 editor.workspace = true
 file_icons.workspace = true
+fuzzy.workspace = true
 itertools.workspace = true
 gpui.workspace = true
 language.workspace = true

crates/outline_panel/src/outline_panel.rs 🔗

@@ -4,7 +4,7 @@ use std::{
     cmp,
     ops::Range,
     path::{Path, PathBuf},
-    sync::Arc,
+    sync::{atomic::AtomicBool, Arc},
     time::Duration,
 };
 
@@ -18,6 +18,7 @@ use editor::{
     DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange,
 };
 use file_icons::FileIcons;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
     actions, anchored, deferred, div, px, uniform_list, Action, AnyElement, AppContext,
     AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId, EntityId,
@@ -28,7 +29,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
-use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
+use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev};
 
 use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
 use project::{File, Fs, Item, Project};
@@ -39,8 +40,9 @@ use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
     item::ItemHandle,
     ui::{
-        h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, Icon, IconName, IconSize,
-        Label, LabelCommon, ListItem, Selectable, Spacing, StyledTypography,
+        h_flex, v_flex, ActiveTheme, Color, ContextMenu, FluentBuilder, HighlightedLabel, Icon,
+        IconName, IconSize, Label, LabelCommon, ListItem, Selectable, Spacing, StyledExt,
+        StyledTypography,
     },
     OpenInTerminal, Workspace,
 };
@@ -51,6 +53,7 @@ actions!(
     [
         ExpandSelectedEntry,
         CollapseSelectedEntry,
+        ExpandAllEntries,
         CollapseAllEntries,
         CopyPath,
         CopyRelativePath,
@@ -64,7 +67,7 @@ actions!(
 );
 
 const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
-const UPDATE_DEBOUNCE_MILLIS: u64 = 80;
+const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 
 type Outline = OutlineItem<language::Anchor>;
 
@@ -79,17 +82,38 @@ pub struct OutlinePanel {
     pending_serialization: Task<Option<()>>,
     fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>,
     fs_entries: Vec<FsEntry>,
+    fs_children_count: HashMap<WorktreeId, HashMap<Arc<Path>, FsChildren>>,
     collapsed_entries: HashSet<CollapsedEntry>,
     unfolded_dirs: HashMap<WorktreeId, BTreeSet<ProjectEntryId>>,
-    last_visible_range: Range<usize>,
     selected_entry: Option<EntryOwned>,
     active_item: Option<ActiveItem>,
     _subscriptions: Vec<Subscription>,
-    loading_outlines: bool,
-    update_task: Task<()>,
+    updating_fs_entries: bool,
+    fs_entries_update_task: Task<()>,
+    cached_entries_update_task: Task<()>,
     outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>,
     excerpts: HashMap<BufferId, HashMap<ExcerptId, Excerpt>>,
-    cached_entries_with_depth: Option<Vec<(usize, EntryOwned)>>,
+    cached_entries_with_depth: Vec<CachedEntry>,
+    filter_editor: View<Editor>,
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+struct FsChildren {
+    files: usize,
+    dirs: usize,
+}
+
+impl FsChildren {
+    fn may_be_fold_part(&self) -> bool {
+        self.dirs == 0 || (self.dirs == 1 && self.files == 0)
+    }
+}
+
+#[derive(Clone, Debug)]
+struct CachedEntry {
+    depth: usize,
+    string_match: Option<StringMatch>,
+    entry: EntryOwned,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -274,6 +298,18 @@ impl OutlinePanel {
     fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         let project = workspace.project().clone();
         let outline_panel = cx.new_view(|cx| {
+            let filter_editor = cx.new_view(|cx| {
+                let mut editor = Editor::single_line(cx);
+                editor.set_placeholder_text("Filter...", cx);
+                editor
+            });
+            let filter_update_subscription =
+                cx.subscribe(&filter_editor, |outline_panel: &mut Self, _, event, cx| {
+                    if let editor::EditorEvent::BufferEdited = event {
+                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
+                    }
+                });
+
             let focus_handle = cx.focus_handle();
             let focus_subscription = cx.on_focus(&focus_handle, Self::focus_in);
             let workspace_subscription = cx.subscribe(
@@ -298,7 +334,7 @@ impl OutlinePanel {
                                 outline_panel.replace_visible_entries(new_active_editor, cx);
                             }
                         } else {
-                            outline_panel.clear_previous();
+                            outline_panel.clear_previous(cx);
                             cx.notify();
                         }
                     }
@@ -324,8 +360,10 @@ impl OutlinePanel {
                 fs: workspace.app_state().fs.clone(),
                 scroll_handle: UniformListScrollHandle::new(),
                 focus_handle,
+                filter_editor,
                 fs_entries: Vec::new(),
                 fs_entries_depth: HashMap::default(),
+                fs_children_count: HashMap::default(),
                 collapsed_entries: HashSet::default(),
                 unfolded_dirs: HashMap::default(),
                 selected_entry: None,
@@ -333,17 +371,18 @@ impl OutlinePanel {
                 width: None,
                 active_item: None,
                 pending_serialization: Task::ready(None),
-                loading_outlines: false,
-                update_task: Task::ready(()),
+                updating_fs_entries: false,
+                fs_entries_update_task: Task::ready(()),
+                cached_entries_update_task: Task::ready(()),
                 outline_fetch_tasks: HashMap::default(),
                 excerpts: HashMap::default(),
-                last_visible_range: 0..0,
-                cached_entries_with_depth: None,
+                cached_entries_with_depth: Vec::new(),
                 _subscriptions: vec![
                     settings_subscription,
                     icons_subscription,
                     focus_subscription,
                     workspace_subscription,
+                    filter_update_subscription,
                 ],
             };
             if let Some(editor) = workspace
@@ -383,31 +422,16 @@ impl OutlinePanel {
     }
 
     fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
-        let Some(editor) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.active_editor.upgrade())
-        else {
-            return;
-        };
         if let Some(EntryOwned::FoldedDirs(worktree_id, entries)) = &self.selected_entry {
             self.unfolded_dirs
                 .entry(*worktree_id)
                 .or_default()
                 .extend(entries.iter().map(|entry| entry.id));
-            self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+            self.update_cached_entries(None, cx);
         }
     }
 
     fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
-        let Some(editor) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.active_editor.upgrade())
-        else {
-            return;
-        };
-
         let (worktree_id, entry) = match &self.selected_entry {
             Some(EntryOwned::Entry(FsEntry::Directory(worktree_id, entry))) => {
                 (worktree_id, Some(entry))
@@ -424,38 +448,12 @@ impl OutlinePanel {
             .read(cx)
             .worktree_for_id(*worktree_id, cx)
             .map(|w| w.read(cx).snapshot());
-        let Some((worktree, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
+        let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else {
             return;
         };
 
         unfolded_dirs.remove(&entry.id);
-        let mut parent = entry.path.parent();
-        while let Some(parent_path) = parent {
-            let removed = worktree.entry_for_path(parent_path).map_or(false, |entry| {
-                if worktree.root_entry().map(|entry| entry.id) == Some(entry.id) {
-                    false
-                } else {
-                    unfolded_dirs.remove(&entry.id)
-                }
-            });
-
-            if removed {
-                parent = parent_path.parent();
-            } else {
-                break;
-            }
-        }
-        for child_dir in worktree
-            .child_entries(&entry.path)
-            .filter(|entry| entry.is_dir())
-        {
-            let removed = unfolded_dirs.remove(&child_dir.id);
-            if !removed {
-                break;
-            }
-        }
-
-        self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+        self.update_cached_entries(None, cx);
     }
 
     fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
@@ -464,6 +462,23 @@ impl OutlinePanel {
         }
     }
 
+    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        if self.filter_editor.focus_handle(cx).is_focused(cx) {
+            self.filter_editor.update(cx, |editor, cx| {
+                if editor.buffer().read(cx).len(cx) > 0 {
+                    editor.set_text("", cx);
+                }
+            });
+        } else {
+            cx.focus_view(&self.filter_editor);
+        }
+
+        if self.context_menu.is_some() {
+            self.context_menu.take();
+            cx.notify();
+        }
+    }
+
     fn open_entry(&mut self, entry: &EntryOwned, cx: &mut ViewContext<OutlinePanel>) {
         let Some(active_editor) = self
             .active_item
@@ -578,9 +593,9 @@ impl OutlinePanel {
 
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
-            self.entries_with_depths(cx)
+            self.cached_entries_with_depth
                 .iter()
-                .map(|(_, entry)| entry)
+                .map(|cached_entry| &cached_entry.entry)
                 .skip_while(|entry| entry != &&selected_entry)
                 .skip(1)
                 .next()
@@ -596,10 +611,10 @@ impl OutlinePanel {
 
     fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
         if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
-            self.entries_with_depths(cx)
+            self.cached_entries_with_depth
                 .iter()
                 .rev()
-                .map(|(_, entry)| entry)
+                .map(|cached_entry| &cached_entry.entry)
                 .skip_while(|entry| entry != &&selected_entry)
                 .skip(1)
                 .next()
@@ -616,10 +631,10 @@ impl OutlinePanel {
     fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
         if let Some(entry_to_select) = self.selected_entry.clone().and_then(|selected_entry| {
             let mut previous_entries = self
-                .entries_with_depths(cx)
+                .cached_entries_with_depth
                 .iter()
                 .rev()
-                .map(|(_, entry)| entry)
+                .map(|cached_entry| &cached_entry.entry)
                 .skip_while(|entry| entry != &&selected_entry)
                 .skip(1);
             match &selected_entry {
@@ -697,8 +712,8 @@ impl OutlinePanel {
     }
 
     fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        if let Some((_, first_entry)) = self.entries_with_depths(cx).iter().next() {
-            self.selected_entry = Some(first_entry.clone());
+        if let Some(first_entry) = self.cached_entries_with_depth.iter().next() {
+            self.selected_entry = Some(first_entry.entry.clone());
             self.autoscroll(cx);
             cx.notify();
         }
@@ -706,10 +721,10 @@ impl OutlinePanel {
 
     fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
         if let Some(new_selection) = self
-            .entries_with_depths(cx)
+            .cached_entries_with_depth
             .iter()
             .rev()
-            .map(|(_, entry)| entry)
+            .map(|cached_entry| &cached_entry.entry)
             .next()
         {
             self.selected_entry = Some(new_selection.clone());
@@ -721,9 +736,9 @@ impl OutlinePanel {
     fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(selected_entry) = self.selected_entry.clone() {
             let index = self
-                .entries_with_depths(cx)
+                .cached_entries_with_depth
                 .iter()
-                .position(|(_, entry)| entry == &selected_entry);
+                .position(|cached_entry| cached_entry.entry == selected_entry);
             if let Some(index) = index {
                 self.scroll_handle.scroll_to_item(index);
                 cx.notify();
@@ -811,9 +826,6 @@ impl OutlinePanel {
             EntryRef::Entry(FsEntry::Directory(directory_worktree, directory_entry)) => {
                 (*directory_worktree, Some(directory_entry))
             }
-            EntryRef::FoldedDirs(directory_worktree, entries) => {
-                (directory_worktree, entries.last())
-            }
             _ => return false,
         };
         let Some(directory_entry) = directory_entry else {
@@ -823,45 +835,24 @@ impl OutlinePanel {
         if self
             .unfolded_dirs
             .get(&directory_worktree)
-            .map_or(false, |unfolded_dirs| {
-                unfolded_dirs.contains(&directory_entry.id)
+            .map_or(true, |unfolded_dirs| {
+                !unfolded_dirs.contains(&directory_entry.id)
             })
         {
-            return true;
+            return false;
         }
 
-        let child_entries = self
-            .fs_entries
-            .iter()
-            .skip_while(|entry| {
-                if let FsEntry::Directory(worktree_id, entry) = entry {
-                    worktree_id != &directory_worktree || entry.id != directory_entry.id
-                } else {
-                    true
-                }
-            })
-            .skip(1)
-            .filter(|next_entry| match next_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())
-                }
-            })
-            .collect::<Vec<_>>();
+        let children = self
+            .fs_children_count
+            .get(&directory_worktree)
+            .and_then(|entries| entries.get(&directory_entry.path))
+            .copied()
+            .unwrap_or_default();
 
-        child_entries.len() == 1 && matches!(child_entries.first(), Some(FsEntry::Directory(..)))
+        children.may_be_fold_part() && children.dirs > 0
     }
 
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
-        let Some(editor) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.active_editor.upgrade())
-        else {
-            return;
-        };
-
         let entry_to_expand = match &self.selected_entry {
             Some(EntryOwned::FoldedDirs(worktree_id, dir_entries)) => dir_entries
                 .last()
@@ -890,58 +881,33 @@ impl OutlinePanel {
                     project.expand_entry(worktree_id, dir_entry_id, cx);
                 });
             }
-            self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+            self.update_cached_entries(None, cx);
         } else {
             self.select_next(&SelectNext, cx)
         }
     }
 
     fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
-        let Some(editor) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.active_editor.upgrade())
-        else {
-            return;
-        };
         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,
-                );
+                self.selected_entry = Some(dir_entry.clone());
+                self.update_cached_entries(None, cx);
             }
             Some(file_entry @ EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _))) => {
                 self.collapsed_entries
                     .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
-                self.update_fs_entries(
-                    &editor,
-                    HashSet::default(),
-                    Some(file_entry.clone()),
-                    None,
-                    false,
-                    cx,
-                );
+                self.selected_entry = Some(file_entry.clone());
+                self.update_cached_entries(None, cx);
             }
             Some(file_entry @ EntryOwned::Entry(FsEntry::ExternalFile(buffer_id, _))) => {
                 self.collapsed_entries
                     .insert(CollapsedEntry::ExternalFile(*buffer_id));
-                self.update_fs_entries(
-                    &editor,
-                    HashSet::default(),
-                    Some(file_entry.clone()),
-                    None,
-                    false,
-                    cx,
-                );
+                self.selected_entry = Some(file_entry.clone());
+                self.update_cached_entries(None, cx);
             }
             Some(dirs_entry @ EntryOwned::FoldedDirs(worktree_id, dir_entries)) => {
                 if let Some(dir_entry) = dir_entries.last() {
@@ -949,14 +915,8 @@ impl OutlinePanel {
                         .collapsed_entries
                         .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
                     {
-                        self.update_fs_entries(
-                            &editor,
-                            HashSet::default(),
-                            Some(dirs_entry.clone()),
-                            None,
-                            false,
-                            cx,
-                        );
+                        self.selected_entry = Some(dirs_entry.clone());
+                        self.update_cached_entries(None, cx);
                     }
                 }
             }
@@ -965,33 +925,56 @@ impl OutlinePanel {
                     .collapsed_entries
                     .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
                 {
-                    self.update_fs_entries(
-                        &editor,
-                        HashSet::default(),
-                        Some(excerpt_entry.clone()),
-                        None,
-                        false,
-                        cx,
-                    );
+                    self.selected_entry = Some(excerpt_entry.clone());
+                    self.update_cached_entries(None, cx);
                 }
             }
             None | Some(EntryOwned::Outline(..)) => {}
         }
     }
 
-    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
-        let Some(editor) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.active_editor.upgrade())
-        else {
-            return;
-        };
+    pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
+        let expanded_entries =
+            self.fs_entries
+                .iter()
+                .fold(HashSet::default(), |mut entries, fs_entry| {
+                    match fs_entry {
+                        FsEntry::ExternalFile(buffer_id, _) => {
+                            entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
+                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
+                                |excerpts| {
+                                    excerpts.iter().map(|(excerpt_id, _)| {
+                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
+                                    })
+                                },
+                            ));
+                        }
+                        FsEntry::Directory(worktree_id, entry) => {
+                            entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
+                        }
+                        FsEntry::File(worktree_id, _, buffer_id, _) => {
+                            entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
+                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
+                                |excerpts| {
+                                    excerpts.iter().map(|(excerpt_id, _)| {
+                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
+                                    })
+                                },
+                            ));
+                        }
+                    }
+                    entries
+                });
+        self.collapsed_entries
+            .retain(|entry| !expanded_entries.contains(entry));
+        self.update_cached_entries(None, cx);
+    }
 
+    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
         let new_entries = self
-            .entries_with_depths(cx)
+            .cached_entries_with_depth
             .iter()
-            .flat_map(|(_, entry)| match entry {
+            .flat_map(|cached_entry| match &cached_entry.entry {
                 EntryOwned::Entry(FsEntry::Directory(worktree_id, entry)) => {
                     Some(CollapsedEntry::Dir(*worktree_id, entry.id))
                 }
@@ -1011,18 +994,10 @@ impl OutlinePanel {
             })
             .collect::<Vec<_>>();
         self.collapsed_entries.extend(new_entries);
-        self.update_fs_entries(&editor, HashSet::default(), None, None, false, cx);
+        self.update_cached_entries(None, cx);
     }
 
     fn toggle_expanded(&mut self, entry: &EntryOwned, cx: &mut ViewContext<Self>) {
-        let Some(editor) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.active_editor.upgrade())
-        else {
-            return;
-        };
-
         match entry {
             EntryOwned::Entry(FsEntry::Directory(worktree_id, dir_entry)) => {
                 let entry_id = dir_entry.id;
@@ -1074,14 +1049,8 @@ impl OutlinePanel {
             EntryOwned::Outline(..) => return,
         }
 
-        self.update_fs_entries(
-            &editor,
-            HashSet::default(),
-            Some(entry.clone()),
-            None,
-            false,
-            cx,
-        );
+        self.selected_entry = Some(entry.clone());
+        self.update_cached_entries(None, cx);
     }
 
     fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
@@ -1101,9 +1070,7 @@ impl OutlinePanel {
             .as_ref()
             .and_then(|entry| match entry {
                 EntryOwned::Entry(entry) => self.relative_path(&entry, cx),
-                EntryOwned::FoldedDirs(_, dirs) => {
-                    dirs.last().map(|entry| entry.path.to_path_buf())
-                }
+                EntryOwned::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()),
                 EntryOwned::Excerpt(..) | EntryOwned::Outline(..) => None,
             })
             .map(|p| p.to_string_lossy().to_string())
@@ -1233,14 +1200,8 @@ impl OutlinePanel {
             }
         }
 
-        self.update_fs_entries(
-            &editor,
-            HashSet::default(),
-            Some(entry_with_selection),
-            None,
-            false,
-            cx,
-        );
+        self.selected_entry = Some(entry_with_selection);
+        self.update_cached_entries(None, cx);
     }
 
     fn render_excerpt(
@@ -1307,6 +1268,7 @@ impl OutlinePanel {
         excerpt_id: ExcerptId,
         rendered_outline: &Outline,
         depth: usize,
+        string_match: Option<&StringMatch>,
         cx: &mut ViewContext<Self>,
     ) -> Stateful<Div> {
         let (item_id, label_element) = (
@@ -1314,7 +1276,14 @@ impl OutlinePanel {
                 "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
                 rendered_outline.range, &rendered_outline.text,
             ))),
-            language::render_item(&rendered_outline, None, cx).into_any_element(),
+            language::render_item(
+                &rendered_outline,
+                string_match
+                    .map(|string_match| string_match.ranges().collect::<Vec<_>>())
+                    .unwrap_or_default(),
+                cx,
+            )
+            .into_any_element(),
         );
         let is_active = match &self.selected_entry {
             Some(EntryOwned::Outline(selected_buffer_id, selected_excerpt_id, selected_entry)) => {
@@ -1344,6 +1313,7 @@ impl OutlinePanel {
         &self,
         rendered_entry: &FsEntry,
         depth: usize,
+        string_match: Option<&StringMatch>,
         cx: &mut ViewContext<Self>,
     ) -> Stateful<Div> {
         let settings = OutlinePanelSettings::get_global(cx);
@@ -1364,10 +1334,14 @@ impl OutlinePanel {
                 };
                 (
                     ElementId::from(entry.id.to_proto() as usize),
-                    Label::new(name)
-                        .single_line()
-                        .color(color)
-                        .into_any_element(),
+                    HighlightedLabel::new(
+                        name,
+                        string_match
+                            .map(|string_match| string_match.positions.clone())
+                            .unwrap_or_default(),
+                    )
+                    .color(color)
+                    .into_any_element(),
                     icon.unwrap_or_else(empty_icon),
                 )
             }
@@ -1388,10 +1362,14 @@ impl OutlinePanel {
                 .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(),
+                    HighlightedLabel::new(
+                        name,
+                        string_match
+                            .map(|string_match| string_match.positions.clone())
+                            .unwrap_or_default(),
+                    )
+                    .color(color)
+                    .into_any_element(),
                     icon.unwrap_or_else(empty_icon),
                 )
             }
@@ -1416,10 +1394,14 @@ impl OutlinePanel {
                 };
                 (
                     ElementId::from(buffer_id.to_proto() as usize),
-                    Label::new(name)
-                        .single_line()
-                        .color(color)
-                        .into_any_element(),
+                    HighlightedLabel::new(
+                        name,
+                        string_match
+                            .map(|string_match| string_match.positions.clone())
+                            .unwrap_or_default(),
+                    )
+                    .color(color)
+                    .into_any_element(),
                     icon.unwrap_or_else(empty_icon),
                 )
             }
@@ -1441,6 +1423,7 @@ impl OutlinePanel {
         worktree_id: WorktreeId,
         dir_entries: &[Entry],
         depth: usize,
+        string_match: Option<&StringMatch>,
         cx: &mut ViewContext<OutlinePanel>,
     ) -> Stateful<Div> {
         let settings = OutlinePanelSettings::get_global(cx);
@@ -1451,13 +1434,7 @@ impl OutlinePanel {
             _ => false,
         };
         let (item_id, label_element, icon) = {
-            let name = dir_entries.iter().fold(String::new(), |mut name, entry| {
-                if !name.is_empty() {
-                    name.push(std::path::MAIN_SEPARATOR)
-                }
-                name.push_str(&self.entry_name(&worktree_id, entry, cx));
-                name
-            });
+            let name = self.dir_names_string(dir_entries, worktree_id, cx);
 
             let is_expanded = dir_entries.iter().all(|dir| {
                 !self
@@ -1481,10 +1458,14 @@ impl OutlinePanel {
                         .map(|entry| entry.id.to_proto())
                         .unwrap_or_else(|| worktree_id.to_proto()) as usize,
                 ),
-                Label::new(name)
-                    .single_line()
-                    .color(color)
-                    .into_any_element(),
+                HighlightedLabel::new(
+                    name,
+                    string_match
+                        .map(|string_match| string_match.positions.clone())
+                        .unwrap_or_default(),
+                )
+                .color(color)
+                .into_any_element(),
                 icon.unwrap_or_else(empty_icon),
             )
         };
@@ -1563,12 +1544,7 @@ impl OutlinePanel {
             })
     }
 
-    fn entry_name(
-        &self,
-        worktree_id: &WorktreeId,
-        entry: &Entry,
-        cx: &ViewContext<OutlinePanel>,
-    ) -> String {
+    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String {
         let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
             Some(worktree) => {
                 let worktree = worktree.read(cx);
@@ -1600,7 +1576,6 @@ impl OutlinePanel {
         new_entries: HashSet<ExcerptId>,
         new_selected_entry: Option<EntryOwned>,
         debounce: Option<Duration>,
-        prefetch: bool,
         cx: &mut ViewContext<Self>,
     ) {
         if !self.active {
@@ -1658,237 +1633,211 @@ impl OutlinePanel {
             },
         );
 
-        self.loading_outlines = true;
-        self.update_task = cx.spawn(|outline_panel, mut cx| async move {
+        self.updating_fs_entries = true;
+        self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
             if let Some(debounce) = debounce {
                 cx.background_executor().timer(debounce).await;
             }
-            let Some((new_collapsed_entries, new_unfolded_dirs, new_fs_entries, new_depth_map)) =
-                cx.background_executor()
-                    .spawn(async move {
-                        let mut processed_external_buffers = HashSet::default();
-                        let mut new_worktree_entries =
-                            HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
-                        let mut worktree_excerpts = HashMap::<
-                            WorktreeId,
-                            HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
-                        >::default();
-                        let mut external_excerpts = HashMap::default();
-
-                        for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
-                            if is_new {
-                                match &worktree {
-                                    Some(worktree) => {
-                                        new_collapsed_entries
-                                            .insert(CollapsedEntry::File(worktree.id(), buffer_id));
-                                    }
-                                    None => {
-                                        new_collapsed_entries
-                                            .insert(CollapsedEntry::ExternalFile(buffer_id));
-                                    }
+            let Some((
+                new_collapsed_entries,
+                new_unfolded_dirs,
+                new_fs_entries,
+                new_depth_map,
+                new_children_count,
+            )) = cx
+                .background_executor()
+                .spawn(async move {
+                    let mut processed_external_buffers = HashSet::default();
+                    let mut new_worktree_entries =
+                        HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
+                    let mut worktree_excerpts = HashMap::<
+                        WorktreeId,
+                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
+                    >::default();
+                    let mut external_excerpts = HashMap::default();
+
+                    for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
+                        if is_new {
+                            match &worktree {
+                                Some(worktree) => {
+                                    new_collapsed_entries
+                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
                                 }
-
-                                for excerpt_id in &excerpts {
+                                None => {
                                     new_collapsed_entries
-                                        .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
+                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
                                 }
                             }
 
-                            if let Some(worktree) = worktree {
-                                let worktree_id = worktree.id();
-                                let unfolded_dirs =
-                                    new_unfolded_dirs.entry(worktree_id).or_default();
-
-                                match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
-                                    Some(entry) => {
-                                        let mut traversal = worktree.traverse_from_path(
-                                            true,
-                                            true,
-                                            true,
-                                            entry.path.as_ref(),
-                                        );
-
-                                        let mut entries_to_add = HashSet::default();
-                                        worktree_excerpts
-                                            .entry(worktree_id)
-                                            .or_default()
-                                            .insert(entry.id, (buffer_id, excerpts));
-                                        let mut current_entry = entry;
-                                        loop {
-                                            if current_entry.is_dir() {
-                                                let is_root =
-                                                    worktree.root_entry().map(|entry| entry.id)
-                                                        == Some(current_entry.id);
-                                                if is_root {
-                                                    root_entries.insert(current_entry.id);
-                                                    if auto_fold_dirs {
-                                                        unfolded_dirs.insert(current_entry.id);
-                                                    }
-                                                }
+                            for excerpt_id in &excerpts {
+                                new_collapsed_entries
+                                    .insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
+                            }
+                        }
 
-                                                if is_new {
-                                                    new_collapsed_entries.remove(
-                                                        &CollapsedEntry::Dir(
-                                                            worktree_id,
-                                                            current_entry.id,
-                                                        ),
-                                                    );
-                                                } else if new_collapsed_entries.contains(
-                                                    &CollapsedEntry::Dir(
-                                                        worktree_id,
-                                                        current_entry.id,
-                                                    ),
-                                                ) {
-                                                    entries_to_add.clear();
+                        if let Some(worktree) = worktree {
+                            let worktree_id = worktree.id();
+                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
+
+                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
+                                Some(entry) => {
+                                    let mut traversal = worktree.traverse_from_path(
+                                        true,
+                                        true,
+                                        true,
+                                        entry.path.as_ref(),
+                                    );
+
+                                    let mut entries_to_add = HashSet::default();
+                                    worktree_excerpts
+                                        .entry(worktree_id)
+                                        .or_default()
+                                        .insert(entry.id, (buffer_id, excerpts));
+                                    let mut current_entry = entry;
+                                    loop {
+                                        if current_entry.is_dir() {
+                                            let is_root =
+                                                worktree.root_entry().map(|entry| entry.id)
+                                                    == Some(current_entry.id);
+                                            if is_root {
+                                                root_entries.insert(current_entry.id);
+                                                if auto_fold_dirs {
+                                                    unfolded_dirs.insert(current_entry.id);
                                                 }
                                             }
+                                            if is_new {
+                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
+                                                    worktree_id,
+                                                    current_entry.id,
+                                                ));
+                                            }
+                                        }
 
-                                            let new_entry_added =
-                                                entries_to_add.insert(current_entry);
-                                            if new_entry_added && traversal.back_to_parent() {
-                                                if let Some(parent_entry) = traversal.entry() {
-                                                    current_entry = parent_entry.clone();
-                                                    continue;
-                                                }
+                                        let new_entry_added = entries_to_add.insert(current_entry);
+                                        if new_entry_added && traversal.back_to_parent() {
+                                            if let Some(parent_entry) = traversal.entry() {
+                                                current_entry = parent_entry.clone();
+                                                continue;
                                             }
-                                            break;
                                         }
-                                        new_worktree_entries
-                                            .entry(worktree_id)
-                                            .or_insert_with(|| {
-                                                (worktree.clone(), HashSet::default())
-                                            })
-                                            .1
-                                            .extend(entries_to_add);
+                                        break;
                                     }
-                                    None => {
-                                        if processed_external_buffers.insert(buffer_id) {
-                                            external_excerpts
-                                                .entry(buffer_id)
-                                                .or_insert_with(|| Vec::new())
-                                                .extend(excerpts);
-                                        }
+                                    new_worktree_entries
+                                        .entry(worktree_id)
+                                        .or_insert_with(|| (worktree.clone(), HashSet::default()))
+                                        .1
+                                        .extend(entries_to_add);
+                                }
+                                None => {
+                                    if processed_external_buffers.insert(buffer_id) {
+                                        external_excerpts
+                                            .entry(buffer_id)
+                                            .or_insert_with(|| Vec::new())
+                                            .extend(excerpts);
                                     }
                                 }
-                            } else if processed_external_buffers.insert(buffer_id) {
-                                external_excerpts
-                                    .entry(buffer_id)
-                                    .or_insert_with(|| Vec::new())
-                                    .extend(excerpts);
                             }
+                        } else if processed_external_buffers.insert(buffer_id) {
+                            external_excerpts
+                                .entry(buffer_id)
+                                .or_insert_with(|| Vec::new())
+                                .extend(excerpts);
                         }
+                    }
 
-                        #[derive(Clone, Copy, Default)]
-                        struct Children {
-                            files: usize,
-                            dirs: usize,
-                        }
-                        let mut children_count =
-                            HashMap::<WorktreeId, HashMap<PathBuf, Children>>::default();
-
-                        let worktree_entries = new_worktree_entries
-                            .into_iter()
-                            .map(|(worktree_id, (worktree_snapshot, entries))| {
-                                let mut entries = entries.into_iter().collect::<Vec<_>>();
-                                // For a proper git status propagation, we have to keep the entries sorted lexicographically.
-                                entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
-                                worktree_snapshot.propagate_git_statuses(&mut entries);
-                                project::sort_worktree_entries(&mut entries);
-                                (worktree_id, entries)
-                            })
-                            .flat_map(|(worktree_id, entries)| {
-                                {
-                                    entries
-                                        .into_iter()
-                                        .filter_map(|entry| {
-                                            if auto_fold_dirs {
-                                                if let Some(parent) = entry.path.parent() {
-                                                    let children = children_count
-                                                        .entry(worktree_id)
-                                                        .or_default()
-                                                        .entry(parent.to_path_buf())
-                                                        .or_default();
-                                                    if entry.is_dir() {
-                                                        children.dirs += 1;
-                                                    } else {
-                                                        children.files += 1;
-                                                    }
+                    let mut new_children_count =
+                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
+
+                    let worktree_entries = new_worktree_entries
+                        .into_iter()
+                        .map(|(worktree_id, (worktree_snapshot, entries))| {
+                            let mut entries = entries.into_iter().collect::<Vec<_>>();
+                            // For a proper git status propagation, we have to keep the entries sorted lexicographically.
+                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
+                            worktree_snapshot.propagate_git_statuses(&mut entries);
+                            project::sort_worktree_entries(&mut entries);
+                            (worktree_id, entries)
+                        })
+                        .flat_map(|(worktree_id, entries)| {
+                            {
+                                entries
+                                    .into_iter()
+                                    .filter_map(|entry| {
+                                        if auto_fold_dirs {
+                                            if let Some(parent) = entry.path.parent() {
+                                                let children = new_children_count
+                                                    .entry(worktree_id)
+                                                    .or_default()
+                                                    .entry(Arc::from(parent))
+                                                    .or_default();
+                                                if entry.is_dir() {
+                                                    children.dirs += 1;
+                                                } else {
+                                                    children.files += 1;
                                                 }
                                             }
+                                        }
 
-                                            if entry.is_dir() {
-                                                Some(FsEntry::Directory(worktree_id, entry))
-                                            } else {
-                                                let (buffer_id, excerpts) = worktree_excerpts
-                                                    .get_mut(&worktree_id)
-                                                    .and_then(|worktree_excerpts| {
-                                                        worktree_excerpts.remove(&entry.id)
-                                                    })?;
-                                                Some(FsEntry::File(
-                                                    worktree_id,
-                                                    entry,
-                                                    buffer_id,
-                                                    excerpts,
-                                                ))
-                                            }
-                                        })
-                                        .collect::<Vec<_>>()
-                                }
-                            })
-                            .collect::<Vec<_>>();
-
-                        let mut visited_dirs = Vec::new();
-                        let mut new_depth_map = HashMap::default();
-                        let new_visible_entries = external_excerpts
-                            .into_iter()
-                            .sorted_by_key(|(id, _)| *id)
-                            .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
-                            .chain(worktree_entries)
-                            .filter(|visible_item| {
-                                match visible_item {
-                                    FsEntry::Directory(worktree_id, dir_entry) => {
-                                        let parent_id = back_to_common_visited_parent(
-                                            &mut visited_dirs,
-                                            worktree_id,
-                                            dir_entry,
-                                        );
-
-                                        visited_dirs.push((dir_entry.id, dir_entry.path.clone()));
-                                        let depth = if root_entries.contains(&dir_entry.id) {
-                                            0
-                                        } else if auto_fold_dirs {
-                                            let (parent_folded, parent_depth) = match parent_id {
-                                                Some((worktree_id, id)) => (
-                                                    new_unfolded_dirs.get(&worktree_id).map_or(
-                                                        true,
-                                                        |unfolded_dirs| {
-                                                            !unfolded_dirs.contains(&id)
-                                                        },
-                                                    ),
-                                                    new_depth_map
-                                                        .get(&(worktree_id, id))
-                                                        .copied()
-                                                        .unwrap_or(0),
-                                                ),
-
-                                                None => (false, 0),
-                                            };
-
-                                            let children = children_count
+                                        if entry.is_dir() {
+                                            Some(FsEntry::Directory(worktree_id, entry))
+                                        } else {
+                                            let (buffer_id, excerpts) = worktree_excerpts
+                                                .get_mut(&worktree_id)
+                                                .and_then(|worktree_excerpts| {
+                                                    worktree_excerpts.remove(&entry.id)
+                                                })?;
+                                            Some(FsEntry::File(
+                                                worktree_id,
+                                                entry,
+                                                buffer_id,
+                                                excerpts,
+                                            ))
+                                        }
+                                    })
+                                    .collect::<Vec<_>>()
+                            }
+                        })
+                        .collect::<Vec<_>>();
+
+                    let mut visited_dirs = Vec::new();
+                    let mut new_depth_map = HashMap::default();
+                    let new_visible_entries = external_excerpts
+                        .into_iter()
+                        .sorted_by_key(|(id, _)| *id)
+                        .map(|(buffer_id, excerpts)| FsEntry::ExternalFile(buffer_id, excerpts))
+                        .chain(worktree_entries)
+                        .filter(|visible_item| {
+                            match visible_item {
+                                FsEntry::Directory(worktree_id, dir_entry) => {
+                                    let parent_id = back_to_common_visited_parent(
+                                        &mut visited_dirs,
+                                        worktree_id,
+                                        dir_entry,
+                                    );
+
+                                    let depth = if root_entries.contains(&dir_entry.id) {
+                                        0
+                                    } else {
+                                        if auto_fold_dirs {
+                                            let children = new_children_count
                                                 .get(&worktree_id)
                                                 .and_then(|children_count| {
-                                                    children_count
-                                                        .get(&dir_entry.path.to_path_buf())
+                                                    children_count.get(&dir_entry.path)
                                                 })
                                                 .copied()
                                                 .unwrap_or_default();
-                                            let folded = if children.dirs > 1
-                                                || (children.dirs == 1 && children.files > 0)
+
+                                            if !children.may_be_fold_part()
                                                 || (children.dirs == 0
                                                     && visited_dirs
                                                         .last()
                                                         .map(|(parent_dir_id, _)| {
-                                                            root_entries.contains(parent_dir_id)
+                                                            new_unfolded_dirs
+                                                                .get(&worktree_id)
+                                                                .map_or(true, |unfolded_dirs| {
+                                                                    unfolded_dirs
+                                                                        .contains(&parent_dir_id)
+                                                                })
                                                         })
                                                         .unwrap_or(true))
                                             {